Completed
Push — master ( 4abbd0...1ae1bf )
by Carlos
04:15
created

LintCommand::execute()   C

Complexity

Conditions 9
Paths 72

Size

Total Lines 76

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 76
rs 6.968
c 0
b 0
f 0
cc 9
nc 72
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        'exclude' => [],
43
        'extensions' => ['php'],
44
    ];
45
46
    /**
47
     * @var \Symfony\Component\Console\Input\InputInterface
48
     */
49
    protected $input;
50
51
    /**
52
     * @var \Symfony\Component\Console\Output\OutputInterface
53
     */
54
    protected $output;
55
56
    /**
57
     * Configures the current command.
58
     */
59
    protected function configure()
60
    {
61
        $this
62
            ->setName('phplint')
63
            ->setDescription('Lint something')
64
            ->addArgument(
65
                'path',
66
                InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
67
                'Path to file or directory to lint.'
68
            )
69
            ->addOption(
70
                'exclude',
71
                null,
72
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
73
                'Path to file or directory to exclude from linting'
74
            )
75
            ->addOption(
76
                'extensions',
77
                null,
78
                InputOption::VALUE_REQUIRED,
79
                'Check only files with selected extensions (default: php)'
80
            )
81
            ->addOption(
82
                'jobs',
83
                'j',
84
                InputOption::VALUE_REQUIRED,
85
                'Number of parraled jobs to run (default: 5)'
86
            )
87
            ->addOption(
88
                'configuration',
89
                'c',
90
                InputOption::VALUE_REQUIRED,
91
                'Read configuration from config file (default: ./.phplint.yml).'
92
            )
93
            ->addOption(
94
                'no-configuration',
95
                null,
96
                InputOption::VALUE_NONE,
97
                'Ignore default configuration file (default: ./.phplint.yml).'
98
            )
99
            ->addOption(
100
                'no-cache',
101
                null,
102
                InputOption::VALUE_NONE,
103
                'Ignore cached data.'
104
            )
105
            ->addOption(
106
                'cache',
107
                null,
108
                InputOption::VALUE_REQUIRED,
109
                'Path to the cache file.'
110
            )
111
            ->addOption(
112
                'no-progress',
113
                null,
114
                InputOption::VALUE_NONE,
115
                'Hide the progress output.'
116
            )
117
            ->addOption(
118
                'json',
119
                null,
120
                InputOption::VALUE_OPTIONAL,
121
                'Path to store JSON results.'
122
            )
123
            ->addOption(
124
                'xml',
125
                null,
126
                InputOption::VALUE_OPTIONAL,
127
                'Path to store JUnit XML results.'
128
            );
129
    }
130
131
    /**
132
     * Initializes the command just after the input has been validated.
133
     *
134
     * This is mainly useful when a lot of commands extends one main command
135
     * where some things need to be initialized based on the input arguments and options.
136
     *
137
     * @param InputInterface  $input  An InputInterface instance
138
     * @param OutputInterface $output An OutputInterface instance
139
     */
140
    public function initialize(InputInterface $input, OutputInterface $output)
141
    {
142
        $this->input = $input;
143
        $this->output = $output;
144
    }
145
146
    /**
147
     * Executes the current command.
148
     *
149
     * This method is not abstract because you can use this class
150
     * as a concrete class. In this case, instead of defining the
151
     * execute() method, you set the code to execute by passing
152
     * a Closure to the setCode() method.
153
     *
154
     * @param InputInterface  $input  An InputInterface instance
155
     * @param OutputInterface $output An OutputInterface instance
156
     *
157
     * @throws \LogicException When this abstract method is not implemented
158
     *
159
     * @return null|int null or 0 if everything went fine, or an error code
160
     *
161
     * @see setCode()
162
     *
163
     * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
164
     */
165
    protected function execute(InputInterface $input, OutputInterface $output)
