Failed Conditions
Pull Request — master (#162)
by
unknown
05:20
created

CheckCommand::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 1

Importance

Changes 4
Bugs 0 Features 2
Metric Value
eloc 19
c 4
b 0
f 2
dl 0
loc 23
ccs 18
cts 18
cp 1
rs 9.6333
cc 1
nc 1
nop 0
crap 1
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()) {
57 4
            $output->writeln($this->getApplication()->getLongVersion());
58
        }
59
60 4
        $composerJson = realpath($input->getArgument('composer-json'));
0 ignored issues
show
Bug introduced by
It seems like $input->getArgument('composer-json') can also be of type string[]; however, parameter $path of realpath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

60
        $composerJson = realpath(/** @scrutinizer ignore-type */ $input->getArgument('composer-json'));
Loading history...
61 4
        if (false === $composerJson) {
62 1
            throw new \InvalidArgumentException('file not found: [' . $input->getArgument('composer-json') . ']');
0 ignored issues
show
Bug introduced by
Are you sure $input->getArgument('composer-json') of type null|string|string[] can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

62
            throw new \InvalidArgumentException('file not found: [' . /** @scrutinizer ignore-type */ $input->getArgument('composer-json') . ']');
Loading history...
63
        }
64 3
        $composerData = $this->getComposerData($composerJson);
65
66 3
        $options = $this->getCheckOptions($input);
67
68 3
        if ($options->getAutoloaderPath() === null) {
69 3
            $options->setAutoloaderPath($this->findComposerAutoloader($composerJson, $composerData));
70
        }
71
72 3
        $getPackageSourceFiles = new LocateComposerPackageSourceFiles();
73 3
        $getAdditionalSourceFiles = new LocateFilesByGlobPattern();
74
75 3
        $sourcesASTs = $this->getASTFromFilesLocator($input);
76
77 3
        $this->verbose("Collecting defined vendor symbols... ", $output);
78 3
        $definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs(
79 3
            (new ComposeGenerators())->__invoke(
80 3
                $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)),
81 3
                $getPackageSourceFiles($composerData, dirname($composerJson)),
82 3
                (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson)
83
            )
84
        ));
85 3
        $this->verbose("found " . count($definedVendorSymbols) . " symbols.", $output, true);
86
87 3
        $this->verbose("Collecting defined extension symbols... ", $output);
88 3
        $definedExtensionSymbols = (new LocateDefinedSymbolsFromExtensions())->__invoke(
89 3
            (new DefinedExtensionsResolver())->__invoke($composerJson, $options->getPhpCoreExtensions())
90
        );
91 3
        $this->verbose("found " . count($definedExtensionSymbols) . " symbols.", $output, true);
92
93 3
        $this->verbose("Collecting used symbols... ", $output);
94 3
        $usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke($sourcesASTs(
95 3
            (new ComposeGenerators())->__invoke(
96 3
                $getPackageSourceFiles($composerData, dirname($composerJson)),
97 3
                $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson))
98
            )
99
        ));
100 3
        $this->verbose("found " . count($usedSymbols) . " symbols.", $output, true);
101
102 3
        if (!count($usedSymbols)) {
103
            throw new \LogicException('There were no symbols found, please check your configuration.');
104
        }
105
106 3
        $this->verbose("Checking for unknown symbols... ", $output, true);
107 3
        $unknownSymbols = array_diff(
108 3
            $usedSymbols,
109 3
            $definedVendorSymbols,
110 3
            $definedExtensionSymbols,
111 3
            $options->getSymbolWhitelist()
112
        );
113
114 3
        if (!$unknownSymbols) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unknownSymbols of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
115 3
            $output->writeln("There were no unknown symbols found.");
116 3
            return 0;
117
        }
118
119
        $output->writeln("The following unknown symbols were found:");
120
        $table = new Table($output);
121
        $table->setHeaders(['unknown symbol', 'guessed dependency']);
122
        $guesser = new DependencyGuesser($options);
123
        foreach ($unknownSymbols as $unknownSymbol) {
124
            $guessedDependencies = [];
125
            foreach ($guesser($unknownSymbol) as $guessedDependency) {
126
                $guessedDependencies[] = $guessedDependency;
127
            }
128
            $table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]);
129
        }
130
        $table->render();
131
132
        return ((int)(bool)$unknownSymbols);
133
    }
134
135 3
    private function getCheckOptions(InputInterface $input): Options
136
    {
137 3
        $fileName = $input->getOption('config-file');
138 3
        if (!$fileName) {
139 2
            return new Options();
140
        }
141 1
        return new Options((new JsonLoader($fileName))->getData());
142
    }
143
144 3
    private function findComposerAutoloader(string $composerJson, array $composerData): ?string
145
    {
146 3
        $projectDir = dirname($composerJson);
147
148 3
        $paths = [];
149 3
        if (isset($composerData['config']['vendor-dir'])) {
150
            $vendorDir = $composerData['config']['vendor-dir'];
151
            if (strpos($vendorDir, '~' . \DIRECTORY_SEPARATOR) === 0) {
152
                $paths[] = $_ENV['HOME'] . substr($vendorDir, 1);
153
            } elseif (strpos($vendorDir, '$HOME' . \DIRECTORY_SEPARATOR) === 0) {
154
                $paths[] = $_ENV['HOME'] . substr($vendorDir, 5);
155
            } elseif (strpos($vendorDir, \DIRECTORY_SEPARATOR) === 0) {
156
                $paths[] = $vendorDir;
157
            } else {
158
                $paths[] = $projectDir . \DIRECTORY_SEPARATOR . $vendorDir;
159
            }
160
        }
161 3
        $paths[] = $projectDir . \DIRECTORY_SEPARATOR . 'vendor';
162
163 3
        foreach ($paths as $path) {
164 3
            $autoloaderPath = $path .  \DIRECTORY_SEPARATOR . '/autoload.php';
165 3
            if (file_exists($autoloaderPath)) {
166 3
                return $autoloaderPath;
167
            }
168
        }
169
170
        return null;
171
    }
172
173
    /**
174
     * @param string $jsonFile
175
     * @throws \ComposerRequireChecker\Exception\InvalidJsonException
176
     * @throws \ComposerRequireChecker\Exception\NotReadableException
177
     */
178 3
    private function getComposerData(string $jsonFile): array
179
    {
180
        // JsonLoader throws an exception if it cannot load the file
181 3
        return (new JsonLoader($jsonFile))->getData();
182
    }
183
184
    /**
185
     * @param InputInterface $input
186
     * @return LocateASTFromFiles
187
     */
188 3
    private function getASTFromFilesLocator(InputInterface $input): LocateASTFromFiles
189
    {
190 3
        $errorHandler = $input->getOption('ignore-parse-errors') ? new CollectingErrorHandler() : null;
191 3
        $sourcesASTs = new LocateASTFromFiles((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $errorHandler);
192 3
        return $sourcesASTs;
193
    }
194
195
196
    /**
197
     * @param string $string the message that should be printed
198
     * @param OutputInterface $output the output to log to
199
     * @param bool $newLine if a new line will be started afterwards
200
     */
201 3
    private function verbose(string $string, OutputInterface $output, bool $newLine = false): void
202
    {
203 3
        if (!$output->isVerbose()) {
204 2
            return;
205
        }
206
207 1
        $output->write($string, $newLine);
208 1
    }
209
}
210