Failed Conditions
Pull Request — master (#141)
by Matthias
08:49
created

CheckCommand::getCheckOptions()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.5923

Importance

Changes 2
Bugs 1 Features 1
Metric Value
eloc 8
c 2
b 1
f 1
dl 0
loc 16
ccs 6
cts 9
cp 0.6667
rs 10
cc 4
nc 4
nop 1
crap 4.5923
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 Phar;
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
{
29 5
    protected function configure()
30
    {
31
        $this
32 5
            ->setName('check')
33 5
            ->setDescription('check the defined dependencies against your code')
34 5
            ->addOption(
35 5
                'config-file',
36 5
                null,
37 5
                InputOption::VALUE_REQUIRED,
38 5
                'the config.json file to configure the checking options'
39
            )
40 5
            ->addArgument(
41 5
                'composer-json',
42 5
                InputArgument::OPTIONAL,
43 5
                'the composer.json of your package, that should be checked',
44 5
                './composer.json'
45
            )
46 5
            ->addOption(
47 5
                'ignore-parse-errors',
48 5
                null,
49 5
                InputOption::VALUE_NONE,
50
                'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise'
51 5
                . ' errors will be thrown'
52
            );
53 5
    }
54
55 4
    protected function execute(InputInterface $input, OutputInterface $output): int
56
    {
57 4
        if (!$output->isQuiet()) {
58 4
            $output->writeln($this->getApplication()->getLongVersion());
59
        }
60
61 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

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

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