Completed
Push — master ( 583a7a...af0418 )
by Carlos
14:02 queued 01:40
created

LintCommand::getScreenColumns()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 47
Code Lines 29

Duplication

Lines 10
Ratio 21.28 %

Importance

Changes 0
Metric Value
dl 10
loc 47
rs 5.1384
c 0
b 0
f 0
cc 12
eloc 29
nc 12
nop 0

How to fix   Complexity   

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