Completed
Push — master ( 570a2a...6035c4 )
by Carlos
02:23
created

LintCommand::executeLint()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 17
rs 9.2
cc 4
eloc 10
nc 1
nop 3
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\Linter;
12
use Symfony\Component\Console\Command\Command;
13
use Symfony\Component\Console\Helper\Helper;
14
use Symfony\Component\Console\Helper\ProgressBar;
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
     * @var string
48
     */
49
    protected $cacheFile = __DIR__.'/../../.phplint-cache';
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,
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
    }
101
102
    /**
103
     * Initializes the command just after the input has been validated.
104
     *
105
     * This is mainly useful when a lot of commands extends one main command
106
     * where some things need to be initialized based on the input arguments and options.
107
     *
108
     * @param InputInterface  $input  An InputInterface instance
109
     * @param OutputInterface $output An OutputInterface instance
110
     */
111
    public function initialize(InputInterface $input, OutputInterface $output)
112
    {
113
        $this->input = $input;
114
        $this->output = $output;
115
    }
116
117
    /**
118
     * Executes the current command.
119
     *
120
     * This method is not abstract because you can use this class
121
     * as a concrete class. In this case, instead of defining the
122
     * execute() method, you set the code to execute by passing
123
     * a Closure to the setCode() method.
124
     *
125
     * @param InputInterface  $input  An InputInterface instance
126
     * @param OutputInterface $output An OutputInterface instance
127
     *
128
     * @throws LogicException When this abstract method is not implemented
129
     *
130
     * @return null|int null or 0 if everything went fine, or an error code
131
     *
132
     * @see setCode()
133
     */
134
    protected function execute(InputInterface $input, OutputInterface $output)
135
    {
136
        $startTime = microtime(true);
137
        $startMemUsage = memory_get_usage(true);
138
139
        $output->writeln($this->getApplication()->getLongVersion()." by overtrue and contributors.\n");
140
141
        if (!$input->getOption('no-cache') && file_exists($this->cacheFile)) {
142
            $linter->setCache(json_decode(file_get_contents($this->cacheFile), true));
0 ignored issues
show
Bug introduced by
The variable $linter seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
143
        }
144
145
        $options = $this->mergeOptions();
146
147
        $linter = new Linter($options['path'], $options['exclude'], $options['extensions']);
148
        $linter->setProcessLimit($options['jobs']);
149
150
        $fileCount = count($linter->getFiles());
151
152
        if ($fileCount <= 0) {
153
            $output->writeln('<info>Could not find files to lint</info>');
154
155
            return 0;
156
        }
157
158
        $errors = $this->executeLint($linter, $output, $fileCount);
159
160
        $timeUsage = Helper::formatTime(microtime(true) - $startTime);
161
        $memUsage = Helper::formatMemory(memory_get_usage(true) - $startMemUsage);
162
163
        $code = 0;
164
        $errCount = count($errors);
165
166
        $output->writeln("\n\nTime: {$timeUsage}, Memory: {$memUsage}MB\n");
167
168
        if ($errCount > 0) {
169
            $output->writeln('<error>FAILURES!</error>');
170
            $output->writeln("<error>Files: {$fileCount}, Failures: {$errCount}</error>");
171
            $this->showErrors($errors);
172
            $code = 1;
173
        } else {
174
            $output->writeln("<info>OK! (Files: {$fileCount}, Success: {$fileCount})</info>");
175
        }
176
177
        return $code;
178
    }
179
180
    protected function executeLint($linter, $output, $fileCount)
181
    {
182
        $maxColumns = floor($this->getScreenColumns() / 2);
183
184
        $linter->setProcessCallback(function ($status, $filename) use ($output, $fileCount, $maxColumns) {
0 ignored issues
show
Unused Code introduced by
The parameter $filename 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...
185
            static $i = 0;
186
187
            if ($i && $i % $maxColumns === 0) {
188
                $percent = floor(($i / $fileCount) * 100);
189
                $output->writeln(str_pad(" {$i} / {$fileCount} ({$percent}%)", 18, " ", STR_PAD_LEFT));
190
            }
191
            $i++;
192
            $output->write($status == 'ok' ? '<info>.</info>' : '<error>E</error>');
193
        });
194
195
        return $linter->lint();
196
    }
