1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* RunCommand |
5
|
|
|
* |
6
|
|
|
* PHP Version 5.3.0 |
7
|
|
|
* |
8
|
|
|
* Copyright (c) 2007-2014, Mayflower GmbH |
9
|
|
|
* All rights reserved. |
10
|
|
|
* |
11
|
|
|
* Redistribution and use in source and binary forms, with or without |
12
|
|
|
* modification, are permitted provided that the following conditions |
13
|
|
|
* are met: |
14
|
|
|
* |
15
|
|
|
* * Redistributions of source code must retain the above copyright |
16
|
|
|
* notice, this list of conditions and the following disclaimer. |
17
|
|
|
* |
18
|
|
|
* * Redistributions in binary form must reproduce the above copyright |
19
|
|
|
* notice, this list of conditions and the following disclaimer in |
20
|
|
|
* the documentation and/or other materials provided with the |
21
|
|
|
* distribution. |
22
|
|
|
* |
23
|
|
|
* * Neither the name of Mayflower GmbH nor the names of his |
24
|
|
|
* contributors may be used to endorse or promote products derived |
25
|
|
|
* from this software without specific prior written permission. |
26
|
|
|
* |
27
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
28
|
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
29
|
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
30
|
|
|
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
31
|
|
|
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
32
|
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
33
|
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
34
|
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
35
|
|
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
36
|
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
37
|
|
|
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
38
|
|
|
* POSSIBILITY OF SUCH DAMAGE. |
39
|
|
|
* |
40
|
|
|
* @category PHP_CodeBrowser |
41
|
|
|
* |
42
|
|
|
* @author Robin Gloster <[email protected]> |
43
|
|
|
* |
44
|
|
|
* @copyright 2007-2010 Mayflower GmbH |
45
|
|
|
* |
46
|
|
|
* @license http://www.opensource.org/licenses/bsd-license.php BSD License |
47
|
|
|
* |
48
|
|
|
* @version SVN: $Id$ |
49
|
|
|
* |
50
|
|
|
* @link http://www.phpunit.de/ |
51
|
|
|
* |
52
|
|
|
* @since File available since 1.1 |
53
|
|
|
*/ |
54
|
|
|
|
55
|
|
|
namespace PHPCodeBrowser\Command; |
56
|
|
|
|
57
|
|
|
use Exception; |
58
|
|
|
use Monolog\Handler\NullHandler; |
59
|
|
|
use Monolog\Logger; |
60
|
|
|
use PHPCodeBrowser\CLIController; |
61
|
|
|
use PHPCodeBrowser\Helper\IOHelper; |
62
|
|
|
use PHPCodeBrowser\Plugins\ErrorCPD; |
63
|
|
|
use PHPCodeBrowser\Plugins\ErrorCRAP; |
64
|
|
|
use PHPCodeBrowser\Plugins\ErrorCheckstyle; |
65
|
|
|
use PHPCodeBrowser\Plugins\ErrorCoverage; |
66
|
|
|
use PHPCodeBrowser\Plugins\ErrorPMD; |
67
|
|
|
use PHPCodeBrowser\Plugins\ErrorPadawan; |
68
|
|
|
use Symfony\Component\Console\Command\Command; |
69
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
70
|
|
|
use Symfony\Component\Console\Input\InputOption; |
71
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Class RunCommand |
75
|
|
|
*/ |
76
|
|
|
class RunCommand extends Command |
77
|
|
|
{ |
78
|
|
|
/** |
79
|
|
|
* |
80
|
|
|
*/ |
81
|
|
|
protected function configure(): void |
82
|
|
|
{ |
83
|
|
|
$plugins = \array_map( |
84
|
|
|
static function ($class) { |
85
|
|
|
return '"'.\substr($class, \strlen('Error')).'"'; |
86
|
|
|
}, |
87
|
|
|
$this->getAvailablePlugins() |
88
|
|
|
); |
89
|
|
|
|
90
|
|
|
$this->setName('phpcb') |
91
|
|
|
->setHelp( |
92
|
|
|
'A Code browser for PHP files with syntax highlighting and colored error-sections found by quality assurance tools like PHPUnit, PHPMD or PHP_CodeSniffer.' |
93
|
|
|
)->addOption( |
94
|
|
|
'log', |
95
|
|
|
'l', |
96
|
|
|
InputOption::VALUE_REQUIRED, |
97
|
|
|
'The path to the xml log files, e.g. generated from PHPUnit. Either this or --source must be given' |
98
|
|
|
)->addOption( |
99
|
|
|
'extensions', |
100
|
|
|
'S', |
101
|
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
102
|
|
|
'PHP file extensions to include. Can be given multiple times' |
103
|
|
|
)->addOption( |
104
|
|
|
'output', |
105
|
|
|
'o', |
106
|
|
|
InputOption::VALUE_REQUIRED, |
107
|
|
|
'Path to the output folder where generated files should be stored' |
108
|
|
|
)->addOption( |
109
|
|
|
'source', |
110
|
|
|
's', |
111
|
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
112
|
|
|
'Path to the project source code. Can either be a directory or a single file. Parse complete source directory if set, else only files found in logs. Either this or --log must be given. Can be given multiple times' |
113
|
|
|
)->addOption( |
114
|
|
|
'ignore', |
115
|
|
|
'i', |
116
|
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
117
|
|
|
'Files or directories that will be ignored during the parsing process. Can be given multiple times' |
118
|
|
|
)->addOption( |
119
|
|
|
'exclude', |
120
|
|
|
'e', |
121
|
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
122
|
|
|
'Excludes all files matching the given glob pattern. This is done after pulling the files in the source dir in if one is given. Can be given multiple times. Note that the match is run against absolute file names' |
123
|
|
|
)->addOption( |
124
|
|
|
'excludePCRE', |
125
|
|
|
'E', |
126
|
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
127
|
|
|
'Works like -e but takes PCRE instead of glob patterns' |
128
|
|
|
)->addOption( |
129
|
|
|
'debugExcludes', |
130
|
|
|
null, |
131
|
|
|
InputOption::VALUE_NONE, |
132
|
|
|
'Print which files are excluded by which expressions and patterns' |
133
|
|
|
)->addOption( |
134
|
|
|
'excludeOK', |
135
|
|
|
null, |
136
|
|
|
InputOption::VALUE_NONE, |
137
|
|
|
'Exclude files with no issues from the report' |
138
|
|
|
)->addOption( |
139
|
|
|
'disablePlugin', |
140
|
|
|
null, |
141
|
|
|
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
142
|
|
|
'Disable single Plugins. Can be one of '.\implode(', ', $plugins) |
143
|
|
|
)->addOption( |
144
|
|
|
'crapThreshold', |
145
|
|
|
null, |
146
|
|
|
InputOption::VALUE_REQUIRED, |
147
|
|
|
'The minimum value for CRAP errors to be recognized. Defaults to 0. Regardless of this setting, values below 30 will be considered notices, those above warnings' |
148
|
|
|
); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Executes the current command. |
153
|
|
|
* |
154
|
|
|
* @param InputInterface $input An InputInterface instance |
155
|
|
|
* @param OutputInterface $output |
156
|
|
|
* |
157
|
|
|
* @phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter |
158
|
|
|
* |
159
|
|
|
* @return int 0 if everything went fine, or an error code |
160
|
|
|
*/ |
161
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int |
162
|
|
|
{ |
163
|
|
|
$this->checkErrors($input); |
164
|
|
|
|
165
|
|
|
$extensions = $this->handleBackwardCompatibility($input->getOption('extensions')); |
166
|
|
|
$ignore = $this->handleBackwardCompatibility($input->getOption('ignore')); |
167
|
|
|
|
168
|
|
|
$excludePCREParam = (array) $input->getOption('excludePCRE'); |
169
|
|
|
$excludePCRE = $this->convertIgnores($ignore, $excludePCREParam); |
170
|
|
|
|
171
|
|
|
$logger = new Logger('PHPCodeBrowser'); |
172
|
|
|
|
173
|
|
|
if (!$input->getOption('debugExcludes')) { |
174
|
|
|
$logger->pushHandler(new NullHandler()); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// init new CLIController |
178
|
|
|
$controller = new CLIController( |
179
|
|
|
$input->getOption('log'), |
180
|
|
|
$input->getOption('source'), |
181
|
|
|
$input->getOption('output'), |
182
|
|
|
$excludePCRE, |
183
|
|
|
$input->getOption('exclude'), |
184
|
|
|
['CRAP' => ['threshold' => $input->getOption('crapThreshold')]], |
185
|
|
|
new IOHelper(), |
186
|
|
|
$logger, |
187
|
|
|
\array_merge($extensions, ['php']), |
188
|
|
|
(bool) $input->getOption('excludeOK') |
189
|
|
|
); |
190
|
|
|
|
191
|
|
|
$plugins = $this->getAvailablePlugins(); |
192
|
|
|
$plugins = $this->disablePlugins((array) $input->getOption('disablePlugin'), $plugins); |
193
|
|
|
$controller->addErrorPlugins($plugins); |
194
|
|
|
|
195
|
|
|
try { |
196
|
|
|
$controller->run(); |
197
|
|
|
} catch (Exception $e) { |
198
|
|
|
\error_log( |
199
|
|
|
<<<HERE |
200
|
|
|
[Error] {$e->getMessage()} |
201
|
|
|
|
202
|
|
|
{$e->getTraceAsString()} |
203
|
|
|
HERE |
204
|
|
|
); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
return 0; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @param InputInterface $input |
212
|
|
|
* |
213
|
|
|
* @throws \InvalidArgumentException if errors are found |
214
|
|
|
*/ |
215
|
|
|
protected function checkErrors(InputInterface $input): void |
216
|
|
|
{ |
217
|
|
|
if (!$input->getOption('log')) { |
218
|
|
|
if (!$input->getOption('source')) { |
219
|
|
|
throw new \InvalidArgumentException('Missing log or source argument.'); |
220
|
|
|
} |
221
|
|
|
} elseif (!\file_exists((string) $input->getOption('log'))) { |
222
|
|
|
throw new \InvalidArgumentException('Log directory does not exist.'); |
223
|
|
|
} elseif (!\is_dir((string) $input->getOption('log'))) { |
224
|
|
|
throw new \InvalidArgumentException('Log argument must be a directory, a file was given.'); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
if ($input->getOption('source')) { |
228
|
|
|
foreach ($input->getOption('source') as $s) { |
229
|
|
|
if (!\file_exists($s)) { |
230
|
|
|
throw new \InvalidArgumentException("Source '{$s}' does not exist"); |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if (!$input->getOption('output')) { |
236
|
|
|
throw new \InvalidArgumentException('Missing output argument.'); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
if (\file_exists((string) $input->getOption('output')) && !\is_dir((string) $input->getOption('output'))) { |
240
|
|
|
throw new \InvalidArgumentException('Output argument must be a directory, a file was given.'); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* Returns a list of available plugins. |
246
|
|
|
* |
247
|
|
|
* Currently hard-coded. |
248
|
|
|
* |
249
|
|
|
* @return array<string> Class names of error plugins |
250
|
|
|
*/ |
251
|
|
|
protected function getAvailablePlugins(): array |
252
|
|
|
{ |
253
|
|
|
return [ |
254
|
|
|
ErrorCheckstyle::class, |
255
|
|
|
ErrorPMD::class, |
256
|
|
|
ErrorCPD::class, |
257
|
|
|
ErrorPadawan::class, |
258
|
|
|
ErrorCoverage::class, |
259
|
|
|
ErrorCRAP::class, |
260
|
|
|
]; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* @param array $disabledPlugins |
265
|
|
|
* @param array $plugins |
266
|
|
|
* |
267
|
|
|
* @return array |
268
|
|
|
*/ |
269
|
|
|
protected function disablePlugins(array $disabledPlugins, array $plugins): array |
270
|
|
|
{ |
271
|
|
|
$disabledPlugins = \array_map( |
272
|
|
|
'strtolower', |
273
|
|
|
$disabledPlugins |
274
|
|
|
); |
275
|
|
|
|
276
|
|
|
foreach ($plugins as $pluginKey => $plugin) { |
277
|
|
|
$name = \substr($plugin, \strlen('Error')); |
278
|
|
|
|
279
|
|
|
if (!\in_array(\strtolower($name), $disabledPlugins, true)) { |
280
|
|
|
continue; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
// Remove it from the plugins list |
284
|
|
|
unset($plugins[$pluginKey]); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
return $plugins; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Convert the --ignore arguments to patterns |
292
|
|
|
* |
293
|
|
|
* @param array $ignored |
294
|
|
|
* @param array $excludePCRE |
295
|
|
|
* |
296
|
|
|
* @return array |
297
|
|
|
*/ |
298
|
|
|
protected function convertIgnores(array $ignored, array $excludePCRE): array |
299
|
|
|
{ |
300
|
|
|
$dirSep = \preg_quote(DIRECTORY_SEPARATOR, '/'); |
301
|
|
|
|
302
|
|
|
foreach ($ignored as $ignore) { |
303
|
|
|
$ig = \realpath($ignore); |
304
|
|
|
|
305
|
|
|
if (!$ig) { |
306
|
|
|
\error_log("[Warning] {$ignore} does not exists"); |
307
|
|
|
} else { |
308
|
|
|
$ig = \preg_quote($ig, '/'); |
309
|
|
|
$excludePCRE[] = "/^{$ig}({$dirSep}|$)/"; |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
return $excludePCRE; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* This converts comma-separated options into an array |
318
|
|
|
* |
319
|
|
|
* @param array $option |
320
|
|
|
* |
321
|
|
|
* @return array |
322
|
|
|
*/ |
323
|
|
|
private function handleBackwardCompatibility(array $option): array |
324
|
|
|
{ |
325
|
|
|
if (\count($option) === 1 && \str_contains($option[0], ',')) { |
326
|
|
|
$option = \explode(',', $option[0]); |
327
|
|
|
\error_log('Usage of comma-separated options is deprecated, specify them one-by-one.', E_DEPRECATED); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
return $option; |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|