Completed
Push — master ( b17148...19f444 )
by Carlos
02:33
created

LintCommand::execute()   C

Complexity

Conditions 8
Paths 40

Size

Total Lines 68
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 68
rs 6.5974
c 0
b 0
f 0
cc 8
eloc 42
nc 40
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 JakubOnderka\PhpConsoleColor\ConsoleColor;
15
use JakubOnderka\PhpConsoleHighlighter\Highlighter;
16
use Overtrue\PHPLint\Cache;
17
use Overtrue\PHPLint\Linter;
18
use Symfony\Component\Console\Command\Command;
19
use Symfony\Component\Console\Helper\Helper;
20
use Symfony\Component\Console\Input\InputArgument;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Terminal;
25
use Symfony\Component\Finder\SplFileInfo;
26
use Symfony\Component\Yaml\Exception\ParseException;
27
use Symfony\Component\Yaml\Yaml;
28
29
/**
30
 * Class LintCommand.
31
 */
32
class LintCommand extends Command
33
{
34
    /**
35
     * @var array
36
     */
37
    protected $defaults = [
38
        'jobs' => 5,
39
        'exclude' => [],
40
        'extensions' => ['php'],
41
    ];
42
43
    /**
44
     * @var \Symfony\Component\Console\Input\InputInterface
45
     */
46
    protected $input;
47
48
    /**
49
     * @var \Symfony\Component\Console\Output\OutputInterface
50
     */
51
    protected $output;
52
53
    /**
54
     * Configures the current command.
55
     */
56
    protected function configure()
57
    {
58
        $this
59
            ->setName('phplint')
60
            ->setDescription('Lint something')
61
            ->addArgument(
62
                'path',
63
                InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
64
                'Path to file or directory to lint.'
65
            )
66
            ->addOption(
67
                'exclude',
68
                null,
69
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
70
                'Path to file or directory to exclude from linting'
71
            )
72
            ->addOption(
73
                'extensions',
74
                null,
75
                InputOption::VALUE_REQUIRED,
76
                'Check only files with selected extensions (default: php)'
77
            )
78
            ->addOption(
79
                'jobs',
80
                'j',
81
                InputOption::VALUE_REQUIRED,
82
                'Number of parraled jobs to run (default: 5)'
83
            )
84
            ->addOption(
85
                'configuration',
86
                'c',
87
                InputOption::VALUE_REQUIRED,
88
                'Read configuration from config file (default: ./.phplint.yml).'
89
            )
90
            ->addOption(
91
                'no-configuration',
92
                null,
93
                InputOption::VALUE_NONE,
94
                'Ignore default configuration file (default: ./.phplint.yml).'
95
            )
96
            ->addOption(
97
                'no-cache',
98
                null,
99
                InputOption::VALUE_NONE,
100
                'Ignore cached data.'
101
            )
102
            ->addOption(
103
                'cache',
104
                null,
105
                InputOption::VALUE_REQUIRED,
106
                'Path to the cache file.'
107
            )
108
            ->addOption(
109
                'json',
110
                null,
111
                InputOption::VALUE_OPTIONAL,
112
                'Output JSON results to a file.'
113
            );
114
    }
115
116
    /**
117
     * Initializes the command just after the input has been validated.
118
     *
119
     * This is mainly useful when a lot of commands extends one main command
120
     * where some things need to be initialized based on the input arguments and options.
121
     *
122
     * @param InputInterface  $input  An InputInterface instance
123
     * @param OutputInterface $output An OutputInterface instance
124
     */
125
    public function initialize(InputInterface $input, OutputInterface $output)
126
    {
127
        $this->input = $input;
128
        $this->output = $output;
129
    }
130
131
    /**
132
     * Executes the current command.
133
     *
134
     * This method is not abstract because you can use this class
135
     * as a concrete class. In this case, instead of defining the
136
     * execute() method, you set the code to execute by passing
137
     * a Closure to the setCode() method.
138
     *
139
     * @param InputInterface  $input  An InputInterface instance
140
     * @param OutputInterface $output An OutputInterface instance
141
     *
142
     * @throws LogicException When this abstract method is not implemented
143
     *
144
     * @return null|int null or 0 if everything went fine, or an error code
145
     *
146
     * @see setCode()
147
     * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
148
     */
149
    protected function execute(InputInterface $input, OutputInterface $output)
150
    {
151
        $startTime = microtime(true);
152
        $startMemUsage = memory_get_usage(true);
153
154
        $output->writeln($this->getApplication()->getLongVersion()." by overtrue and contributors.\n");
155
156
        $options = $this->mergeOptions();
157
        $verbosity = $output->getVerbosity();
158
159
        if ($verbosity >= OutputInterface::VERBOSITY_DEBUG) {
160
            $output->writeln('Options: '.json_encode($options)."\n");
161
        }
162
163
        $linter = new Linter($options['path'], $options['exclude'], $options['extensions']);
164
        $linter->setProcessLimit($options['jobs']);
165
166
        if (!empty($options['cache'])) {
167
            Cache::setFilename($options['cache']);
168
        }
169
170
        $usingCache = 'No';
171
        if (!$input->getOption('no-cache') && Cache::isCached()) {
172
            $usingCache = 'Yes';
173
            $linter->setCache(Cache::get());
174
        }
175
176
        $fileCount = count($linter->getFiles());
177
178
        if ($fileCount <= 0) {
179
            $output->writeln('<info>Could not find files to lint</info>');
180
181
            return 0;
182
        }
183
184
        $errors = $this->executeLint($linter, $input, $output, $fileCount);
185
186
        $timeUsage = Helper::formatTime(microtime(true) - $startTime);
187
        $memUsage = Helper::formatMemory(memory_get_usage(true) - $startMemUsage);
188
189
        $code = 0;
190
        $errCount = count($errors);
191
192
        $output->writeln(sprintf(
193
            "\n\nTime: <info>%s</info>\tMemory: <info>%s</info>\tCache: <info>%s</info>\n",
194
            $timeUsage, $memUsage, $usingCache
195
        ));
196
197
        if ($errCount > 0) {
198
            $output->writeln('<error>FAILURES!</error>');
199
            $output->writeln("<error>Files: {$fileCount}, Failures: {$errCount}</error>");
200
            $this->showErrors($errors);
201
            $code = 1;
202
        } else {
203
            $output->writeln("<info>OK! (Files: {$fileCount}, Success: {$fileCount})</info>");
204
        }
205
206
        if (!empty($options['json'])) {
207
            $this->dumpResult($options['json'], $errors, $options, [
208
                'time_usage' => $timeUsage,
209
                'memory_usage' => $memUsage,
210
                'using_cache' => $usingCache == 'Yes',
211
                'files_count' => $fileCount,
212
            ]);
213
        }
214
215
        return $code;
216
    }
217
218
    /**
219
     * @param string $path
220
     * @param array  $errors
221
     * @param array  $options
222
     * @param array  $context
223
     */
224
    protected function dumpResult($path, array $errors, array $options, array $context = [])
225
    {
226
        $result = [
227
            'status' => 'success',
228
            'options' => $options,
229
            'errors' => $errors,
230
        ];
231
232
        \file_put_contents((string) $path, \json_encode(\array_merge($result, $context)));
233
    }
234
235
    /**
236
     * Execute lint and return errors.
237
     *
238
     * @param Linter          $linter
239
     * @param InputInterface  $input
240
     * @param OutputInterface $output
241
     * @param int             $fileCount
242
     *
243
     * @return array
244
     */
245
    protected function executeLint($linter, $input, $output, $fileCount)
246
    {
247
        $cache = !$input->getOption('no-cache');
248
        $maxColumns = floor((new Terminal())->getWidth() / 2);
249
        $verbosity = $output->getVerbosity();
250
251
        $linter->setProcessCallback(function ($status, SplFileInfo $file) use ($output, $verbosity, $fileCount, $maxColumns) {
252
            static $i = 1;
253
254
            $percent = floor(($i / $fileCount) * 100);
255
            $process = str_pad(" {$i} / {$fileCount} ({$percent}%)", 18, ' ', STR_PAD_LEFT);
256
257
            if ($verbosity >= OutputInterface::VERBOSITY_VERBOSE) {
258
                $filename = str_pad(" {$i}: ".$file->getRelativePathname(), $maxColumns - 10, ' ', \STR_PAD_RIGHT);
259
                $status = \str_pad(('ok' === $status ? '<info>OK</info>' : '<error>Error</error>'), 20, ' ', \STR_PAD_RIGHT);
260
                $output->writeln(\sprintf("%s\t%s\t%s", $filename, $status, $process));
261
            } else {
262
                if ($i && 0 === $i % $maxColumns) {
263
                    $output->writeln($process);
264
                }
265
                $output->write('ok' === $status ? '<info>.</info>' : '<error>E</error>');
266
            }
267
            ++$i;
268
        });
269
270
        return $linter->lint([], $cache);
271
    }
272
273
    /**
274
     * Show errors detail.
275
     *
276
     * @param array $errors
277
     *
278
     * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
279
     */
280
    protected function showErrors($errors)
281
    {
282
        $i = 0;
283
        $this->output->writeln("\nThere was ".count($errors).' errors:');
284
285
        foreach ($errors as $filename => $error) {
286
            $this->output->writeln('<comment>'.++$i.". {$filename}:{$error['line']}".'</comment>');
287
288
            $this->output->write($this->getHighlightedCodeSnippet($filename, $error['line']));
289
290
            $this->output->writeln("<error> {$error['error']}</error>");
291
        }
292
    }
293
294
    /**
295
     * @param string $filePath
296
     * @param int    $lineNumber
297
     * @param int    $linesBefore
298
     * @param int    $linesAfter
299
     *
300
     * @return string
301
     */
302
    protected function getCodeSnippet($filePath, $lineNumber, $linesBefore = 3, $linesAfter = 3)
303
    {
304
        $lines = file($filePath);
305
        $offset = $lineNumber - $linesBefore - 1;
306
        $offset = max($offset, 0);
307
        $length = $linesAfter + $linesBefore + 1;
308
        $lines = array_slice($lines, $offset, $length, $preserveKeys = true);
309
        end($lines);
310
        $lineStrlen = strlen(key($lines) + 1);
311
        $snippet = '';
312
313
        foreach ($lines as $i => $line) {
314
            $snippet .= (abs($lineNumber) === $i + 1 ? '  > ' : '    ');
315
            $snippet .= str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| ' . rtrim($line) . PHP_EOL;
316
        }
317
318
        return $snippet;
319
    }
320
321
    /**
322
     * @param string $filePath
323
     * @param int    $lineNumber
324
     * @param int    $linesBefore
325
     * @param int    $linesAfter
326
     *
327
     * @return string
328
     * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException
329
     */
330
    public function getHighlightedCodeSnippet($filePath, $lineNumber, $linesBefore = 3, $linesAfter = 3)
331
    {
332
        if (
333
            !class_exists('\JakubOnderka\PhpConsoleHighlighter\Highlighter') ||
334
            !class_exists('\JakubOnderka\PhpConsoleColor\ConsoleColor')
335
        ) {
336
            return $this->getCodeSnippet($filePath, $lineNumber, $linesBefore, $linesAfter);
337
        }
338
339
        $colors = new ConsoleColor();
340
        $highlighter = new Highlighter($colors);
341
        $fileContent = file_get_contents($filePath);
342
        return $highlighter->getCodeSnippet($fileContent, $lineNumber, $linesBefore, $linesAfter);
343
    }
344
345
    /**
346
     * Merge options.
347
     *
348
     * @return array
349
     */
350
    protected function mergeOptions()
351
    {
352
        $options = $this->input->getOptions();
353
        $options['path'] = $this->input->getArgument('path');
354
        $options['cache'] = $this->input->getOption('cache');
355
356
        $config = [];
357
358
        if (!$this->input->getOption('no-configuration')) {
359
            $filename = $this->getConfigFile();
360
361
            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...
362
                $options['configuration'] = $filename;
363
            }
364
365
            if (!empty($options['configuration'])) {
366
                $this->output->writeln("<comment>Loaded config from \"{$options['configuration']}\"</comment>\n");
367
                $config = $this->loadConfiguration($options['configuration']);
368
            } else {
369
                $this->output->writeln("<comment>No config file loaded.</comment>\n");
370
            }
371
        } else {
372
            $this->output->writeln("<comment>No config file loaded.</comment>\n");
373
        }
374
375
        $options = array_merge($this->defaults, array_filter($config), array_filter($options));
376
377
        is_array($options['extensions']) || $options['extensions'] = explode(',', $options['extensions']);
378
379
        return $options;
380
    }
381
382
    /**
383
     * Get configuration file.
384
     *
385
     * @return string|null
386
     */
387
    protected function getConfigFile()
388
    {
389
        $inputPath = $this->input->getArgument('path');
390
391
        $dir = './';
392
393
        if (1 == count($inputPath) && $first = reset($inputPath)) {
394
            $dir = is_dir($first) ? $first : dirname($first);
395
        }
396
397
        $filename = rtrim($dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'.phplint.yml';
398
399
        return realpath($filename);
400
    }
401
402
    /**
403
     * Load configuration from yaml.
404
     *
405
     * @param string $path
406
     *
407
     * @return array
408
     */
409
    protected function loadConfiguration($path)
410
    {
411
        try {
412
            $configuration = Yaml::parse(file_get_contents($path));
413
            if (!is_array($configuration)) {
414
                throw new ParseException('Invalid content.', 1);
415
            }
416
417
            return $configuration;
418
        } catch (ParseException $e) {
419
            $this->output->writeln(sprintf('<error>Unable to parse the YAML string: %s</error>', $e->getMessage()));
420
        }
421
    }
422
}
423