166
    {
167
        $startTime = microtime(true);
168
        $startMemUsage = memory_get_usage(true);
169
170
        $output->writeln($this->getApplication()->getLongVersion() . " by overtrue and contributors.\n");
171
172
        $options = $this->mergeOptions();
173
        $verbosity = $output->getVerbosity();
174
175
        if ($verbosity >= OutputInterface::VERBOSITY_DEBUG) {
176
            $output->writeln('Options: ' . json_encode($options) . "\n");
177
        }
178
179
        $linter = new Linter($options['path'], $options['exclude'], $options['extensions']);
180
        $linter->setProcessLimit($options['jobs']);
181
182
        if (!empty($options['cache'])) {
183
            Cache::setFilename($options['cache']);
184
        }
185
186
        $usingCache = 'No';
187
        if (!$input->getOption('no-cache') && Cache::isCached()) {
188
            $usingCache = 'Yes';
189
            $linter->setCache(Cache::get());
190
        }
191
192
        $fileCount = count($linter->getFiles());
193
194
        if ($fileCount <= 0) {
195
            $output->writeln('<info>Could not find files to lint</info>');
196
197
            return 0;
198
        }
199
200
        $errors = $this->executeLint($linter, $input, $output, $fileCount);
201
202
        $timeUsage = Helper::formatTime(microtime(true) - $startTime);
203
        $memUsage = Helper::formatMemory(memory_get_usage(true) - $startMemUsage);
204
205
        $code = 0;
206
        $errCount = count($errors);
207
208
        $output->writeln(sprintf(
209
            "\n\nTime: <info>%s</info>\tMemory: <info>%s</info>\tCache: <info>%s</info>\n",
210
            $timeUsage,
211
            $memUsage,
212
            $usingCache
213
        ));
214
215
        if ($errCount > 0) {
216
            $output->writeln('<error>FAILURES!</error>');
217
            $output->writeln("<error>Files: {$fileCount}, Failures: {$errCount}</error>");
218
            $this->showErrors($errors);
219
            $code = 1;
220
        } else {
221
            $output->writeln("<info>OK! (Files: {$fileCount}, Success: {$fileCount})</info>");
222
        }
223
224
        $context = [
225
            'time_usage' => $timeUsage,
226
            'memory_usage' => $memUsage,
227
            'using_cache' => 'Yes' == $usingCache,
228
            'files_count' => $fileCount,
229
        ];
230
231
        if (!empty($options['json'])) {
232
            $this->dumpJsonResult((string) $options['json'], $errors, $options, $context);
233
        }
234
235
        if (!empty($options['xml'])) {
236
            $this->dumpXmlResult((string) $options['xml'], $errors, $options, $context);
237
        }
238
239
        return $code;
240
    }
241
242
    /**
243
     * @param string $path
244
     * @param array  $errors
245
     * @param array  $options
246
     * @param array  $context
247
     */
248
    protected function dumpJsonResult($path, array $errors, array $options, array $context = [])
249
    {
250
        $result = [
251
            'status' => 'success',
252
            'options' => $options,
253
            'errors' => $errors,
254
        ];
255
256
        \file_put_contents($path, \json_encode(\array_merge($result, $context)));
257
    }
258
259
    /**
260
     * @param string $path
261
     * @param array  $errors
262
     * @param array  $options
263
     * @param array  $context
264
     *
265
     * @throws Exception
266
     */
267
    protected function dumpXmlResult($path, array $errors, array $options, array $context = [])
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
268
    {
269
        $document = new Document();
270
        $suite = $document->addTestSuite();
271
        $suite->setName('PHP Linter');
272
        $suite->setTimestamp(new DateTime());
273
        $suite->setTime($context['time_usage']);
274
        $testCase = $suite->addTestCase();
275
        foreach ($errors as $errorName => $value) {
276
            $testCase->addError($errorName, 'Error', $value['error']);
277
        }
278
        $document->save($path);
279
    }
280
281
    /**
282
     * Execute lint and return errors.
283
     *
284
     * @param Linter          $linter
285
     * @param InputInterface  $input
286
     * @param OutputInterface $output
287
     * @param int             $fileCount
288
     *
289
     * @return array
290
     */
291
    protected function executeLint($linter, $input, $output, $fileCount)
292
    {
293
        $cache = !$input->getOption('no-cache');
294
        $maxColumns = floor((new Terminal())->getWidth() / 2);
295
        $verbosity = $output->getVerbosity();
296
        $displayProgress = !$input->getOption('no-progress');
297
298
        $displayProgress && $linter->setProcessCallback(function ($status, SplFileInfo $file) use ($output, $verbosity, $fileCount, $maxColumns) {
299
            static $i = 1;
300
301
            $percent = floor(($i / $fileCount) * 100);
302
            $process = str_pad(" {$i} / {$fileCount} ({$percent}%)", 18, ' ', STR_PAD_LEFT);
303
304
            if ($verbosity >= OutputInterface::VERBOSITY_VERBOSE) {
305
                $filename = str_pad(" {$i}: " . $file->getRelativePathname(), $maxColumns - 10, ' ', \STR_PAD_RIGHT);
306
                $status = \str_pad(('ok' === $status ? '<info>OK</info>' : '<error>Error</error>'), 20, ' ', \STR_PAD_RIGHT);
307
                $output->writeln(\sprintf("%s\t%s\t%s", $filename, $status, $process));
308
            } else {
309
                if ($i && 0 === $i % $maxColumns) {
310
                    $output->writeln($process);
311
                }
312
                $output->write('ok' === $status ? '<info>.</info>' : '<error>E</error>');
313
            }
314
            ++$i;
315
        });
316
317
        $displayProgress || $output->write('<info>Checking...</info>');
318
319
        return $linter->lint([], $cache);
320
    }
321
322
    /**
323
     * Show errors detail.
324
     *
325
     * @param array $errors
326
     *
327
     * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
328
     */
329
    protected function showErrors($errors)
