Passed
Pull Request — master (#6)
by
unknown
33:00
created

PhpcsDiff::setExitCode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 7
rs 10
cc 2
nc 2
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
    const DEFAULT_RULESET = 'ruleset.xml';
15
16
    /**
17
     * @var array
18
     */
19
    protected $argv = [];
20
21
    /**
22
     * @var CLImate
23
     */
24
    protected $climate;
25
26
    /**
27
     * @var bool
28
     */
29
    protected $isVerbose = false;
30
31
    /**
32
     * @var int
33
     */
34
    protected $exitCode = 0;
35
36
    /**
37
     * @var string
38
     */
39
    protected $baseBranch;
40
41
    /**
42
     * @var string
43
     */
44
    protected $currentBranch = '';
45
46
    /**
47
     * @param array $argv
48
     * @param CLImate $climate
49
     */
50
    public function __construct(array $argv, CLImate $climate)
51
    {
52
        $this->argv = $argv;
53
        $this->climate = $climate;
54
55
        if ($this->isFlagSet('-v')) {
56
            $this->climate->comment('Running in verbose mode.');
57
            $this->isVerbose = true;
58
        }
59
60
        $this->ruleset = $this->getOption('--ruleset', self::DEFAULT_RULESET);
0 ignored issues
show
Bug Best Practice introduced by
The property ruleset does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
61
62
        $this->baseBranch = $this->getArgument(1, '');
63
        $this->currentBranch = $this->getArgument(2, '');
64
        if ($this->isVerbose) {
65
            $this->climate->comment(
66
                'Comparing branch: "' . $this->baseBranch . '" against: "' . $this->currentBranch . '"'
67
            );
68
        }
69
    }
70
71
    protected function getArgument($index, $default = null)
72
    {
73
        $arguments = array_filter($this->argv, function ($val) {
74
            return strpos($val, '-') === false;
75
        });
76
77
        return isset($arguments[$index]) ? $arguments[$index] : $default;
78
    }
79
80
    protected function getOption($name, $default = null)
81
    {
82
        foreach ($this->argv as $arg) {
83
            list($flag, $val) = explode('=', $arg);
84
85
            if ($flag == $name) {
86
                return $val;
87
            }
88
        }
89
90
        return $default;
91
    }
92
93
    /**
94
     * @param string $flag
95
     * @return bool
96
     */
97
    protected function isFlagSet($flag)
98
    {
99
        return in_array($flag, array_values($this->argv));
100
    }
101
102
    /**
103
     * @param int $exitCode
104
     */
105
    protected function setExitCode($exitCode)
106
    {
107
        if (!is_int($exitCode)) {
0 ignored issues
show
introduced by
The condition is_int($exitCode) is always true.
Loading history...
108
            throw new \UnexpectedValueException('The exit code provided is not a valid integer.');
109
        }
110
111
        $this->exitCode = $exitCode;
112
    }
113
114
    /**
115
     * @return int
116
     */
117
    public function getExitCode()
118
    {
119
        return $this->exitCode;
120
    }
121
122
    /**
123
     * @todo Automatically look at server envs for the travis base branch, if not provided?
124
     */
125
    public function run()
126
    {
127
        try {
128
            $filter = new Filter([new PhpFileRule()], $this->getChangedFiles());
129
        } catch (FilterException $exception) {
130
            $this->error($exception->getMessage());
131
            return;
132
        }
133
134
        $fileDiff = $filter->filter()->getFilteredData();
135
136
        if (empty($fileDiff)) {
137
            $this->climate->info('No difference to compare.');
138
            return;
139
        }
140
141
        if ($this->isVerbose) {
142
            $fileDiffCount = count($fileDiff);
143
            $this->climate->comment(
144
                'Checking ' . $fileDiffCount . ' file(s) for violations.'
145
            );
146
        }
147
148
        $phpcsOutput = $this->runPhpcs($fileDiff, $this->ruleset);
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
     * @param string $ruleset
200
     * @return mixed
201
     */
202
    protected function runPhpcs(array $files = [], $ruleset = 'ruleset.xml')
203
    {
204
        $exec = 'vendor/bin/phpcs';
205
206
        if (is_file(__DIR__ . '/../../../bin/phpcs')) {
207
            $exec = realpath(__DIR__ . '/../../../bin/phpcs');
208
        } elseif (is_file(__DIR__ . '/../bin/phpcs')) {
209
            $exec = realpath(__DIR__ . '/../bin/phpcs');
210
        }
211
212
        return json_decode(
213
            shell_exec($exec . ' --report=json --standard=' . $ruleset . ' ' . implode(' ', $files)),
214
            true
215
        );
216
    }
217
218
    /**
219
     * @param array $output
220
     */
221
    protected function outputViolations(array $output)
222
    {
223
        $this->climate->flank(strtoupper('Start of phpcs check'), '#', 10)->br();
224
        $this->climate->out(implode(PHP_EOL, $output));
225
        $this->climate->flank(strtoupper('End of phpcs check'), '#', 11)->br();
226
227
        $this->error('Violations have been reported.');
228
    }
229
230
    /**
231
     * Returns a list of files which are within the diff based on the current branch
232
     *
233
     * @return array
234
     */
235
    protected function getChangedFiles()
236
    {
237
        // Get a list of changed files (not including deleted files)
238
        $output = shell_exec(
239
            'git diff ' . $this->baseBranch . ' ' . $this->currentBranch . ' --name-only --diff-filter=d'
240
        );
241
242
        // Convert files into an array
243
        $output = explode(PHP_EOL, $output);
244
245
        // Remove any empty values
246
        return array_filter($output);
247
    }
248
249
    /**
250
     * Extract the changed lines for each file from the git diff output
251
     *
252
     * @param array $files
253
     * @return array
254
     */
255
    protected function getChangedLinesPerFile(array $files)
256
    {
257
        $extract = [];
258
        $pattern = '/@@ -[0-9]+(?:,[0-9]+)? \+([0-9]+)(?:,([0-9]+))? @@/';
259
260
        foreach ($files as $file => $data) {
261
            $command = 'git diff -U0 ' . $this->baseBranch . ' ' . $this->currentBranch . ' ' . $file;
262
            $output = shell_exec($command);
263
            preg_match_all($pattern, $output, $lineDiff);
264
            $lines = array_values($lineDiff[0]);
265
266
            $linesChanged = [];
267
268
            foreach ($lines as $line) {
269
                preg_match($pattern, $line, $matches);
270
271
272
                $start = $end = (int)$matches[1];
273
274
                // Multiple lines were changed, so we need to calculate the end line
275
                if (isset($matches[2])) {
276
                    $length = (int)$matches[2];
277
                    $end = $start + $length - 1;
278
                }
279
280
                foreach (range($start, $end) as $l) {
281
                    $linesChanged[$l] = null;
282
                }
283
            }
284
285
            $extract[$file] = array_keys($linesChanged);
286
        }
287
288
        return $extract;
289
    }
290
291
    /**
292
     * @param string $message
293
     * @param int $exitCode
294
     */
295
    protected function error($message, $exitCode = 1)
296
    {
297
        $this->climate->error($message);
298
        $this->setExitCode($exitCode);
299
    }
300
}
301