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
|
|
|
|
25
|
|
|
class CheckCommand extends Command |
26
|
|
|
{ |
27
|
3 |
|
protected function configure() |
28
|
|
|
{ |
29
|
|
|
$this |
30
|
3 |
|
->setName('check') |
31
|
3 |
|
->setDescription('check the defined dependencies against your code') |
32
|
3 |
|
->addOption( |
33
|
3 |
|
'config-file', |
34
|
3 |
|
null, |
35
|
3 |
|
InputOption::VALUE_REQUIRED, |
36
|
3 |
|
'the config.json file to configure the checking options' |
37
|
|
|
) |
38
|
3 |
|
->addArgument( |
39
|
3 |
|
'composer-json', |
40
|
3 |
|
InputArgument::OPTIONAL, |
41
|
3 |
|
'the composer.json of your package, that should be checked', |
42
|
3 |
|
'./composer.json' |
43
|
|
|
) |
44
|
3 |
|
->addOption( |
45
|
3 |
|
'ignore-parse-errors', |
46
|
3 |
|
null, |
47
|
3 |
|
InputOption::VALUE_NONE, |
48
|
|
|
'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise' |
49
|
3 |
|
. ' errors will be thrown' |
50
|
|
|
); |
51
|
3 |
|
} |
52
|
|
|
|
53
|
2 |
|
protected function execute(InputInterface $input, OutputInterface $output): int |
54
|
|
|
{ |
55
|
2 |
|
if (!$output->isQuiet()) { |
56
|
2 |
|
$output->writeln($this->getApplication()->getLongVersion()); |
57
|
|
|
} |
58
|
|
|
|
59
|
2 |
|
$composerJson = realpath($input->getArgument('composer-json')); |
60
|
2 |
|
if (false === $composerJson) { |
61
|
1 |
|
throw new \InvalidArgumentException('file not found: [' . $input->getArgument('composer-json') . ']'); |
62
|
|
|
} |
63
|
1 |
|
$this->checkJsonFile($composerJson); |
64
|
|
|
|
65
|
1 |
|
$options = $this->getCheckOptions($input); |
66
|
|
|
|
67
|
1 |
|
$getPackageSourceFiles = new LocateComposerPackageSourceFiles(); |
68
|
1 |
|
$getAdditionalSourceFiles = new LocateFilesByGlobPattern(); |
69
|
|
|
|
70
|
1 |
|
$sourcesASTs = $this->getASTFromFilesLocator($input); |
71
|
|
|
|
72
|
1 |
|
$definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs( |
73
|
1 |
|
(new ComposeGenerators())->__invoke( |
74
|
1 |
|
$getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)), |
75
|
1 |
|
$getPackageSourceFiles($composerJson), |
76
|
1 |
|
(new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson) |
77
|
|
|
) |
78
|
|
|
)); |
79
|
|
|
|
80
|
1 |
|
$definedExtensionSymbols = (new LocateDefinedSymbolsFromExtensions())->__invoke( |
81
|
1 |
|
(new DefinedExtensionsResolver())->__invoke($composerJson, $options->getPhpCoreExtensions()) |
82
|
|
|
); |
83
|
|
|
|
84
|
1 |
|
$usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke( |
85
|
1 |
|
(new ComposeGenerators())->__invoke( |
86
|
1 |
|
$sourcesASTs($getPackageSourceFiles($composerJson)), |
87
|
1 |
|
$getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)) |
88
|
|
|
) |
89
|
|
|
); |
90
|
|
|
|
91
|
1 |
|
if (!count($usedSymbols)) { |
92
|
|
|
throw new \LogicException('There were no symbols found, please check your configuration.'); |
93
|
|
|
} |
94
|
|
|
|
95
|
1 |
|
$unknownSymbols = array_diff( |
96
|
1 |
|
$usedSymbols, |
97
|
1 |
|
$definedVendorSymbols, |
98
|
1 |
|
$definedExtensionSymbols, |
99
|
1 |
|
$options->getSymbolWhitelist() |
100
|
|
|
); |
101
|
|
|
|
102
|
1 |
|
if (!$unknownSymbols) { |
|
|
|
|
103
|
1 |
|
$output->writeln("There were no unknown symbols found."); |
104
|
1 |
|
return 0; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
$output->writeln("The following unknown symbols were found:"); |
108
|
|
|
$table = new Table($output); |
109
|
|
|
$table->setHeaders(['unknown symbol', 'guessed dependency']); |
110
|
|
|
$guesser = new DependencyGuesser(); |
111
|
|
|
foreach ($unknownSymbols as $unknownSymbol) { |
112
|
|
|
$guessedDependencies = []; |
113
|
|
|
foreach ($guesser($unknownSymbol) as $guessedDependency) { |
114
|
|
|
$guessedDependencies[] = $guessedDependency; |
115
|
|
|
} |
116
|
|
|
$table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]); |
117
|
|
|
} |
118
|
|
|
$table->render(); |
119
|
|
|
|
120
|
|
|
return ((int)(bool)$unknownSymbols); |
121
|
|
|
} |
122
|
|
|
|
123
|
1 |
|
private function getCheckOptions(InputInterface $input): Options |
124
|
|
|
{ |
125
|
1 |
|
$fileName = $input->getOption('config-file'); |
126
|
1 |
|
if (!$fileName) { |
127
|
1 |
|
return new Options(); |
128
|
|
|
} |
129
|
|
|
return new Options((new JsonLoader($fileName))->getData()); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* @param string $jsonFile |
134
|
|
|
* @throws \ComposerRequireChecker\Exception\InvalidJsonException |
135
|
|
|
* @throws \ComposerRequireChecker\Exception\NotReadableException |
136
|
|
|
* @internal param string $composerJson the path to composer.json |
137
|
|
|
*/ |
138
|
1 |
|
private function checkJsonFile(string $jsonFile) |
139
|
|
|
{ |
140
|
|
|
// JsonLoader throws an exception if it cannot load the file |
141
|
1 |
|
new JsonLoader($jsonFile); |
142
|
1 |
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @param InputInterface $input |
146
|
|
|
* @return LocateASTFromFiles |
147
|
|
|
*/ |
148
|
1 |
|
private function getASTFromFilesLocator(InputInterface $input): LocateASTFromFiles |
149
|
|
|
{ |
150
|
1 |
|
$errorHandler = $input->getOption('ignore-parse-errors') ? new CollectingErrorHandler() : null; |
151
|
1 |
|
$sourcesASTs = new LocateASTFromFiles((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $errorHandler); |
152
|
1 |
|
return $sourcesASTs; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
} |
156
|
|
|
|
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.