Passed
Pull Request — master (#68)
by Björn
03:16
created

CheckCommand::handleResult()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4.0047

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 21
ccs 14
cts 15
cp 0.9333
rs 9.7998
c 0
b 0
f 0
cc 4
nc 4
nop 2
crap 4.0047
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\GeneratorUtil\ComposeGenerators;
13
use ComposerRequireChecker\JsonLoader;
14
use ComposerRequireChecker\UsedSymbolsLocator\LocateUsedSymbolsFromASTRoots;
15
use PhpParser\ErrorHandler\Collecting as CollectingErrorHandler;
16
use PhpParser\ParserFactory;
17
use Symfony\Component\Console\Command\Command;
18
use Symfony\Component\Console\Helper\Table;
19
use Symfony\Component\Console\Input\InputArgument;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\InputOption;
22
use Symfony\Component\Console\Output\OutputInterface;
23
24
class CheckCommand extends Command
25
{
26 4
    protected function configure()
27
    {
28
        $this
29 4
            ->setName('check')
30 4
            ->setDescription('check the defined dependencies against your code')
31 4
            ->addOption(
32 4
                'config-file',
33 4
                null,
34 4
                InputOption::VALUE_REQUIRED,
35 4
                'the config.json file to configure the checking options'
36
            )
37 4
            ->addArgument(
38 4
                'composer-json',
39 4
                InputArgument::OPTIONAL,
40 4
                'the composer.json of your package, that should be checked',
41 4
                './composer.json'
42
            )
43 4
        ->addOption(
44 4
                'ignore-parse-errors',
45 4
                null,
46 4
                InputOption::VALUE_NONE,
47
                'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise'
48 4
                . ' errors will be thrown'
49
            );
50 4
    }
51
52 2
    protected function execute(InputInterface $input, OutputInterface $output): int
53
    {
54 2
        if (!$output->isQuiet()) {
55 2
            $output->writeln($this->getApplication()->getLongVersion());
56
        }
57
58 2
        $composerJson = realpath($input->getArgument('composer-json'));
59 2
        if (false === $composerJson) {
60 1
            throw new \InvalidArgumentException('file not found: [' . $input->getArgument('composer-json') . ']');
61
        }
62 1
        $this->checkJsonFile($composerJson);
63
64 1
        $options = $this->getCheckOptions($input);
65
66 1
        $getPackageSourceFiles = new LocateComposerPackageSourceFiles();
67
68 1
        $sourcesASTs = $this->getASTFromFilesLocator($input);
69
70 1
        $definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs(
71 1
            (new ComposeGenerators())->__invoke(
72 1
                $getPackageSourceFiles($composerJson),
73 1
                (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson)
74
            )
75
        ));
76 1
        while (count($definedVendorSymbols->getIncludes())) {
77
            (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs($definedVendorSymbols->getIncludes()), $definedVendorSymbols);
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($sourcesASTs($getPackageSourceFiles($composerJson)));
86
87 1
        if (!count($usedSymbols)) {
88
            throw new \LogicException('There were no symbols found, please check your configuration.');
89
        }
90
91 1
        return $this->handleResult(
92 1
            array_diff(
93 1
                $usedSymbols,
94 1
                $definedVendorSymbols->getSymbols(),
95 1
                $definedExtensionSymbols,
96 1
                $options->getSymbolWhitelist()
97
            ),
98 1
            $output
99
        );
100
    }
101
102
    /**
103
     * @param array $unknownSymbols
104
     * @param OutputInterface $output
105
     * @return int
106
     */
107 2
    private function handleResult(?array $unknownSymbols, OutputInterface $output): int
108
    {
109 2
        if (!$unknownSymbols) {
110 1
            $output->writeln("There were no unknown symbols found.");
111 1
            return 0;
112
        }
113
114 1
        $output->writeln("The following unknown symbols were found:");
115 1
        $table = new Table($output);
116 1
        $table->setHeaders(['unknown symbol', 'guessed dependency']);
117 1
        $guesser = new DependencyGuesser();
118 1
        foreach ($unknownSymbols as $unknownSymbol) {
119 1
            $guessedDependencies = [];
120 1
            foreach ($guesser($unknownSymbol) as $guessedDependency) {
121
                $guessedDependencies[] = $guessedDependency;
122
            }
123 1
            $table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]);
124
        }
125 1
        $table->render();
126
127 1
        return ((int)(bool)$unknownSymbols);
128
    }
129
130
    /**
131
     * @param InputInterface $input
132
     * @return Options
133
     */
134 1
    private function getCheckOptions(InputInterface $input): Options
135
    {
136 1
        $fileName = $input->getOption('config-file');
137 1
        if (!$fileName) {
138 1
            return new Options();
139
        }
140
        return new Options((new JsonLoader($fileName))->getData());
141
    }
142
143
    /**
144
     * @param string $jsonFile
145
     * @throws \ComposerRequireChecker\Exception\InvalidJsonException
146
     * @throws \ComposerRequireChecker\Exception\NotReadableException
147
     * @internal param string $composerJson the path to composer.json
148
     */
149 1
    private function checkJsonFile(string $jsonFile)
150
    {
151
        // JsonLoader throws an exception if it cannot load the file
152 1
        new JsonLoader($jsonFile);
153 1
    }
154
155
    /**
156
     * @param InputInterface $input
157
     * @return LocateASTFromFiles
158
     */
159 1
    private function getASTFromFilesLocator(InputInterface $input): LocateASTFromFiles
160
    {
161 1
        $errorHandler = $input->getOption('ignore-parse-errors') ? new CollectingErrorHandler() : null;
162 1
        $sourcesASTs = new LocateASTFromFiles((new ParserFactory())->create(ParserFactory::PREFER_PHP7), $errorHandler);
163 1
        return $sourcesASTs;
164
    }
165
166
}
167