Passed
Pull Request — master (#11)
by Tiago
21:56
created

PhpcsDiff::getFlag()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 16
rs 9.9666
cc 4
nc 4
nop 1
1
<?php
2
3
namespace PhpcsDiff;
4
5
use League\CLImate\CLImate;
6
use PhpcsDiff\Filter\Exception\FilterException;
7
use PhpcsDiff\Filter\Filter;
8
use PhpcsDiff\Filter\Rule\HasMessagesRule;
9
use PhpcsDiff\Filter\Rule\PhpFileRule;
10
use PhpcsDiff\Mapper\PhpcsViolationsMapper;
11
12
class PhpcsDiff
13
{
14
    /**
15
     * @var array
16
     */
17
    protected $argv = [];
18
19
    /**
20
     * @var CLImate
21
     */
22
    protected $climate;
23
24
    /**
25
     * @var bool
26
     */
27
    protected $isVerbose = false;
28
29
    /**
30
     * @var int
31
     */
32
    protected $exitCode = 0;
33
34
    /**
35
     * @var string
36
     */
37
    protected $baseBranch;
38
39
    /**
40
     * @var string
41
     */
42
    protected $currentBranch = '';
43
44
    /**
45
     * @var string
46
     */
47
    protected $standard = 'ruleset.xml';
48
49
    /**
50
     * @param array $argv
51
     * @param CLImate $climate
52
     */
53
    public function __construct(array $argv, CLImate $climate)
54
    {
55
        $this->argv = $argv;
56
        $this->climate = $climate;
57
58
        if ($this->getFlag('-v')) {
59
            $this->climate->comment('Running in verbose mode.');
60
            $this->isVerbose = true;
61
        }
62
63
        $standard = $this->getFlag('--standard');
64
        if (!empty($standard) && is_string($standard)) {
65
            $this->climate->comment('Custom standard: ' . $standard . PHP_EOL);
66
67
            $this->standard = $standard;
68
        }
69
70
        if (!isset($this->argv[1])) {
71
            $this->error('Please provide a <bold>base branch</bold> as the first argument.');
72
            return;
73
        }
74
75
        $this->baseBranch = 'origin/' . str_replace('origin/', '', $this->argv[1]);
76
        $this->currentBranch = trim(shell_exec('git rev-parse --verify HEAD'));
77
78
        if (empty($this->currentBranch)) {
79
            $this->error('Unable to get <bold>current</bold> branch.');
80
        }
81
    }
82
83
    /**
84
     * @param string $flag
85
     * @return bool|string
86
     */
87
    protected function getFlag(string $flag)
88
    {
89
        $return = false;
90
        $argv = $this->argv;
91
92
        foreach ($argv as $key => $arg) {
93
            if (strtok($arg, '=') === $flag) {
94
                $return = !empty(strrchr($arg, '=')) ? substr(strrchr($arg, '='), 1) : true;
95
96
                unset($argv[$key]);
97
                $argv = array_values($argv);
98
            }
99
        }
100
101
        $this->argv = $argv;
102
        return $return;
103
    }
104
105
    /**
106
     * @param int $exitCode
107
     */
108
    protected function setExitCode(int $exitCode)
109
    {
110
        $this->exitCode = $exitCode;
111
    }
112
113
    /**
114
     * @return int
115
     */
116
    public function getExitCode(): int
117
    {
118
        return $this->exitCode;
119
    }
120
121
    /**
122
     * @todo Automatically look at server envs for the travis base branch, if not provided?
123
     */
124
    public function run(): void
125
    {
126
        try {
127
            $filter = new Filter([new PhpFileRule()], $this->getChangedFiles());
128
        } catch (FilterException $exception) {
129
            $this->error($exception->getMessage());
130
            return;
131
        }
132
133
        $fileDiff = $filter->filter()->getFilteredData();
134
135
        if (empty($fileDiff)) {
136
            $this->climate->info('No difference to compare.');
137
            return;
138
        }
139
140
        if ($this->isVerbose) {
141
            $fileDiffCount = count($fileDiff);
142
            $this->climate->comment(
143
                'Checking ' . $fileDiffCount . ' ' .
144
                ngettext('file', 'files', $fileDiffCount) . ' for violations.'
145
            );
146
        }
147
148
        $phpcsOutput = $this->runPhpcs($fileDiff);
149
150
        if (is_null($phpcsOutput)) {
151
            $this->error('Unable to run phpcs executable.');
152
            return;
153
        }
154
155
        if ($this->isVerbose) {
156
            $this->climate->comment('Filtering phpcs output.');
157
        }
158
159
        try {
160
            $filter = new Filter([new HasMessagesRule()], $phpcsOutput['files']);
161
        } catch (FilterException $exception) {
162
            $this->error($exception->getMessage());
163
            return;
164
        }
165
166
        $files = $filter->filter()->getFilteredData();
167
168
        if ($this->isVerbose) {
169
            $this->climate->comment('Getting changed lines from git diff.');
170
        }
171
172
        $changedLinesPerFile = $this->getChangedLinesPerFile($files);
173
174
        if ($this->isVerbose) {
175
            $this->climate->comment('Comparing phpcs output with changes lines from git diff.');
176
        }
177
178
        $violations = (new PhpcsViolationsMapper(
179
            $changedLinesPerFile,
180
            getcwd()
181
        ))->map($files);
182
183
        if ($this->isVerbose) {
184
            $this->climate->comment('Preparing report.');
185
        }
186
187
        if (empty($violations)) {
188
            $this->climate->info('No violations to report.');
189
            return;
190
        }
191
192
        $this->outputViolations($violations);
193
    }
194
195
    /**
196
     * Run phpcs on a list of files passed into the method
197
     *
198
     * @param array $files
199
     * @return mixed
200
     */
201
    protected function runPhpcs(array $files = [])
202
    {
203
        $exec = null;
204
        $root = dirname(__DIR__);
205
206
        $locations = [
207
            'vendor/bin/phpcs',
208
            $root . '/../../bin/phpcs',
209
            $root . '/../bin/phpcs',
210
            $root . '/bin/phpcs',
211
            $root . '/vendor/bin/phpcs',
212
            '~/.config/composer/vendor/bin/phpcs',
213
            '~/.composer/vendor/bin/phpcs',
214
        ];
215
216
        foreach ($locations as $location) {
217
            if (is_file($location)) {
218
                $exec = $location;
219
                break;
220
            }
221
        }
222
223
        if (!$exec) {
224
            return null;
225
        }
226
227
        if ($this->isVerbose) {
228
            $this->climate->info('Using phpcs executable: ' . $exec);
229
        }
230
231
        $exec = PHP_BINARY . ' ' . $exec;
232
        $command = $exec . ' --report=json --standard=' . $this->standard . ' ' . implode(' ', $files);
233
        $output = shell_exec($command);
234
235
        if ($this->isVerbose) {
236
            $this->climate->info('Running: ' . $command);
237
        }
238
239
        $json = $output ? json_decode($output, true) : null;
240
        if ($json === null && $output) {
241
            $this->climate->error($output);
242
        }
243
244
        return $json;
245
    }
246
247
    /**
248
     * @param array $output
249
     */
250
    protected function outputViolations(array $output): void
251
    {
252
        $this->climate->flank(strtoupper('Start of phpcs check'), '#', 10)->br();
253
        $this->climate->out(implode(PHP_EOL, $output));
254
        $this->climate->flank(strtoupper('End of phpcs check'), '#', 11)->br();
255
256
        $this->error('Violations have been reported.');
257
    }
258
259
    /**
260
     * Returns a list of files which are within the diff based on the current branch
261
     *
262
     * @return array
263
     */
264
    protected function getChangedFiles(): array
265
    {
266
        // Get a list of changed files (not including deleted files)
267
        $output = shell_exec(
268
            'git diff ' . $this->baseBranch . ' ' . $this->currentBranch . ' --name-only --diff-filter=ACM'
269
        );
270
271
        // Convert files into an array
272
        $output = explode(PHP_EOL, $output);
273
274
        // Remove any empty values
275
        return array_filter($output);
276
    }
277
278
    /**
279
     * Extract the changed lines for each file from the git diff output
280
     *
281
     * @param array $files
282
     * @return array
283
     */
284
    protected function getChangedLinesPerFile(array $files): array
285
    {
286
        $extract = [];
287
        $pattern = [
288
            'basic' => '^@@ (.*) @@',
289
            'specific' => '@@ -[0-9]+(?:,[0-9]+)? \+([0-9]+)(?:,([0-9]+))? @@',
290
        ];
291
292
        foreach ($files as $file => $data) {
293
            $command = 'git diff -U0 ' . $this->baseBranch . ' ' . $this->currentBranch . ' ' . $file .
294
                ' | grep -E ' . escapeshellarg($pattern['basic']);
295
296
            $lineDiff = shell_exec($command);
297
            $lines = array_filter(explode(PHP_EOL, $lineDiff));
298
            $linesChanged = [];
299
300
            foreach ($lines as $line) {
301
                preg_match('/' . $pattern['specific'] . '/', $line, $matches);
302
303
                // If there were no specific matches, skip this line
304
                if ([] === $matches) {
305
                    continue;
306
                }
307
308
                $start = $end = (int)$matches[1];
309
310
                // Multiple lines were changed, so we need to calculate the end line
311
                if (isset($matches[2])) {
312
                    $length = (int)$matches[2];
313
                    $end = $start + $length - 1;
314
                }
315
316
                foreach (range($start, $end) as $l) {
317
                    $linesChanged[$l] = null;
318
                }
319
            }
320
321
            $extract[$file] = array_keys($linesChanged);
322
        }
323
324
        return $extract;
325
    }
326
327
    /**
328
     * @param string $message
329
     * @param int $exitCode
330
     */
331
    protected function error(string $message, int $exitCode = 1): void
332
    {
333
        $this->climate->error($message);
334
        $this->setExitCode($exitCode);
335
    }
336
}
337