197
198
    /**
199
     * Show errors detail.
200
     *
201
     * @param array $errors
202
     */
203
    protected function showErrors($errors)
204
    {
205
        $i = 0;
206
        $this->output->writeln("\nThere was ".count($errors).' errors:');
207
208
        foreach ($errors as $filename => $error) {
209
            $this->output->writeln('<comment>'.++$i.". {$filename}:{$error['line']}".'</comment>');
210
            $error = preg_replace('~in\s+'.preg_quote($filename).'~', '', $error);
211
            $this->output->writeln("<error> {$error['error']}</error>");
212
        }
213
    }
214
215
    /**
216
     * Merge options.
217
     *
218
     * @return array
219
     */
220
    protected function mergeOptions()
221
    {
222
        $options = $this->input->getOptions();
223
        $options['path'] = $this->input->getArgument('path') ?: './';
224
225
        $config = [];
226
227
        if (!$this->input->getOption('no-configuration')) {
228
            $filename = rtrim($options['path'], DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'.phplint.yml';
229
230
            if (!$options['configuration'] && file_exists($filename)) {
231
                $options['configuration'] = realpath($filename);
232
            }
233
234
            if (!empty($options['configuration'])) {
235
                $this->output->writeln("<comment>Loaded config from \"{$options['configuration']}\"</comment>\n");
236
                $config = $this->loadConfiguration($options['configuration']);
237
            }
238
        }
239
240
        $options = array_merge($this->defaults, array_filter($config), array_filter($options));
241
242
        is_array($options['extensions']) || $options['extensions'] = explode(',', $options['extensions']);
243
244
        return $options;
245
    }
246
247
    /**
248
     * Load configuration from yaml.
249
     *
250
     * @param string $path
251
     *
252
     * @return array
253
     */
254
    protected function loadConfiguration($path)
255
    {
256
        try {
257
            $configuration = Yaml::parse(file_get_contents($path));
258
            if (!is_array($configuration)) {
259
                throw new ParseException('Invalid content.', 1);
260
            }
261
262
            return $configuration;
263
        } catch (ParseException $e) {
264
            $this->output->writeln(sprintf('<error>Unable to parse the YAML string: %s</error>', $e->getMessage()));
265
        }
266
    }
267
268
    /**
269
     * Get screen columns.
270
     *
271
     * @return int
272
     */
273
    protected function getScreenColumns()
274
    {
275
        if (DIRECTORY_SEPARATOR == '\\') {
276
            $columns = 80;
277
278
            if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) {
279
                $columns = $matches[1];
280
            } elseif (function_exists('proc_open')) {
281
                $process = proc_open(
282
                    'mode CON',
283
                    array(
284
                        1 => array('pipe', 'w'),
285
                        2 => array('pipe', 'w')
286
                    ),
287
                    $pipes,
288
                    null,
289
                    null,
290
                    array('suppress_errors' => true)
291
                );
292
                if (is_resource($process)) {
293
                    $info = stream_get_contents($pipes[1]);
294
                    fclose($pipes[1]);
295
                    fclose($pipes[2]);
296
                    proc_close($process);
297
                    if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
298
                        $columns = $matches[2];
299
                    }
300
                }
301
            }
302
303
            return $columns - 1;
304
        }
305
306
        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...
307
            return 80;
308
        }
309
310 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...
311
            if ((int) $match[1] > 0) {
312
                return (int) $match[1];
313
            }
314
        }
315
316 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...
317
            if ((int) $match[1] > 0) {
318
                return (int) $match[1];
319
            }
320
        }
321
322
        return 80;
323
    }
324
}
325