Completed
Push — master ( d607f5...fbc455 )
by Carlos
02:33
created

LintCommand::loadConfiguration()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
342
            return 80;
343
        }
344
345 View Code Duplication
        if (function_exists('shell_exec') && preg_match('#\d+ (\d+)#', shell_exec('stty size'), $match) === 1) {
0 ignored issues
show
Duplication introduced by
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.

Loading history...
346
            if ((int) $match[1] > 0) {
347
                return (int) $match[1];
348
            }
349
        }
350
351 View Code Duplication
        if (function_exists('shell_exec') && preg_match('#columns = (\d+);#', shell_exec('stty'), $match) === 1) {
0 ignored issues
show
Duplication introduced by
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.

Loading history...
352
            if ((int) $match[1] > 0) {
353
                return (int) $match[1];
354
            }
355
        }
356
357
        return 80;
358
    }
359
}
360