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'), |
|
|
|
|
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(); |
|
|
|
|
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
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.