Passed
Pull Request — master (#3)
by Oliver
56:04 queued 21:02
created

PhpcsDiff::getChangedLinesPerFile()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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