Passed
Push — master ( 215b59...9cd7ac )
by Marco
38s
created

CheckCommand::checkJsonFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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