Failed Conditions
Pull Request — master (#163)
by Guillaume
07:51
created

CheckCommand   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 163
Duplicated Lines 0 %

Test Coverage

Coverage 95.79%

Importance

Changes 12
Bugs 1 Features 3
Metric Value
wmc 17
eloc 89
c 12
b 1
f 3
dl 0
loc 163
ccs 91
cts 95
cp 0.9579
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 29 1
A getASTFromFilesLocator() 0 5 2
A getCheckOptions() 0 7 2
A verbose() 0 7 2
B execute() 0 83 9
A getComposerData() 0 4 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 7
    protected function configure()
29
    {
30
        $this
31 7
            ->setName('check')
32 7
            ->setDescription('check the defined dependencies against your code')
33 7
            ->addOption(
34 7
                'config-file',
35 7
                null,
36 7
                InputOption::VALUE_REQUIRED,
37 7
                'the config.json file to configure the checking options'
38
            )
39 7
            ->addArgument(
40 7
                'composer-json',
41 7
                InputArgument::OPTIONAL,
42 7
                'the composer.json of your package, that should be checked',
43 7
                './composer.json'
44
            )
45 7
            ->addOption(
46 7
                'ignore-parse-errors',
47 7
                null,
48 7
                InputOption::VALUE_NONE,
49
                'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise'
50 7
                . ' errors will be thrown'
51
            )
52 7
            ->addOption(
53 7
                'dev',
54 7
                null,
55 7
                InputOption::VALUE_NONE,
56 7
                'check that the development sources (i.e. tests) have not indirect dependencies'
57
            );
58 7
    }
59
60 6
    protected function execute(InputInterface $input, OutputInterface $output): int
61
    {
62 6
        if (!$output->isQuiet()) {
63 6
            $output->writeln($this->getApplication()->getLongVersion());
64
        }
65
66 6
        $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

66
        $composerJson = realpath(/** @scrutinizer ignore-type */ $input->getArgument('composer-json'));
Loading history...
67 6
        if (false === $composerJson) {
68 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

68
            throw new \InvalidArgumentException('file not found: [' . /** @scrutinizer ignore-type */ $input->getArgument('composer-json') . ']');
Loading history...
69
        }
70 5
        $composerData = $this->getComposerData($composerJson);
71
72 5
        $checkDevSources = (bool)$input->getOption('dev');
73
74 5
        $options = $this->getCheckOptions($input);
75
76 5
        $getPackageSourceFiles = new LocateComposerPackageSourceFiles();
77 5
        $getAdditionalSourceFiles = new LocateFilesByGlobPattern();
78
79 5
        $sourcesASTs = $this->getASTFromFilesLocator($input);
80
81 5
        $additionalLocators = $checkDevSources ? [
82
            $getPackageSourceFiles($composerData, dirname($composerJson), 'autoload-dev'),
83
            (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson, 'require-dev')
84 5
        ] : [];
85
86 5
        $this->verbose("Collecting defined vendor symbols... ", $output);
87 5
        $definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs(
88 5
            (new ComposeGenerators())->__invoke(
89 5
                $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)),
90 5
                $getPackageSourceFiles($composerData, dirname($composerJson), 'autoload'),
91 5
                (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson, 'require'),
92 5
                ...$additionalLocators
93
            )
94
        ));
95 5
        $this->verbose("found " . count($definedVendorSymbols) . " symbols.", $output, true);
96
97 5
        $this->verbose("Collecting defined extension symbols... ", $output);
98 5
        $definedExtensionSymbols = (new LocateDefinedSymbolsFromExtensions())->__invoke(
99 5
            (new DefinedExtensionsResolver())->__invoke($composerJson, $options->getPhpCoreExtensions())
100
        );
101 5
        $this->verbose("found " . count($definedExtensionSymbols) . " symbols.", $output, true);
102
103 5
        $this->verbose("Collecting used symbols... ", $output);
104 5
        $usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke($sourcesASTs(
105 5
            (new ComposeGenerators())->__invoke(
106 5
                $getPackageSourceFiles($composerData, dirname($composerJson), $checkDevSources ? 'autoload-dev' : 'autoload'),
107 5
                $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson))
108
            )
109
        ));
110 5
        $this->verbose("found " . count($usedSymbols) . " symbols.", $output, true);
111
112 5
        if (!count($usedSymbols)) {
113
            throw new \LogicException('There were no symbols found, please check your configuration.');
114
        }
115
116 5
        $this->verbose("Checking for unknown symbols... ", $output, true);
117 5
        $unknownSymbols = array_diff(
118 5
            $usedSymbols,
119 5
            $definedVendorSymbols,
120 5
            $definedExtensionSymbols,
121 5
            $options->getSymbolWhitelist()
122
        );
123
124 5
        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...
125 4
            $output->writeln("There were no unknown symbols found.");
126 4
            return 0;
127
        }
128
129 1
        $output->writeln("The following unknown symbols were found:");
130 1
        $table = new Table($output);
131 1
        $table->setHeaders(['unknown symbol', 'guessed dependency']);
132 1
        $guesser = new DependencyGuesser($options);
133 1
        foreach ($unknownSymbols as $unknownSymbol) {
134 1
            $guessedDependencies = [];
135 1
            foreach ($guesser($unknownSymbol) as $guessedDependency) {
136
                $guessedDependencies[] = $guessedDependency;
137
            }
138 1
            $table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]);
139
        }
140 1
        $table->render();
141
142 1
        return ((int)(bool)$unknownSymbols);
143
    }
144
145 5
    private function getCheckOptions(InputInterface $input): Options
146
    {
147 5
        $fileName = $input->getOption('config-file');
148 5
        if (!$fileName) {
149 3
            return new Options();
150
        }
151 2
        return new Options((new JsonLoader($fileName))->getData());
152
    }
153
154
    /**
155
     * @param string $jsonFile
156
     * @throws \ComposerRequireChecker\Exception\InvalidJsonException
157
     * @throws \ComposerRequireChecker\Exception\NotReadableException
158
     */
159 5
    private function getComposerData(string $jsonFile): array
160
    {
161
        // JsonLoader throws an exception if it cannot load the file
162 5
        return (new JsonLoader($jsonFile))->getData();
163
    }
164
165
    /**
166
     * @param InputInterface $input
167
     * @return LocateASTFromFiles
168
     */
169 5
    private function getASTFromFilesLocator(InputInterface $input): LocateASTFromFiles
170
    {
171 5
        $errorHandler = $input->getOption('ignore-parse-errors') ? new CollectingErrorHandler() : null;
172 5
        $sourcesASTs = new LocateASTFromFiles((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $errorHandler);
173 5
        return $sourcesASTs;
174
    }
175
176
177
    /**
178
     * @param string $string the message that should be printed
179
     * @param OutputInterface $output the output to log to
180
     * @param bool $newLine if a new line will be started afterwards
181
     */
182 5
    private function verbose(string $string, OutputInterface $output, bool $newLine = false): void
183
    {
184 5
        if (!$output->isVerbose()) {
185 4
            return;
186
        }
187
188 1
        $output->write($string, $newLine);
189 1
    }
190
}
191