This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the overtrue/phplint |
||
5 | * |
||
6 | * (c) overtrue <[email protected]> |
||
7 | * |
||
8 | * This source file is subject to the MIT license that is bundled |
||
9 | * with this source code in the file LICENSE. |
||
10 | */ |
||
11 | |||
12 | namespace Overtrue\PHPLint\Command; |
||
13 | |||
14 | use DateTime; |
||
15 | use Exception; |
||
16 | use JakubOnderka\PhpConsoleColor\ConsoleColor; |
||
17 | use JakubOnderka\PhpConsoleHighlighter\Highlighter; |
||
18 | use N98\JUnitXml\Document; |
||
19 | use Overtrue\PHPLint\Cache; |
||
20 | use Overtrue\PHPLint\Linter; |
||
21 | use Symfony\Component\Console\Command\Command; |
||
22 | use Symfony\Component\Console\Helper\Helper; |
||
23 | use Symfony\Component\Console\Input\InputArgument; |
||
24 | use Symfony\Component\Console\Input\InputInterface; |
||
25 | use Symfony\Component\Console\Input\InputOption; |
||
26 | use Symfony\Component\Console\Output\OutputInterface; |
||
27 | use Symfony\Component\Console\Terminal; |
||
28 | use Symfony\Component\Finder\SplFileInfo; |
||
29 | use Symfony\Component\Yaml\Exception\ParseException; |
||
30 | use Symfony\Component\Yaml\Yaml; |
||
31 | |||
32 | /** |
||
33 | * Class LintCommand. |
||
34 | */ |
||
35 | class LintCommand extends Command |
||
36 | { |
||
37 | /** |
||
38 | * @var array |
||
39 | */ |
||
40 | protected $defaults = [ |
||
41 | 'jobs' => 5, |
||
42 | 'path' => '.', |
||
43 | 'exclude' => [], |
||
44 | 'extensions' => ['php'], |
||
45 | 'warning' => false |
||
46 | ]; |
||
47 | |||
48 | /** |
||
49 | * @var \Symfony\Component\Console\Input\InputInterface |
||
50 | */ |
||
51 | protected $input; |
||
52 | |||
53 | /** |
||
54 | * @var \Symfony\Component\Console\Output\OutputInterface |
||
55 | */ |
||
56 | protected $output; |
||
57 | |||
58 | /** |
||
59 | * Configures the current command. |
||
60 | */ |
||
61 | protected function configure() |
||
62 | { |
||
63 | $this |
||
64 | ->setName('phplint') |
||
65 | ->setDescription('Lint something') |
||
66 | ->addArgument( |
||
67 | 'path', |
||
68 | InputArgument::OPTIONAL | InputArgument::IS_ARRAY, |
||
69 | 'Path to file or directory to lint.' |
||
70 | ) |
||
71 | ->addOption( |
||
72 | 'exclude', |
||
73 | null, |
||
74 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, |
||
75 | 'Path to file or directory to exclude from linting' |
||
76 | ) |
||
77 | ->addOption( |
||
78 | 'extensions', |
||
79 | null, |
||
80 | InputOption::VALUE_REQUIRED, |
||
81 | 'Check only files with selected extensions (default: php)' |
||
82 | ) |
||
83 | ->addOption( |
||
84 | 'jobs', |
||
85 | 'j', |
||
86 | InputOption::VALUE_REQUIRED, |
||
87 | 'Number of parraled jobs to run (default: 5)' |
||
88 | ) |
||
89 | ->addOption( |
||
90 | 'configuration', |
||
91 | 'c', |
||
92 | InputOption::VALUE_REQUIRED, |
||
93 | 'Read configuration from config file (default: ./.phplint.yml).' |
||
94 | ) |
||
95 | ->addOption( |
||
96 | 'no-configuration', |
||
97 | null, |
||
98 | InputOption::VALUE_NONE, |
||
99 | 'Ignore default configuration file (default: ./.phplint.yml).' |
||
100 | ) |
||
101 | ->addOption( |
||
102 | 'no-cache', |
||
103 | null, |
||
104 | InputOption::VALUE_NONE, |
||
105 | 'Ignore cached data.' |
||
106 | ) |
||
107 | ->addOption( |
||
108 | 'cache', |
||
109 | null, |
||
110 | InputOption::VALUE_REQUIRED, |
||
111 | 'Path to the cache file.' |
||
112 | ) |
||
113 | ->addOption( |
||
114 | 'no-progress', |
||
115 | null, |
||
116 | InputOption::VALUE_NONE, |
||
117 | 'Hide the progress output.' |
||
118 | ) |
||
119 | ->addOption( |
||
120 | 'json', |
||
121 | null, |
||
122 | InputOption::VALUE_OPTIONAL, |
||
123 | 'Path to store JSON results.' |
||
124 | ) |
||
125 | ->addOption( |
||
126 | 'xml', |
||
127 | null, |
||
128 | InputOption::VALUE_OPTIONAL, |
||
129 | 'Path to store JUnit XML results.' |
||
130 | ) |
||
131 | ->addOption( |
||
132 | 'warning', |
||
133 | 'w', |
||
134 | InputOption::VALUE_NONE, |
||
135 | 'Also show warnings.' |
||
136 | ) |
||
137 | ->addOption( |
||
138 | 'quiet', |
||
139 | 'q', |
||
140 | InputOption::VALUE_NONE, |
||
141 | 'Allow to silenty fail.' |
||
142 | ); |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * Initializes the command just after the input has been validated. |
||
147 | * |
||
148 | * This is mainly useful when a lot of commands extends one main command |
||
149 | * where some things need to be initialized based on the input arguments and options. |
||
150 | * |
||
151 | * @param InputInterface $input An InputInterface instance |
||
152 | * @param OutputInterface $output An OutputInterface instance |
||
153 | */ |
||
154 | public function initialize(InputInterface $input, OutputInterface $output) |
||
155 | { |
||
156 | $this->input = $input; |
||
157 | $this->output = $output; |
||
158 | } |
||
159 | |||
160 | /** |
||
161 | * Executes the current command. |
||
162 | * |
||
163 | * This method is not abstract because you can use this class |
||
164 | * as a concrete class. In this case, instead of defining the |
||
165 | * execute() method, you set the code to execute by passing |
||
166 | * a Closure to the setCode() method. |
||
167 | * |
||
168 | * @param InputInterface $input An InputInterface instance |
||
169 | * @param OutputInterface $output An OutputInterface instance |
||
170 | * |
||
171 | * @throws \LogicException When this abstract method is not implemented |
||
172 | * |
||
173 | * @return null|int null or 0 if everything went fine, or an error code |
||
174 | * |
||
175 | * @see setCode() |
||
176 | * |
||
177 | * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException |
||
178 | */ |
||
179 | protected function execute(InputInterface $input, OutputInterface $output) |
||
180 | { |
||
181 | $startTime = microtime(true); |
||
182 | $startMemUsage = memory_get_usage(true); |
||
183 | |||
184 | $output->writeln($this->getApplication()->getLongVersion() . " by overtrue and contributors.\n"); |
||
185 | |||
186 | $options = $this->mergeOptions(); |
||
187 | $verbosity = $output->getVerbosity(); |
||
188 | |||
189 | if ($verbosity >= OutputInterface::VERBOSITY_DEBUG) { |
||
190 | $output->writeln('Options: ' . json_encode($options) . "\n"); |
||
191 | } |
||
192 | |||
193 | $linter = new Linter($options['path'], $options['exclude'], $options['extensions'], $options['warning']); |
||
194 | $linter->setProcessLimit($options['jobs']); |
||
195 | |||
196 | if (!empty($options['cache'])) { |
||
197 | Cache::setFilename($options['cache']); |
||
198 | } |
||
199 | |||
200 | $usingCache = 'No'; |
||
201 | if (!$input->getOption('no-cache') && Cache::isCached()) { |
||
202 | $usingCache = 'Yes'; |
||
203 | $linter->setCache(Cache::get()); |
||
204 | } |
||
205 | |||
206 | $fileCount = count($linter->getFiles()); |
||
207 | |||
208 | if ($fileCount <= 0) { |
||
209 | $output->writeln('<info>Could not find files to lint</info>'); |
||
210 | |||
211 | return 0; |
||
212 | } |
||
213 | |||
214 | $errors = $this->executeLint($linter, $input, $output, $fileCount); |
||
215 | |||
216 | $timeUsage = Helper::formatTime(microtime(true) - $startTime); |
||
217 | $memUsage = Helper::formatMemory(memory_get_usage(true) - $startMemUsage); |
||
218 | |||
219 | $code = 0; |
||
220 | $errCount = count($errors); |
||
221 | |||
222 | $output->writeln(sprintf( |
||
223 | "\n\nTime: <info>%s</info>\tMemory: <info>%s</info>\tCache: <info>%s</info>\n", |
||
224 | $timeUsage, |
||
225 | $memUsage, |
||
226 | $usingCache |
||
227 | )); |
||
228 | |||
229 | if ($errCount > 0) { |
||
230 | $output->writeln('<error>FAILURES!</error>'); |
||
231 | $output->writeln("<error>Files: {$fileCount}, Failures: {$errCount}</error>"); |
||
232 | $this->showErrors($errors); |
||
233 | |||
234 | if (empty($options['quiet'])) { |
||
235 | $code = 1; |
||
236 | } |
||
237 | } else { |
||
238 | $output->writeln("<info>OK! (Files: {$fileCount}, Success: {$fileCount})</info>"); |
||
239 | } |
||
240 | |||
241 | $context = [ |
||
242 | 'time_usage' => $timeUsage, |
||
243 | 'memory_usage' => $memUsage, |
||
244 | 'using_cache' => 'Yes' == $usingCache, |
||
245 | 'files_count' => $fileCount, |
||
246 | ]; |
||
247 | |||
248 | if (!empty($options['json'])) { |
||
249 | $this->dumpJsonResult((string) $options['json'], $errors, $options, $context); |
||
250 | } |
||
251 | |||
252 | if (!empty($options['xml'])) { |
||
253 | $this->dumpXmlResult((string) $options['xml'], $errors, $options, $context); |
||
254 | } |
||
255 | |||
256 | return $code; |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * @param string $path |
||
261 | * @param array $errors |
||
262 | * @param array $options |
||
263 | * @param array $context |
||
264 | */ |
||
265 | protected function dumpJsonResult($path, array $errors, array $options, array $context = []) |
||
266 | { |
||
267 | $result = [ |
||
268 | 'status' => 'success', |
||
269 | 'options' => $options, |
||
270 | 'errors' => $errors, |
||
271 | ]; |
||
272 | |||
273 | \file_put_contents($path, \json_encode(\array_merge($result, $context))); |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * @param string $path |
||
278 | * @param array $errors |
||
279 | * @param array $options |
||
280 | * @param array $context |
||
281 | * |
||
282 | * @throws Exception |
||
283 | */ |
||
284 | protected function dumpXmlResult($path, array $errors, array $options, array $context = []) |
||
0 ignored issues
–
show
|
|||
285 | { |
||
286 | $document = new Document(); |
||
287 | $suite = $document->addTestSuite(); |
||
288 | $suite->setName('PHP Linter'); |
||
289 | $suite->setTimestamp(new DateTime()); |
||
290 | $suite->setTime($context['time_usage']); |
||
291 | $testCase = $suite->addTestCase(); |
||
292 | foreach ($errors as $errorName => $value) { |
||
293 | $testCase->addError($errorName, 'Error', $value['error']); |
||
294 | } |
||
295 | $document->save($path); |
||
296 | } |
||
297 | |||
298 | /** |
||
299 | * Execute lint and return errors. |
||
300 | * |
||
301 | * @param Linter $linter |
||
302 | * @param InputInterface $input |
||
303 | * @param OutputInterface $output |
||
304 | * @param int $fileCount |
||
305 | * |
||
306 | * @return array |
||
307 | */ |
||
308 | protected function executeLint($linter, $input, $output, $fileCount) |
||
309 | { |
||
310 | $cache = !$input->getOption('no-cache'); |
||
311 | $maxColumns = floor((new Terminal())->getWidth() / 2); |
||
312 | $verbosity = $output->getVerbosity(); |
||
313 | $displayProgress = !$input->getOption('no-progress'); |
||
314 | |||
315 | $displayProgress && $linter->setProcessCallback(function ($status, SplFileInfo $file) use ($output, $verbosity, $fileCount, $maxColumns) { |
||
316 | static $i = 1; |
||
317 | |||
318 | $percent = floor(($i / $fileCount) * 100); |
||
319 | $process = str_pad(" {$i} / {$fileCount} ({$percent}%)", 18, ' ', STR_PAD_LEFT); |
||
320 | |||
321 | if ($verbosity >= OutputInterface::VERBOSITY_VERBOSE) { |
||
322 | $filename = str_pad(" {$i}: " . $file->getRelativePathname(), $maxColumns - 10, ' ', \STR_PAD_RIGHT); |
||
323 | View Code Duplication | if ($status === 'ok') { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
324 | $status = '<info>OK</info>'; |
||
325 | } elseif ($status === 'error') { |
||
326 | $status = '<error>Error</error>'; |
||
327 | } else { |
||
328 | $status = '<error>Warning</error>'; |
||
329 | } |
||
330 | |||
331 | $status = \str_pad($status, 20, ' ', \STR_PAD_RIGHT); |
||
332 | $output->writeln(\sprintf("%s\t%s\t%s", $filename, $status, $process)); |
||
333 | } else { |
||
334 | if ($i && 0 === $i % $maxColumns) { |
||
335 | $output->writeln($process); |
||
336 | } |
||
337 | View Code Duplication | if ($status === 'ok') { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
338 | $status = '<info>.</info>'; |
||
339 | } elseif ($status === 'error') { |
||
340 | $status = '<error>E</error>'; |
||
341 | } else { |
||
342 | $status = '<error>W</error>'; |
||
343 | } |
||
344 | |||
345 | $output->write($status); |
||
346 | } |
||
347 | ++$i; |
||
348 | }); |
||
349 | |||
350 | $displayProgress || $output->write('<info>Checking...</info>'); |
||
351 | |||
352 | return $linter->lint([], $cache); |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * Show errors detail. |
||
357 | * |
||
358 | * @param array $errors |
||
359 | * |
||
360 | * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException |
||
361 | */ |
||
362 | protected function showErrors($errors) |
||
363 | { |
||
364 | $i = 0; |
||
365 | $this->output->writeln("\nThere was " . count($errors) . ' errors:'); |
||
366 | |||
367 | foreach ($errors as $filename => $error) { |
||
368 | $this->output->writeln('<comment>' . ++$i . ". {$filename}:{$error['line']}" . '</comment>'); |
||
369 | |||
370 | $this->output->write($this->getHighlightedCodeSnippet($filename, $error['line'])); |
||
371 | |||
372 | $this->output->writeln("<error> {$error['error']}</error>"); |
||
373 | } |
||
374 | } |
||
375 | |||
376 | /** |
||
377 | * @param string $filePath |
||
378 | * @param int $lineNumber |
||
379 | * @param int $linesBefore |
||
380 | * @param int $linesAfter |
||
381 | * |
||
382 | * @return string |
||
383 | */ |
||
384 | protected function getCodeSnippet($filePath, $lineNumber, $linesBefore = 3, $linesAfter = 3) |
||
385 | { |
||
386 | $lines = file($filePath); |
||
387 | $offset = $lineNumber - $linesBefore - 1; |
||
388 | $offset = max($offset, 0); |
||
389 | $length = $linesAfter + $linesBefore + 1; |
||
390 | $lines = array_slice($lines, $offset, $length, $preserveKeys = true); |
||
391 | end($lines); |
||
392 | $lineStrlen = strlen(key($lines) + 1); |
||
393 | $snippet = ''; |
||
394 | |||
395 | foreach ($lines as $i => $line) { |
||
396 | $snippet .= (abs($lineNumber) === $i + 1 ? ' > ' : ' '); |
||
397 | $snippet .= str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| ' . rtrim($line) . PHP_EOL; |
||
398 | } |
||
399 | |||
400 | return $snippet; |
||
401 | } |
||
402 | |||
403 | /** |
||
404 | * @param string $filePath |
||
405 | * @param int $lineNumber |
||
406 | * @param int $linesBefore |
||
407 | * @param int $linesAfter |
||
408 | * |
||
409 | * @return string |
||
410 | * |
||
411 | * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException |
||
412 | */ |
||
413 | public function getHighlightedCodeSnippet($filePath, $lineNumber, $linesBefore = 3, $linesAfter = 3) |
||
414 | { |
||
415 | if ( |
||
416 | !class_exists('\JakubOnderka\PhpConsoleHighlighter\Highlighter') || |
||
417 | !class_exists('\JakubOnderka\PhpConsoleColor\ConsoleColor') |
||
418 | ) { |
||
419 | return $this->getCodeSnippet($filePath, $lineNumber, $linesBefore, $linesAfter); |
||
420 | } |
||
421 | |||
422 | $colors = new ConsoleColor(); |
||
423 | $highlighter = new Highlighter($colors); |
||
424 | $fileContent = file_get_contents($filePath); |
||
425 | |||
426 | return $highlighter->getCodeSnippet($fileContent, $lineNumber, $linesBefore, $linesAfter); |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * Merge options. |
||
431 | * |
||
432 | * @return array |
||
433 | */ |
||
434 | protected function mergeOptions() |
||
435 | { |
||
436 | $options = $this->input->getOptions(); |
||
437 | $options['path'] = $this->input->getArgument('path'); |
||
438 | $options['cache'] = $this->input->getOption('cache'); |
||
439 | if ($options['warning'] === false) { |
||
440 | unset($options['warning']); |
||
441 | } |
||
442 | |||
443 | $config = []; |
||
444 | |||
445 | if (!$this->input->getOption('no-configuration')) { |
||
446 | $filename = $this->getConfigFile(); |
||
447 | |||
448 | if (empty($options['configuration']) && $filename) { |
||
0 ignored issues
–
show
The expression
$filename of type string|null is loosely compared to true ; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For '' == false // true
'' == null // true
'ab' == false // false
'ab' == null // false
// It is often better to use strict comparison
'' === false // false
'' === null // false
![]() |
|||
449 | $options['configuration'] = $filename; |
||
450 | } |
||
451 | |||
452 | if (!empty($options['configuration'])) { |
||
453 | $this->output->writeln("<comment>Loaded config from \"{$options['configuration']}\"</comment>\n"); |
||
454 | $config = $this->loadConfiguration($options['configuration']); |
||
455 | } else { |
||
456 | $this->output->writeln("<comment>No config file loaded.</comment>\n"); |
||
457 | } |
||
458 | } else { |
||
459 | $this->output->writeln("<comment>No config file loaded.</comment>\n"); |
||
460 | } |
||
461 | |||
462 | $options = array_merge($this->defaults, array_filter($config), array_filter($options)); |
||
463 | |||
464 | is_array($options['extensions']) || $options['extensions'] = explode(',', $options['extensions']); |
||
465 | |||
466 | return $options; |
||
467 | } |
||
468 | |||
469 | /** |
||
470 | * Get configuration file. |
||
471 | * |
||
472 | * @return string|null |
||
473 | */ |
||
474 | protected function getConfigFile() |
||
475 | { |
||
476 | $inputPath = $this->input->getArgument('path'); |
||
477 | |||
478 | $dir = './'; |
||
479 | |||
480 | if (1 == count($inputPath) && $first = reset($inputPath)) { |
||
481 | $dir = is_dir($first) ? $first : dirname($first); |
||
482 | } |
||
483 | |||
484 | $filename = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.phplint.yml'; |
||
485 | |||
486 | return realpath($filename); |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * Load configuration from yaml. |
||
491 | * |
||
492 | * @param string $path |
||
493 | * |
||
494 | * @return array |
||
495 | */ |
||
496 | protected function loadConfiguration($path) |
||
497 | { |
||
498 | try { |
||
499 | $configuration = Yaml::parse(file_get_contents($path)); |
||
500 | if (!is_array($configuration)) { |
||
501 | throw new ParseException('Invalid content.', 1); |
||
502 | } |
||
503 | |||
504 | return $configuration; |
||
505 | } catch (ParseException $e) { |
||
506 | $this->output->writeln(sprintf('<error>Unable to parse the YAML string: %s</error>', $e->getMessage())); |
||
507 | |||
508 | return []; |
||
509 | } |
||
510 | } |
||
511 | } |
||
512 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.