Passed
Pull Request — master (#77)
by Matthias
04:21
created

CheckCommand   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 127
Duplicated Lines 0 %

Test Coverage

Coverage 82.43%

Importance

Changes 0
Metric Value
wmc 13
eloc 70
dl 0
loc 127
ccs 61
cts 74
cp 0.8243
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getASTFromFilesLocator() 0 5 2
A getCheckOptions() 0 7 2
A configure() 0 23 1
A checkJsonFile() 0 4 1
B execute() 0 67 7
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())
85 1
            ->__invoke(
86 1
                $sourcesASTs($getPackageSourceFiles($composerJson)),
87 1
                $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson))
0 ignored issues
show
Unused Code introduced by
The call to ComposerRequireChecker\U...romASTRoots::__invoke() has too many arguments starting with $getAdditionalSourceFile...dirname($composerJson)). ( Ignorable by Annotation )

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

87
            ->/** @scrutinizer ignore-call */ __invoke(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
88
            );
89
90 1
        if (!count($usedSymbols)) {
91
            throw new \LogicException('There were no symbols found, please check your configuration.');
92
        }
93
94 1
        $unknownSymbols = array_diff(
95 1
            $usedSymbols,
96 1
            $definedVendorSymbols,
97 1
            $definedExtensionSymbols,
98 1
            $options->getSymbolWhitelist()
99
        );
100
101 1
        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...
102 1
            $output->writeln("There were no unknown symbols found.");
103 1
            return 0;
104
        }
105
106
        $output->writeln("The following unknown symbols were found:");
107
        $table = new Table($output);
108
        $table->setHeaders(['unknown symbol', 'guessed dependency']);
109
        $guesser = new DependencyGuesser();
110
        foreach ($unknownSymbols as $unknownSymbol) {
111
            $guessedDependencies = [];
112
            foreach ($guesser($unknownSymbol) as $guessedDependency) {
113
                $guessedDependencies[] = $guessedDependency;
114
            }
115
            $table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]);
116
        }
117
        $table->render();
118
119
        return ((int)(bool)$unknownSymbols);
120
    }
121
122 1
    private function getCheckOptions(InputInterface $input): Options
123
    {
124 1
        $fileName = $input->getOption('config-file');
125 1
        if (!$fileName) {
126 1
            return new Options();
127
        }
128
        return new Options((new JsonLoader($fileName))->getData());
129
    }
130
131
    /**
132
     * @param string $jsonFile
133
     * @throws \ComposerRequireChecker\Exception\InvalidJsonException
134
     * @throws \ComposerRequireChecker\Exception\NotReadableException
135
     * @internal param string $composerJson the path to composer.json
136
     */
137 1
    private function checkJsonFile(string $jsonFile)
138
    {
139
        // JsonLoader throws an exception if it cannot load the file
140 1
        new JsonLoader($jsonFile);
141 1
    }
142
143
    /**
144
     * @param InputInterface $input
145
     * @return LocateASTFromFiles
146
     */
147 1
    private function getASTFromFilesLocator(InputInterface $input): LocateASTFromFiles
148
    {
149 1
        $errorHandler = $input->getOption('ignore-parse-errors') ? new CollectingErrorHandler() : null;
150 1
        $sourcesASTs = new LocateASTFromFiles((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $errorHandler);
151 1
        return $sourcesASTs;
152
    }
153
154
}
155