Completed
Push — master ( 00b617...54bcf9 )
by Povilas
18s queued 10s
created

Command::getResourceUsage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
3
namespace Povils\PHPMND\Console;
4
5
use Povils\PHPMND\Detector;
6
use Povils\PHPMND\ExtensionResolver;
7
use Povils\PHPMND\FileReportList;
8
use Povils\PHPMND\HintList;
9
use Povils\PHPMND\PHPFinder;
10
use Povils\PHPMND\Printer;
11
use SebastianBergmann\Timer\ResourceUsageFormatter;
12
use SebastianBergmann\Timer\Timer;
13
use Symfony\Component\Console\Command\Command as BaseCommand;
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
20
/**
21
 * Class Command
22
 *
23
 * @package Povils\PHPMND\Console
24
 */
25
class Command extends BaseCommand
26
{
27
    const EXIT_CODE_SUCCESS = 0;
28
    const EXIT_CODE_FAILURE = 1;
29
30
    /**
31
     * @var Timer
32
     */
33
    private $timer;
34
35
    protected function configure(): void
36
    {
37
        $this
38
            ->setName('phpmnd')
39
            ->setDefinition(
40
                [
41
                    new InputArgument(
42
                        'directories',
43
                        InputArgument::REQUIRED + InputArgument::IS_ARRAY,
44
                        'One or more files and/or directories to analyze'
45
                    ),
46
                ]
47
            )
48
            ->addOption(
49
                'extensions',
50
                null,
51
                InputOption::VALUE_REQUIRED,
52
                'A comma-separated list of extensions'
53
            )
54
            ->addOption(
55
                'ignore-numbers',
56
                null,
57
                InputOption::VALUE_REQUIRED,
58
                'A comma-separated list of numbers to ignore',
59
                '0, 1'
60
            )
61
            ->addOption(
62
                'ignore-funcs',
63
                null,
64
                InputOption::VALUE_REQUIRED,
65
                'A comma-separated list of functions to ignore when using "argument" extension'
66
            )
67
            ->addOption(
68
                'exclude',
69
                null,
70
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
71
                'Exclude a directory from code analysis (must be relative to source)'
72
            )
73
            ->addOption(
74
                'exclude-path',
75
                null,
76
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
77
                'Exclude a path from code analysis (must be relative to source)'
78
            )
79
            ->addOption(
80
                'exclude-file',
81
                null,
82
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
83
                'Exclude a file from code analysis (must be relative to source)'
84
            )
85
            ->addOption(
86
                'suffixes',
87
                null,
88
                InputOption::VALUE_REQUIRED,
89
                'Comma-separated string of valid source code filename extensions',
90
                'php'
91
            )
92
            ->addOption(
93
                'progress',
94
                null,
95
                InputOption::VALUE_NONE,
96
                'Show progress bar'
97
            )
98
            ->addOption(
99
                'hint',
100
                null,
101
                InputOption::VALUE_NONE,
102
                'Suggest replacements for magic numbers'
103
            )
104
            ->addOption(
105
                'non-zero-exit-on-violation',
106
                null,
107
                InputOption::VALUE_NONE,
108
                'Return a non zero exit code when there are magic numbers'
109
            )
110
            ->addOption(
111
                'strings',
112
                null,
113
                InputOption::VALUE_NONE,
114
                'Include strings literal search in code analysis'
115
            )
116
            ->addOption(
117
                'ignore-strings',
118
                null,
119
                InputOption::VALUE_REQUIRED,
120
                'A comma-separated list of strings to ignore when using "strings" option'
121
            )
122
            ->addOption(
123
                'include-numeric-string',
124
                null,
125
                InputOption::VALUE_NONE,
126
                'Include strings which are numeric'
127
            )
128
            ->addOption(
129
                'allow-array-mapping',
130
                null,
131
                InputOption::VALUE_NONE,
132
                'Allow array mapping (key as strings) when using "array" extension.'
133
            )
134
            ->addOption(
135
                'xml-output',
136
                null,
137
                InputOption::VALUE_REQUIRED,
138
                'Generate an XML output to the specified path'
139
            )
140
            ->addOption(
141
                'whitelist',
142
                null,
143
                InputOption::VALUE_REQUIRED,
144
                'Link to a file containing filenames to search',
145
                ''
146
            )
147
        ;
148
    }
149
150
    protected function execute(InputInterface $input, OutputInterface $output): int
151
    {
152
        $this->startTimer();
153
        $finder = $this->createFinder($input);
154
155
        if (0 === $finder->count()) {
156
            $output->writeln('No files found to scan');
157
            return self::EXIT_CODE_SUCCESS;
158
        }
159
160
        $progressBar = null;
161
        if ($input->getOption('progress')) {
162
            $progressBar = new ProgressBar($output, $finder->count());
163
            $progressBar->start();
164
        }
165
166
        $hintList = new HintList;
167
        $detector = new Detector($this->createOption($input), $hintList);
168
169
        $fileReportList = new FileReportList();
170
        $printer = new Printer\Console();
171
        $whitelist = $this->getFileOption($input->getOption('whitelist'));
172
173
        foreach ($finder as $file) {
174
            if (count($whitelist) > 0 && !in_array($file->getRelativePathname(), $whitelist)) {
175
                continue;
176
            }
177
178
            try {
179
                $fileReport = $detector->detect($file);
180
                if ($fileReport->hasMagicNumbers()) {
181
                    $fileReportList->addFileReport($fileReport);
182
                }
183
            } catch (\Exception $e) {
184
                $output->writeln($e->getMessage());
185
            }
186
187
            if ($input->getOption('progress')) {
188
                $progressBar->advance();
189
            }
190
        }
191
192
        if ($input->getOption('progress')) {
193
            $progressBar->finish();
194
        }
195
196
        if ($input->getOption('xml-output')) {
197
            $xmlOutput = new Printer\Xml($input->getOption('xml-output'));
198
            $xmlOutput->printData($output, $fileReportList, $hintList);
199
        }
200
201
        if ($output->getVerbosity() !== OutputInterface::VERBOSITY_QUIET) {
202
            $output->writeln('');
203
            $printer->printData($output, $fileReportList, $hintList);
204
            $output->writeln('<info>' . $this->getResourceUsage() . '</info>');
205
        }
206
207
        if ($input->getOption('non-zero-exit-on-violation') && $fileReportList->hasMagicNumbers()) {
208
            return self::EXIT_CODE_FAILURE;
209
        }
210
        return self::EXIT_CODE_SUCCESS;
211
    }
212
213
    private function createOption(InputInterface $input): Option
214
    {
215
        $option = new Option;
216
        $option->setIgnoreNumbers(array_map([$this, 'castToNumber'], $this->getCSVOption($input, 'ignore-numbers')));
217
        $option->setIgnoreFuncs($this->getCSVOption($input, 'ignore-funcs'));
218
        $option->setIncludeStrings($input->getOption('strings'));
219
        $option->setIncludeNumericStrings($input->getOption('include-numeric-string'));
220
        $option->setIgnoreStrings($this->getCSVOption($input, 'ignore-strings'));
221
        $option->setAllowArrayMapping($input->getOption('allow-array-mapping'));
222
        $option->setGiveHint($input->getOption('hint'));
223
        $option->setExtensions(
224
            (new ExtensionResolver())->resolve($this->getCSVOption($input, 'extensions'))
225
        );
226
227
        return $option;
228
    }
229
230
    private function getCSVOption(InputInterface $input, string $option): array
231
    {
232
        $result = $input->getOption($option);
233
        if (false === is_array($result)) {
234
            return array_filter(
235
                explode(',', $result),
236
                function ($value) {
237
                    return false === empty($value);
238
                }
239
            );
240
        }
241
242
        if (null === $result) {
243
            return [];
244
        }
245
246
        return $result;
247
    }
248
249
    protected function createFinder(InputInterface $input): PHPFinder
250
    {
251
        return new PHPFinder(
252
            $input->getArgument('directories'),
0 ignored issues
show
Bug introduced by
It seems like $input->getArgument('directories') targeting Symfony\Component\Consol...nterface::getArgument() can also be of type null or string; however, Povils\PHPMND\PHPFinder::__construct() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
253
            $input->getOption('exclude'),
254
            $input->getOption('exclude-path'),
255
            $input->getOption('exclude-file'),
256
            $this->getCSVOption($input, 'suffixes')
257
        );
258
    }
259
260
    private function castToNumber(string $value)
261
    {
262
        if (is_numeric($value)) {
263
            $value += 0; // '2' -> (int) 2, '2.' -> (float) 2.0
264
        }
265
266
        return $value;
267
    }
268
269
    private function getFileOption($filename)
270
    {
271
        $filename = $this->convertFileDescriptorLink($filename);
272
273
        if (file_exists($filename)) {
274
            return array_map('trim', file($filename));
275
        }
276
277
        return [];
278
    }
279
280
    private function convertFileDescriptorLink($path)
281
    {
282
        if (strpos($path, '/dev/fd') === 0) {
283
            return str_replace('/dev/fd', 'php://fd', $path);
284
        }
285
286
        return $path;
287
    }
288
289
    private function startTimer()
290
    {
291
        if (class_exists(ResourceUsageFormatter::class)) {
292
            $this->timer = new Timer();
293
            $this->timer->start();
294
        }
295
    }
296
297
    private function getResourceUsage()
298
    {
299
        // php-timer ^4.0||^5.0
300
        if (class_exists(ResourceUsageFormatter::class)) {
301
            return (new ResourceUsageFormatter)->resourceUsage($this->timer->stop());
302
        }
303
304
        // php-timer ^2.0||^3.0
305
        return Timer::resourceUsage();
0 ignored issues
show
Bug introduced by
The method resourceUsage() does not seem to exist on object<SebastianBergmann\Timer\Timer>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
306
    }
307
}
308