330
    {
331
        $i = 0;
332
        $this->output->writeln("\nThere was " . count($errors) . ' errors:');
333
334
        foreach ($errors as $filename => $error) {
335
            $this->output->writeln('<comment>' . ++$i . ". {$filename}:{$error['line']}" . '</comment>');
336
337
            $this->output->write($this->getHighlightedCodeSnippet($filename, $error['line']));
338
339
            $this->output->writeln("<error> {$error['error']}</error>");
340
        }
341
    }
342
343
    /**
344
     * @param string $filePath
345
     * @param int    $lineNumber
346
     * @param int    $linesBefore
347
     * @param int    $linesAfter
348
     *
349
     * @return string
350
     */
351
    protected function getCodeSnippet($filePath, $lineNumber, $linesBefore = 3, $linesAfter = 3)
352
    {
353
        $lines = file($filePath);
354
        $offset = $lineNumber - $linesBefore - 1;
355
        $offset = max($offset, 0);
356
        $length = $linesAfter + $linesBefore + 1;
357
        $lines = array_slice($lines, $offset, $length, $preserveKeys = true);
358
        end($lines);
359
        $lineStrlen = strlen(key($lines) + 1);
360
        $snippet = '';
361
362
        foreach ($lines as $i => $line) {
363
            $snippet .= (abs($lineNumber) === $i + 1 ? '  > ' : '    ');
364
            $snippet .= str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| ' . rtrim($line) . PHP_EOL;
365
        }
366
367
        return $snippet;
368
    }
369
370
    /**
371
     * @param string $filePath
372
     * @param int    $lineNumber
373
     * @param int    $linesBefore
374
     * @param int    $linesAfter
375
     *
376
     * @return string
377
     *
378
     * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
379
     */
380
    public function getHighlightedCodeSnippet($filePath, $lineNumber, $linesBefore = 3, $linesAfter = 3)
381
    {
382
        if (
383
            !class_exists('\JakubOnderka\PhpConsoleHighlighter\Highlighter') ||
384
            !class_exists('\JakubOnderka\PhpConsoleColor\ConsoleColor')
385
        ) {
386
            return $this->getCodeSnippet($filePath, $lineNumber, $linesBefore, $linesAfter);
387
        }
388
389
        $colors = new ConsoleColor();
390
        $highlighter = new Highlighter($colors);
391
        $fileContent = file_get_contents($filePath);
392
393
        return $highlighter->getCodeSnippet($fileContent, $lineNumber, $linesBefore, $linesAfter);
394
    }
395
396
    /**
397
     * Merge options.
398
     *
399
     * @return array
400
     */
401
    protected function mergeOptions()
402
    {
403
        $options = $this->input->getOptions();
404
        $options['path'] = $this->input->getArgument('path');
405
        $options['cache'] = $this->input->getOption('cache');
406
407
        $config = [];
408
409
        if (!$this->input->getOption('no-configuration')) {
410
            $filename = $this->getConfigFile();
411
412
            if (empty($options['configuration']) && $filename) {
0 ignored issues
show
Bug Best Practice introduced by
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 ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
413
                $options['configuration'] = $filename;
414
            }
415
416
            if (!empty($options['configuration'])) {
417
                $this->output->writeln("<comment>Loaded config from \"{$options['configuration']}\"</comment>\n");
418
                $config = $this->loadConfiguration($options['configuration']);
419
            } else {
420
                $this->output->writeln("<comment>No config file loaded.</comment>\n");
421
            }
422
        } else {
423
            $this->output->writeln("<comment>No config file loaded.</comment>\n");
424
        }
425
426
        $options = array_merge($this->defaults, array_filter($config), array_filter($options));
427
428
        is_array($options['extensions']) || $options['extensions'] = explode(',', $options['extensions']);
429
430
        return $options;
431
    }
432
433
    /**
434
     * Get configuration file.
435
     *
436
     * @return string|null
437
     */
438
    protected function getConfigFile()
439
    {
440
        $inputPath = $this->input->getArgument('path');
441
442
        $dir = './';
443
444
        if (1 == count($inputPath) && $first = reset($inputPath)) {
445
            $dir = is_dir($first) ? $first : dirname($first);
446
        }
447
448
        $filename = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.phplint.yml';
449
450
        return realpath($filename);
451
    }
452
453
    /**
454
     * Load configuration from yaml.
455
     *
456
     * @param string $path
457
     *
458
     * @return array
459
     */
460
    protected function loadConfiguration($path)
461
    {
462
        try {
463
            $configuration = Yaml::parse(file_get_contents($path));
464
            if (!is_array($configuration)) {
465
                throw new ParseException('Invalid content.', 1);
466
            }
467
468
            return $configuration;
469
        } catch (ParseException $e) {
470
            $this->output->writeln(sprintf('<error>Unable to parse the YAML string: %s</error>', $e->getMessage()));
471
472
            return [];
473
        }
474
    }
475
}
476