Passed
Pull Request — master (#11)
by Tiago
03:09
created

PhpcsDiff::getFlag()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 10
c 4
b 0
f 0
dl 0
loc 20
rs 9.6111
cc 5
nc 8
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
98
                break;
99
            }
100
        }
101
102
        if ($return) {
103
            $this->argv = array_values($argv);
104
        }
105
106
        return $return;
107
    }
108
109
    /**
110
     * @param int $exitCode
111
     */
112
    protected function setExitCode(int $exitCode)
113
    {
114
        $this->exitCode = $exitCode;
115
    }
116
117
    /**
118
     * @return int
119
     */
120
    public function getExitCode(): int
121
    {
122
        return $this->exitCode;
123
    }
124
125
    /**
126
     * @todo Automatically look at server envs for the travis base branch, if not provided?
127
     */
128
    public function run(): void
129
    {
130
        try {
131
            $filter = new Filter([new PhpFileRule()], $this->getChangedFiles());
132
        } catch (FilterException $exception) {
133
            $this->error($exception->getMessage());
134
            return;
135
        }
136
137
        $fileDiff = $filter->filter()->getFilteredData();
138
139
        if (empty($fileDiff)) {
140
            $this->climate->info('No difference to compare.');
141
            return;
142
        }
143
144
        if ($this->isVerbose) {
145
            $fileDiffCount = count($fileDiff);
146
            $this->climate->comment(
147
                'Checking ' . $fileDiffCount . ' ' .
148
                ngettext('file', 'files', $fileDiffCount) . ' for violations.'
149
            );
150
        }
151
152
        $phpcsOutput = $this->runPhpcs($fileDiff);
153
154
        if (is_null($phpcsOutput)) {
155
            $this->error('Unable to run phpcs executable.');
156
            return;
157
        }
158
159
        if ($this->isVerbose) {
160
            $this->climate->comment('Filtering phpcs output.');
161
        }
162
163
        try {
164
            $filter = new Filter([new HasMessagesRule()], $phpcsOutput['files']);
165
        } catch (FilterException $exception) {
166
            $this->error($exception->getMessage());
167
            return;
168
        }
169
170
        $files = $filter->filter()->getFilteredData();
171
172
        if ($this->isVerbose) {
173
            $this->climate->comment('Getting changed lines from git diff.');
174
        }
175
176
        $changedLinesPerFile = $this->getChangedLinesPerFile($files);
177
178
        if ($this->isVerbose) {
179
            $this->climate->comment('Comparing phpcs output with changes lines from git diff.');
180
        }
181
182
        $violations = (new PhpcsViolationsMapper(
183
            $changedLinesPerFile,
184
            getcwd()
185
        ))->map($files);
186
187
        if ($this->isVerbose) {
188
            $this->climate->comment('Preparing report.');
189
        }
190
191
        if (empty($violations)) {
192
            $this->climate->info('No violations to report.');
193
            return;
194
        }
195
196
        $this->outputViolations($violations);
197
    }
198
199
    /**
200
     * Run phpcs on a list of files passed into the method
201
     *
202
     * @param array $files
203
     * @return mixed
204
     */
205
    protected function runPhpcs(array $files = [])
206
    {
207
        $exec = null;
208
        $root = dirname(__DIR__);
209
210
        $locations = [
211
            'vendor/bin/phpcs',
212
            $root . '/../../bin/phpcs',
213
            $root . '/../bin/phpcs',
214
            $root . '/bin/phpcs',
215
            $root . '/vendor/bin/phpcs',
216
            '~/.config/composer/vendor/bin/phpcs',
217
            '~/.composer/vendor/bin/phpcs',
218
        ];
219
220
        foreach ($locations as $location) {
221
            if (is_file($location)) {
222
                $exec = $location;
223
                break;
224
            }
225
        }
226
227
        if (!$exec) {
228
            return null;
229
        }
230
231
        if ($this->isVerbose) {
232
            $this->climate->info('Using phpcs executable: ' . $exec);
233
        }
234
235
        $exec = PHP_BINARY . ' ' . $exec;
236
        $command = $exec . ' --report=json --standard=' . $this->standard . ' ' . implode(' ', $files);
237
        $output = shell_exec($command);
238
239
        if ($this->isVerbose) {
240
            $this->climate->info('Running: ' . $command);
241
        }
242
243
        $json = $output ? json_decode($output, true) : null;
244
        if ($json === null && $output) {
245
            $this->climate->error($output);
246
        }
247
248
        return $json;
249
    }
250
251
    /**
252
     * @param array $output
253
     */
254
    protected function outputViolations(array $output): void
255
    {
256
        $this->climate->flank(strtoupper('Start of phpcs check'), '#', 10)->br();
257
        $this->climate->out(implode(PHP_EOL, $output));
258
        $this->climate->flank(strtoupper('End of phpcs check'), '#', 11)->br();
259
260
        $this->error('Violations have been reported.');
261
    }
262
263
    /**
264
     * Returns a list of files which are within the diff based on the current branch
265
     *
266
     * @return array
267
     */
268
    protected function getChangedFiles(): array
269
    {
270
        // Get a list of changed files (not including deleted files)
271
        $output = shell_exec(
272
            'git diff ' . $this->baseBranch . ' ' . $this->currentBranch . ' --name-only --diff-filter=ACM'
273
        );
274
275
        // Convert files into an array
276
        $output = explode(PHP_EOL, $output);
277
278
        // Remove any empty values
279
        return array_filter($output);
280
    }
281
282
    /**
283
     * Extract the changed lines for each file from the git diff output
284
     *
285
     * @param array $files
286
     * @return array
287
     */
288
    protected function getChangedLinesPerFile(array $files): array
289
    {
290
        $extract = [];
291
        $pattern = [
292
            'basic' => '^@@ (.*) @@',
293
            'specific' => '@@ -[0-9]+(?:,[0-9]+)? \+([0-9]+)(?:,([0-9]+))? @@',
294
        ];
295
296
        foreach ($files as $file => $data) {
297
            $command = 'git diff -U0 ' . $this->baseBranch . ' ' . $this->currentBranch . ' ' . $file .
298
                ' | grep -E ' . escapeshellarg($pattern['basic']);
299
300
            $lineDiff = shell_exec($command);
301
            $lines = array_filter(explode(PHP_EOL, $lineDiff));
302
            $linesChanged = [];
303
304
            foreach ($lines as $line) {
305
                preg_match('/' . $pattern['specific'] . '/', $line, $matches);
306
307
                // If there were no specific matches, skip this line
308
                if ([] === $matches) {
309
                    continue;
310
                }
311
312
                $start = $end = (int)$matches[1];
313
314
                // Multiple lines were changed, so we need to calculate the end line
315
                if (isset($matches[2])) {
316
                    $length = (int)$matches[2];
317
                    $end = $start + $length - 1;
318
                }
319
320
                foreach (range($start, $end) as $l) {
321
                    $linesChanged[$l] = null;
322
                }
323
            }
324
325
            $extract[$file] = array_keys($linesChanged);
326
        }
327
328
        return $extract;
329
    }
330
331
    /**
332
     * @param string $message
333
     * @param int $exitCode
334
     */
335
    protected function error(string $message, int $exitCode = 1): void
336
    {
337
        $this->climate->error($message);
338
        $this->setExitCode($exitCode);
339
    }
340
}
341