Completed
Pull Request — master (#75)
by Scott
01:45
created

Command::convertFileDescriptorLink()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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\Timer;
12
use Symfony\Component\Console\Command\Command as BaseCommand;
13
use Symfony\Component\Console\Helper\ProgressBar;
14
use Symfony\Component\Console\Input\InputArgument;
15
use Symfony\Component\Console\Input\InputInterface;
16
use Symfony\Component\Console\Input\InputOption;
17
use Symfony\Component\Console\Output\OutputInterface;
18
19
/**
20
 * Class Command
21
 *
22
 * @package Povils\PHPMND\Console
23
 */
24
class Command extends BaseCommand
25
{
26
    const EXIT_CODE_SUCCESS = 0;
27
    const EXIT_CODE_FAILURE = 1;
28
29
    /**
30
     * @inheritdoc
31
     */
32
    protected function configure()
33
    {
34
        $this
35
            ->setName('phpmnd')
36
            ->setDefinition(
37
                [
38
                    new InputArgument(
39
                        'directory',
40
                        InputArgument::REQUIRED,
41
                        'Directory to analyze'
42
                    )
43
                ]
44
            )
45
            ->addOption(
46
                'extensions',
47
                null,
48
                InputOption::VALUE_REQUIRED,
49
                'A comma-separated list of extensions'
50
            )
51
            ->addOption(
52
                'ignore-numbers',
53
                null,
54
                InputOption::VALUE_REQUIRED,
55
                'A comma-separated list of numbers to ignore',
56
                '0, 1'
57
            )
58
            ->addOption(
59
                'ignore-funcs',
60
                null,
61
                InputOption::VALUE_REQUIRED,
62
                'A comma-separated list of functions to ignore when using "argument" extension'
63
            )
64
            ->addOption(
65
                'exclude',
66
                null,
67
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
68
                'Exclude a directory from code analysis (must be relative to source)'
69
            )
70
            ->addOption(
71
                'exclude-path',
72
                null,
73
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
74
                'Exclude a path from code analysis (must be relative to source)'
75
            )
76
            ->addOption(
77
                'exclude-file',
78
                null,
79
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
80
                'Exclude a file from code analysis (must be relative to source)'
81
            )
82
            ->addOption(
83
                'suffixes',
84
                null,
85
                InputOption::VALUE_REQUIRED,
86
                'Comma-separated string of valid source code filename extensions',
87
                'php'
88
            )
89
            ->addOption(
90
                'progress',
91
                null,
92
                InputOption::VALUE_NONE,
93
                'Show progress bar'
94
            )
95
            ->addOption(
96
                'hint',
97
                null,
98
                InputOption::VALUE_NONE,
99
                'Suggest replacements for magic numbers'
100
            )
101
            ->addOption(
102
                'non-zero-exit-on-violation',
103
                null,
104
                InputOption::VALUE_NONE,
105
                'Return a non zero exit code when there are magic numbers'
106
            )
107
            ->addOption(
108
                'strings',
109
                null,
110
                InputOption::VALUE_NONE,
111
                'Include strings literal search in code analysis'
112
            )
113
            ->addOption(
114
                'ignore-strings',
115
                null,
116
                InputOption::VALUE_REQUIRED,
117
                'A comma-separated list of strings to ignore when using "strings" option'
118
            )
119
            ->addOption(
120
                'include-numeric-string',
121
                null,
122
                InputOption::VALUE_NONE,
123
                'Include strings which are numeric'
124
            )
125
            ->addOption(
126
                'allow-array-mapping',
127
                null,
128
                InputOption::VALUE_NONE,
129
                'Allow array mapping (key as strings) when using "array" extension.'
130
            )
131
            ->addOption(
132
                'xml-output',
133
                null,
134
                InputOption::VALUE_REQUIRED,
135
                'Generate an XML output to the specified path'
136
            )
137
            ->addOption(
138
                'whitelist',
139
                null,
140
                InputOption::VALUE_REQUIRED,
141
                'Link to a file containing filenames to search',
142
                ''
143
            )
144
        ;
145
    }
146
147
    /**
148
     * @inheritdoc
149
     */
150
    protected function execute(InputInterface $input, OutputInterface $output)
151
    {
152
        $finder = $this->createFinder($input);
153
154
        if (0 === $finder->count()) {
155
            $output->writeln('No files found to scan');
156
            return self::EXIT_CODE_SUCCESS;
157
        }
158
159
        $progressBar = null;
160
        if ($input->getOption('progress')) {
161
            $progressBar = new ProgressBar($output, $finder->count());
162
            $progressBar->start();
163
        }
164
165
        $hintList = new HintList;
166
        $detector = new Detector($this->createOption($input), $hintList);
167
168
        $fileReportList = new FileReportList();
169
        $printer = new Printer\Console();
170
        $whitelist = $this->getFileOption($input->getOption('whitelist'));
171
172
        foreach ($finder as $file) {
173
            if (count($whitelist) > 0 && !in_array($file->getRelativePathname(), $whitelist)) {
174
                continue;
175
            }
176
177
            try {
178
                $fileReport = $detector->detect($file);
179
                if ($fileReport->hasMagicNumbers()) {
180
                    $fileReportList->addFileReport($fileReport);
181
                }
182
            } catch (\Exception $e) {
183
                $output->writeln($e->getMessage());
184
            }
185
186
            if ($input->getOption('progress')) {
187
                $progressBar->advance();
188
            }
189
        }
190
191
        if ($input->getOption('progress')) {
192
            $progressBar->finish();
193
        }
194
195
        if ($input->getOption('xml-output')) {
196
            $xmlOutput = new Printer\Xml($input->getOption('xml-output'));
197
            $xmlOutput->printData($output, $fileReportList, $hintList);
198
        }
199
200
        if ($output->getVerbosity() !== OutputInterface::VERBOSITY_QUIET) {
201
            $output->writeln('');
202
            $printer->printData($output, $fileReportList, $hintList);
203
204
            $resourceUsage = class_exists(Timer::class) ? Timer::resourceUsage() : \PHP_Timer::resourceUsage();
205
206
            $output->writeln('<info>' . $resourceUsage . '</info>');
207
        }
208
209
        if ($input->getOption('non-zero-exit-on-violation') && $fileReportList->hasMagicNumbers()) {
210
            return self::EXIT_CODE_FAILURE;
211
        }
212
        return self::EXIT_CODE_SUCCESS;
213
    }
214
215
    /**
216
     * @param InputInterface $input
217
     * @return Option
218
     * @throws \Exception
219
     */
220
    private function createOption(InputInterface $input)
221
    {
222
        $option = new Option;
223
        $option->setIgnoreNumbers(array_map([$this, 'castToNumber'], $this->getCSVOption($input, 'ignore-numbers')));
224
        $option->setIgnoreFuncs($this->getCSVOption($input, 'ignore-funcs'));
225
        $option->setIncludeStrings($input->getOption('strings'));
226
        $option->setIncludeNumericStrings($input->getOption('include-numeric-string'));
227
        $option->setIgnoreStrings($this->getCSVOption($input, 'ignore-strings'));
228
        $option->setAllowArrayMapping($input->getOption('allow-array-mapping'));
229
        $option->setGiveHint($input->getOption('hint'));
230
        $option->setExtensions(
231
            (new ExtensionResolver())->resolve($this->getCSVOption($input, 'extensions'))
232
        );
233
234
        return $option;
235
    }
236
237
    /**
238
     * @param InputInterface $input
239
     * @param string $option
240
     *
241
     * @return array
242
     */
243
    private function getCSVOption(InputInterface $input, $option)
244
    {
245
        $result = $input->getOption($option);
246
        if (false === is_array($result)) {
247
            return array_filter(
248
                explode(',', $result),
249
                function ($value) {
250
                    return false === empty($value);
251
                }
252
            );
253
        }
254
255
        if (null === $result) {
256
            return [];
257
        }
258
259
        return $result;
260
    }
261
262
    /**
263
     * @param InputInterface $input
264
     *
265
     * @return PHPFinder
266
     */
267
    protected function createFinder(InputInterface $input)
268
    {
269
        return new PHPFinder(
270
            $input->getArgument('directory'),
271
            $input->getOption('exclude'),
272
            $input->getOption('exclude-path'),
273
            $input->getOption('exclude-file'),
274
            $this->getCSVOption($input, 'suffixes')
275
        );
276
    }
277
278
    /**
279
     * @param string $value
280
     *
281
     * @return int|float|string
282
     */
283
    private function castToNumber($value)
284
    {
285
        if (is_numeric($value)) {
286
            $value += 0; // '2' -> (int) 2, '2.' -> (float) 2.0
287
        }
288
289
        return $value;
290
    }
291
292
    private function getFileOption($filename)
293
    {
294
        $filename = $this->convertFileDescriptorLink($filename);
295
296
        if (file_exists($filename)) {
297
            return array_map('trim', file($filename));
298
        }
299
300
        return [];
301
    }
302
303
    private function convertFileDescriptorLink($path)
304
    {
305
        if (strpos($path, '/dev/fd') === 0) {
306
            return str_replace('/dev/fd', 'php://fd', $path);
307
        }
308
309
        return $path;
310
    }
311
}
312