Test Failed
Pull Request — master (#115)
by Cees-Jan
04:07
created

CheckCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
ccs 5
cts 5
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace ComposerRequireChecker\Cli;
4
5
use Composer\Autoload\ClassLoader;
6
use ComposerRequireChecker\ASTLocator\LocateASTFromFiles;
7
use ComposerRequireChecker\DefinedExtensionsResolver\DefinedExtensionsResolver;
8
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromASTRoots;
9
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromExtensions;
10
use ComposerRequireChecker\DependencyGuesser\DependencyGuesser;
11
use ComposerRequireChecker\FileLocator\LocateComposerPackageDirectDependenciesSourceFiles;
12
use ComposerRequireChecker\FileLocator\LocateComposerPackageSourceFiles;
13
use ComposerRequireChecker\FileLocator\LocateFilesByGlobPattern;
14
use ComposerRequireChecker\GeneratorUtil\ComposeGenerators;
15
use ComposerRequireChecker\JsonLoader;
16
use ComposerRequireChecker\UsedSymbolsLocator\LocateUsedSymbolsFromASTRoots;
17
use PhpParser\ErrorHandler\Collecting as CollectingErrorHandler;
18
use PhpParser\ParserFactory;
19
use Symfony\Component\Console\Command\Command;
20
use Symfony\Component\Console\Helper\Table;
21
use Symfony\Component\Console\Input\InputArgument;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Input\InputOption;
24
use Symfony\Component\Console\Output\OutputInterface;
25
use function dirname;
26
27
class CheckCommand extends Command
28 5
{
29
    /** @var ClassLoader */
30
    private $classLoader;
31 5
32 5
    public function __construct(ClassLoader $autoloader)
33 5
    {
34 5
        $this->classLoader = $autoloader;
35 5
36 5
        parent::__construct();
37 5
    }
38
39 5
    protected function configure()
40 5
    {
41 5
        $this
42 5
            ->setName('check')
43 5
            ->setDescription('check the defined dependencies against your code')
44
            ->addOption(
45 5
                'config-file',
46 5
                null,
47 5
                InputOption::VALUE_REQUIRED,
48 5
                'the config.json file to configure the checking options'
49
            )
50 5
            ->addArgument(
51
                'composer-json',
52 5
                InputArgument::OPTIONAL,
53
                'the composer.json of your package, that should be checked',
54 4
                './composer.json'
55
            )
56 4
            ->addOption(
57 4
                'ignore-parse-errors',
58
                null,
59
                InputOption::VALUE_NONE,
60 4
                'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise'
61 4
                . ' errors will be thrown'
62 1
            );
63
    }
64 3
65 3
    protected function execute(InputInterface $input, OutputInterface $output): int
66
    {
67 3
        if (!$output->isQuiet()) {
68
            $output->writeln($this->getApplication()->getLongVersion());
69 3
        }
70 3
71
        $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

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

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