1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace ComposerRequireChecker\Cli; |
4
|
|
|
|
5
|
|
|
use ComposerRequireChecker\ASTLocator\LocateASTFromFiles; |
6
|
|
|
use ComposerRequireChecker\DefinedExtensionsResolver\DefinedExtensionsResolver; |
7
|
|
|
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromASTRoots; |
8
|
|
|
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromExtensions; |
9
|
|
|
use ComposerRequireChecker\DependencyGuesser\DependencyGuesser; |
10
|
|
|
use ComposerRequireChecker\FileLocator\LocateComposerPackageDirectDependenciesSourceFiles; |
11
|
|
|
use ComposerRequireChecker\FileLocator\LocateComposerPackageSourceFiles; |
12
|
|
|
use ComposerRequireChecker\FileLocator\LocateFilesByGlobPattern; |
13
|
|
|
use ComposerRequireChecker\GeneratorUtil\ComposeGenerators; |
14
|
|
|
use ComposerRequireChecker\JsonLoader; |
15
|
|
|
use ComposerRequireChecker\UsedSymbolsLocator\LocateUsedSymbolsFromASTRoots; |
16
|
|
|
use PhpParser\ErrorHandler\Collecting as CollectingErrorHandler; |
17
|
|
|
use PhpParser\ParserFactory; |
18
|
|
|
use Symfony\Component\Console\Command\Command; |
19
|
|
|
use Symfony\Component\Console\Helper\Table; |
20
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
21
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
22
|
|
|
use Symfony\Component\Console\Input\InputOption; |
23
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
24
|
|
|
use function dirname; |
25
|
|
|
|
26
|
|
|
class CheckCommand extends Command |
27
|
|
|
{ |
28
|
5 |
|
protected function configure() |
29
|
|
|
{ |
30
|
|
|
$this |
31
|
5 |
|
->setName('check') |
32
|
5 |
|
->setDescription('check the defined dependencies against your code') |
33
|
5 |
|
->addOption( |
34
|
5 |
|
'config-file', |
35
|
5 |
|
null, |
36
|
5 |
|
InputOption::VALUE_REQUIRED, |
37
|
5 |
|
'the config.json file to configure the checking options' |
38
|
|
|
) |
39
|
5 |
|
->addArgument( |
40
|
5 |
|
'composer-json', |
41
|
5 |
|
InputArgument::OPTIONAL, |
42
|
5 |
|
'the composer.json of your package, that should be checked', |
43
|
5 |
|
'./composer.json' |
44
|
|
|
) |
45
|
5 |
|
->addOption( |
46
|
5 |
|
'ignore-parse-errors', |
47
|
5 |
|
null, |
48
|
5 |
|
InputOption::VALUE_NONE, |
49
|
|
|
'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise' |
50
|
5 |
|
. ' errors will be thrown' |
51
|
|
|
); |
52
|
5 |
|
} |
53
|
|
|
|
54
|
4 |
|
protected function execute(InputInterface $input, OutputInterface $output): int |
55
|
|
|
{ |
56
|
4 |
|
if (!$output->isQuiet() && $this->getApplication() !== null) { |
57
|
4 |
|
$output->writeln($this->getApplication()->getLongVersion()); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** @var string $composerJsonArgument */ |
61
|
4 |
|
$composerJsonArgument = $input->getArgument('composer-json'); |
62
|
4 |
|
$composerJson = realpath((string) $composerJsonArgument); |
63
|
4 |
|
if (false === $composerJson) { |
64
|
1 |
|
throw new \InvalidArgumentException('file not found: [' . $composerJsonArgument . ']'); |
65
|
|
|
} |
66
|
3 |
|
$composerData = $this->getComposerData($composerJson); |
67
|
|
|
|
68
|
3 |
|
$options = $this->getCheckOptions($input); |
69
|
|
|
|
70
|
3 |
|
$getPackageSourceFiles = new LocateComposerPackageSourceFiles(); |
71
|
3 |
|
$getAdditionalSourceFiles = new LocateFilesByGlobPattern(); |
72
|
|
|
|
73
|
3 |
|
$sourcesASTs = $this->getASTFromFilesLocator($input); |
74
|
|
|
|
75
|
3 |
|
$this->verbose("Collecting defined vendor symbols... ", $output); |
76
|
3 |
|
$definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs( |
77
|
3 |
|
(new ComposeGenerators())->__invoke( |
78
|
3 |
|
$getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)), |
79
|
3 |
|
$getPackageSourceFiles($composerData, dirname($composerJson)), |
80
|
3 |
|
(new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson) |
81
|
|
|
) |
82
|
|
|
)); |
83
|
3 |
|
$this->verbose("found " . count($definedVendorSymbols) . " symbols.", $output, true); |
84
|
|
|
|
85
|
3 |
|
$this->verbose("Collecting defined extension symbols... ", $output); |
86
|
3 |
|
$definedExtensionSymbols = (new LocateDefinedSymbolsFromExtensions())->__invoke( |
87
|
3 |
|
(new DefinedExtensionsResolver())->__invoke($composerJson, $options->getPhpCoreExtensions()) |
88
|
|
|
); |
89
|
3 |
|
$this->verbose("found " . count($definedExtensionSymbols) . " symbols.", $output, true); |
90
|
|
|
|
91
|
3 |
|
$this->verbose("Collecting used symbols... ", $output); |
92
|
3 |
|
$usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke($sourcesASTs( |
93
|
3 |
|
(new ComposeGenerators())->__invoke( |
94
|
3 |
|
$getPackageSourceFiles($composerData, dirname($composerJson)), |
95
|
3 |
|
$getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)) |
96
|
|
|
) |
97
|
|
|
)); |
98
|
3 |
|
$this->verbose("found " . count($usedSymbols) . " symbols.", $output, true); |
99
|
|
|
|
100
|
3 |
|
if (!count($usedSymbols)) { |
101
|
|
|
throw new \LogicException('There were no symbols found, please check your configuration.'); |
102
|
|
|
} |
103
|
|
|
|
104
|
3 |
|
$this->verbose("Checking for unknown symbols... ", $output, true); |
105
|
3 |
|
$unknownSymbols = array_diff( |
106
|
3 |
|
$usedSymbols, |
107
|
3 |
|
$definedVendorSymbols, |
108
|
3 |
|
$definedExtensionSymbols, |
109
|
3 |
|
$options->getSymbolWhitelist() |
110
|
|
|
); |
111
|
|
|
|
112
|
3 |
|
if (!$unknownSymbols) { |
|
|
|
|
113
|
3 |
|
$output->writeln("There were no unknown symbols found."); |
114
|
3 |
|
return 0; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
$output->writeln("The following unknown symbols were found:"); |
118
|
|
|
$table = new Table($output); |
119
|
|
|
$table->setHeaders(['unknown symbol', 'guessed dependency']); |
120
|
|
|
$guesser = new DependencyGuesser($options); |
121
|
|
|
foreach ($unknownSymbols as $unknownSymbol) { |
122
|
|
|
$guessedDependencies = []; |
123
|
|
|
foreach ($guesser($unknownSymbol) as $guessedDependency) { |
124
|
|
|
$guessedDependencies[] = $guessedDependency; |
125
|
|
|
} |
126
|
|
|
$table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]); |
127
|
|
|
} |
128
|
|
|
$table->render(); |
129
|
|
|
|
130
|
|
|
return ((int)(bool)$unknownSymbols); |
131
|
|
|
} |
132
|
|
|
|
133
|
3 |
|
private function getCheckOptions(InputInterface $input): Options |
134
|
|
|
{ |
135
|
3 |
|
$fileName = $input->getOption('config-file'); |
136
|
3 |
|
if (!$fileName || !is_string($fileName)) { |
137
|
2 |
|
return new Options(); |
138
|
|
|
} |
139
|
1 |
|
return new Options((new JsonLoader($fileName))->getData()); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @param string $jsonFile |
144
|
|
|
* @throws \ComposerRequireChecker\Exception\InvalidJsonException |
145
|
|
|
* @throws \ComposerRequireChecker\Exception\NotReadableException |
146
|
|
|
*/ |
147
|
3 |
|
private function getComposerData(string $jsonFile): array |
148
|
|
|
{ |
149
|
|
|
// JsonLoader throws an exception if it cannot load the file |
150
|
3 |
|
return (new JsonLoader($jsonFile))->getData(); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @param InputInterface $input |
155
|
|
|
* @return LocateASTFromFiles |
156
|
|
|
*/ |
157
|
3 |
|
private function getASTFromFilesLocator(InputInterface $input): LocateASTFromFiles |
158
|
|
|
{ |
159
|
3 |
|
$errorHandler = $input->getOption('ignore-parse-errors') ? new CollectingErrorHandler() : null; |
160
|
3 |
|
$sourcesASTs = new LocateASTFromFiles((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $errorHandler); |
161
|
3 |
|
return $sourcesASTs; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @param string $string the message that should be printed |
167
|
|
|
* @param OutputInterface $output the output to log to |
168
|
|
|
* @param bool $newLine if a new line will be started afterwards |
169
|
|
|
*/ |
170
|
3 |
|
private function verbose(string $string, OutputInterface $output, bool $newLine = false): void |
171
|
|
|
{ |
172
|
3 |
|
if (!$output->isVerbose()) { |
173
|
2 |
|
return; |
174
|
|
|
} |
175
|
|
|
|
176
|
1 |
|
$output->write($string, $newLine); |
177
|
1 |
|
} |
178
|
|
|
} |
179
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.