Completed
Pull Request — master (#9)
by Matthias
02:27
created

CheckCommand   B

Complexity

Total Complexity 13

Size/Duplication

Total Lines 117
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 3
Bugs 0 Features 1
Metric Value
wmc 13
c 3
b 0
f 1
lcom 1
cbo 16
dl 0
loc 117
rs 8.4615

4 Methods

Rating   Name   Duplication   Size   Complexity  
A configure() 0 19 1
B execute() 0 56 5
A getCheckOptions() 0 19 4
A checkJsonFile() 0 11 3
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: matthias
5
 * Date: 28.11.15
6
 * Time: 16:16
7
 */
8
9
namespace ComposerRequireChecker\Cli;
10
11
use ComposerRequireChecker\ASTLocator\LocateASTFromFiles;
12
use ComposerRequireChecker\DefinedExtensionsResolver\DefinedExtensionsResolver;
13
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromASTRoots;
14
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromExtensions;
15
use ComposerRequireChecker\DependencyGuesser\DependencyGuesser;
16
use ComposerRequireChecker\Exception\InvalidInputFileException;
17
use ComposerRequireChecker\FileLocator\LocateComposerPackageDirectDependenciesSourceFiles;
18
use ComposerRequireChecker\FileLocator\LocateComposerPackageSourceFiles;
19
use ComposerRequireChecker\GeneratorUtil\ComposeGenerators;
20
use ComposerRequireChecker\UsedSymbolsLocator\LocateUsedSymbolsFromASTRoots;
21
use PhpParser\ParserFactory;
22
use Symfony\Component\Console\Command\Command;
23
use Symfony\Component\Console\Helper\Table;
24
use Symfony\Component\Console\Helper\TableCell;
25
use Symfony\Component\Console\Input\InputArgument;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\OutputInterface;
29
30
class CheckCommand extends Command
31
{
32
    protected function configure()
33
    {
34
        $this
35
            ->setName('check')
36
            ->setDescription('check the defined dependencies against your code')
37
            ->addOption(
38
                'config-file',
39
                null,
40
                InputOption::VALUE_REQUIRED,
41
                'the config.json file to configure the checking options'
42
            )
43
            ->addArgument(
44
                'composer-json',
45
                InputArgument::OPTIONAL,
46
                'the composer.json of your package, that should be checked',
47
                './composer.json'
48
            )
49
        ;
50
    }
51
52
    protected function execute(InputInterface $input, OutputInterface $output) : int
53
    {
54
55
        if(!$output->isQuiet()) {
56
            $output->writeln($this->getApplication()->getLongVersion());
57
        }
58
59
        $composerJson = $input->getArgument('composer-json');
60
        $this->checkJsonFile($composerJson);
61
62
        $getPackageSourceFiles = new LocateComposerPackageSourceFiles();
63
64
        $sourcesASTs  = new LocateASTFromFiles((new ParserFactory())->create(ParserFactory::PREFER_PHP7));
65
66
        $definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs(
67
            (new ComposeGenerators())->__invoke(
68
                $getPackageSourceFiles($composerJson),
69
                (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson)
0 ignored issues
show
Unused Code introduced by
The call to ComposeGenerators::__invoke() has too many arguments starting with (new \ComposerRequireChe...__invoke($composerJson).

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
70
            )
71
        ));
72
73
        $options = $this->getCheckOptions($input);
74
75
        $definedExtensionSymbols = (new LocateDefinedSymbolsFromExtensions())->__invoke(
76
            (new DefinedExtensionsResolver())->__invoke($composerJson, $options->getPhpCoreExtensions())
77
        );
78
79
        $usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke($sourcesASTs($getPackageSourceFiles($composerJson)));
80
81
        $unknownSymbols = array_diff(
82
            $usedSymbols,
83
            $definedVendorSymbols,
84
            $definedExtensionSymbols,
85
            $options->getSymbolWhitelist()
86
        );
87
88
        if (!$unknownSymbols) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unknownSymbols of type string[] 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...
89
            $output->writeln("There were no unknown symbols found.");
90
            return 0;
91
        }
92
93
        $output->writeln("The following unknown symbols were found:");
94
        $table = new Table($output);
95
        $table->setHeaders(['unknown symbol', 'guessed dependency']);
96
        $guesser = new DependencyGuesser();
97
        foreach ($unknownSymbols as $unknownSymbol) {
98
            $guessedDependencies = [];
99
            foreach($guesser($unknownSymbol) as $guessedDependency) {
100
                $guessedDependencies[] = $guessedDependency;
101
            }
102
            $table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]);
103
        }
104
        $table->render();
105
106
        return ((int) (bool) $unknownSymbols);
107
    }
108
109
    private function getCheckOptions(InputInterface $input) : Options
110
    {
111
        $fileName = $input->getOption('config-file');
112
        if(!$fileName) {
113
            return new Options();
114
        }
115
116
        if(!is_readable($fileName)) {
117
            throw new \InvalidArgumentException('unable to read ' . $fileName);
118
        }
119
120
        $jsonData = json_decode(file_get_contents($fileName), true);
121
        if(false === $jsonData) {
122
            throw new \Exception('error parsing the config file: ' . json_last_error_msg());
123
        }
124
125
        return new Options($jsonData);
126
127
    }
128
129
    /**
130
     * @param string $jsonFile
131
     * @throws InvalidInputFileException
132
     * @internal param string $composerJson the path to composer.json
133
     */
134
    private function checkJsonFile(string $jsonFile)
135
    {
136
        if(!is_readable($jsonFile)) {
137
            throw new InvalidInputFileException('cannot read ' . $jsonFile);
138
        }
139
140
        if(false == json_decode(file_get_contents($jsonFile))) {
141
            throw new InvalidInputFileException('error parsing ' . $jsonFile . ': ' . json_last_error_msg());
142
        }
143
144
    }
145
146
}