Passed
Pull Request — master (#6)
by
unknown
63:55 queued 28:52
created

PhpcsDiff::getOption()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 11
rs 10
cc 3
nc 3
nop 2
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
65
        if ($this->isVerbose) {
66
            if (!$this->baseBranch && !$this->currentBranch) {
67
                $this->climate->comment("Comparing unstaged changes.");
68
            } else {
69
                $this->climate->comment(
70
                    'Comparing branch: "' . $this->baseBranch . '" against: "' . $this->currentBranch . '"'
71
                );
72
            }
73
        }
74
    }
75
76
    protected function getArgument($index, $default = null)
77
    {
78
        $arguments = array_filter($this->argv, function ($val) {
79
            return strpos($val, '-') === false;
80
        });
81
82
        return isset($arguments[$index]) ? $arguments[$index] : $default;
83
    }
84
85
    protected function getOption($name, $default = null)
86
    {
87
        foreach ($this->argv as $arg) {
88
            list($flag, $val) = explode('=', $arg);
89
90
            if ($flag == $name) {
91
                return $val;
92
            }
93
        }
94
95
        return $default;
96
    }
97
98
    /**
99
     * @param string $flag
100
     * @return bool
101
     */
102
    protected function isFlagSet($flag)
103
    {
104
        return in_array($flag, array_values($this->argv));
105
    }
106
107
    /**
108
     * @param int $exitCode
109
     */
110
    protected function setExitCode($exitCode)
111
    {
112
        if (!is_int($exitCode)) {
0 ignored issues
show
introduced by
The condition is_int($exitCode) is always true.
Loading history...
113
            throw new \UnexpectedValueException('The exit code provided is not a valid integer.');
114
        }
115
116
        $this->exitCode = $exitCode;
117
    }
118
119
    /**
120
     * @return int
121
     */
122
    public function getExitCode()
123
    {
124
        return $this->exitCode;
125
    }
126
127
    /**
128
     * @todo Automatically look at server envs for the travis base branch, if not provided?
129
     */
130
    public function run()
131
    {
132
        try {
133
            $filter = new Filter([new PhpFileRule()], $this->getChangedFiles());
134
        } catch (FilterException $exception) {
135
            $this->error($exception->getMessage());
136
            return;
137
        }
138
139
        $fileDiff = $filter->filter()->getFilteredData();
140
141
        if (empty($fileDiff)) {
142
            $this->climate->info('No difference to compare.');
143
            return;
144
        }
145
146
        if ($this->isVerbose) {
147
            $fileDiffCount = count($fileDiff);
148
            $this->climate->comment(
149
                'Checking ' . $fileDiffCount . ' file(s) for violations.'
150
            );
151
        }
152
153
        $phpcsOutput = $this->runPhpcs($fileDiff, $this->ruleset);
154
155
        if (is_null($phpcsOutput)) {
156
            $this->error('Unable to run phpcs executable.');
157
            return;
158
        }
159
160
        if ($this->isVerbose) {
161
            $this->climate->comment('Filtering phpcs output.');
162
        }
163
164
        try {
165
            $filter = new Filter([new HasMessagesRule()], $phpcsOutput['files']);
166
        } catch (FilterException $exception) {
167
            $this->error($exception->getMessage());
168
            return;
169
        }
170
171
        $files = $filter->filter()->getFilteredData();
172
173
        if ($this->isVerbose) {
174
            $this->climate->comment('Getting changed lines from git diff.');
175
        }
176
177
        $changedLinesPerFile = $this->getChangedLinesPerFile($files);
178
179
        if ($this->isVerbose) {
180
            $this->climate->comment('Comparing phpcs output with changes lines from git diff.');
181
        }
182
183
        $violations = (new PhpcsViolationsMapper(
184
            $changedLinesPerFile,
185
            getcwd()
186
        ))->map($files);
187
188
        if ($this->isVerbose) {
189
            $this->climate->comment('Preparing report.');
190
        }
191
192
        if (empty($violations)) {
193
            $this->climate->info('No violations to report.');
194
            return;
195
        }
196
197
        $this->outputViolations($violations);
198
    }
199
200
    /**
201
     * Run phpcs on a list of files passed into the method
202
     *
203
     * @param array $files
204
     * @param string $ruleset
205
     * @return mixed
206
     */
207
    protected function runPhpcs(array $files = [], $ruleset = 'ruleset.xml')
208
    {
209
        $exec = 'vendor/bin/phpcs';
210
211
        if (is_file(__DIR__ . '/../../../bin/phpcs')) {
212
            $exec = realpath(__DIR__ . '/../../../bin/phpcs');
213
        } elseif (is_file(__DIR__ . '/../bin/phpcs')) {
214
            $exec = realpath(__DIR__ . '/../bin/phpcs');
215
        }
216
217
        return json_decode(
218
            shell_exec($exec . ' --report=json --standard=' . $ruleset . ' ' . implode(' ', $files)),
219
            true
220
        );
221
    }
222
223
    /**
224
     * @param array $output
225
     */
226
    protected function outputViolations(array $output)
227
    {
228
        $this->climate->flank(strtoupper('Start of phpcs check'), '#', 10)->br();
229
        $this->climate->out(implode(PHP_EOL, $output));
230
        $this->climate->flank(strtoupper('End of phpcs check'), '#', 11)->br();
231
232
        $this->error('Violations have been reported.');
233
    }
234
235
    /**
236
     * Returns a list of files which are within the diff based on the current branch
237
     *
238
     * @return array
239
     */
240
    protected function getChangedFiles()
241
    {
242
        // Get a list of changed files (not including deleted files)
243
        $output = shell_exec(
244
            'git diff ' . $this->baseBranch . ' ' . $this->currentBranch . ' --name-only --diff-filter=d'
245
        );
246
247
        // Convert files into an array
248
        $output = explode(PHP_EOL, $output);
249
250
        // Remove any empty values
251
        return array_filter($output);
252
    }
253
254
    /**
255
     * Extract the changed lines for each file from the git diff output
256
     *
257
     * @param array $files
258
     * @return array
259
     */
260
    protected function getChangedLinesPerFile(array $files)
261
    {
262
        $extract = [];
263
        $pattern = '/@@ -[0-9]+(?:,[0-9]+)? \+([0-9]+)(?:,([0-9]+))? @@/';
264
265
        foreach ($files as $file => $data) {
266
            $command = 'git diff -U0 ' . $this->baseBranch . ' ' . $this->currentBranch . ' ' . $file;
267
            $output = shell_exec($command);
268
            preg_match_all($pattern, $output, $lineDiff);
269
            $lines = array_values($lineDiff[0]);
270
271
            $linesChanged = [];
272
273
            foreach ($lines as $line) {
274
                preg_match($pattern, $line, $matches);
275
276
277
                $start = $end = (int)$matches[1];
278
279
                // Multiple lines were changed, so we need to calculate the end line
280
                if (isset($matches[2])) {
281
                    $length = (int)$matches[2];
282
                    $end = $start + $length - 1;
283
                }
284
285
                foreach (range($start, $end) as $l) {
286
                    $linesChanged[$l] = null;
287
                }
288
            }
289
290
            $extract[$file] = array_keys($linesChanged);
291
        }
292
293
        return $extract;
294
    }
295
296
    /**
297
     * @param string $message
298
     * @param int $exitCode
299
     */
300
    protected function error($message, $exitCode = 1)
301
    {
302
        $this->climate->error($message);
303
        $this->setExitCode($exitCode);
304
    }
305
}
306