Passed
Pull Request — master (#51)
by Dominik
02:37
created

CheckCommand::resolveLicenses()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 17
ccs 8
cts 9
cp 0.8889
rs 9.9666
cc 4
nc 4
nop 1
crap 4.0218
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Dominikb\ComposerLicenseChecker;
6
7
use Dominikb\ComposerLicenseChecker\Contracts\DependencyLoaderAware;
8
use Dominikb\ComposerLicenseChecker\Contracts\LicenseConstraintAware;
9
use Dominikb\ComposerLicenseChecker\Contracts\LicenseLookupAware;
10
use Dominikb\ComposerLicenseChecker\Exceptions\CommandExecutionException;
11
use Dominikb\ComposerLicenseChecker\Traits\DependencyLoaderAwareTrait;
12
use Dominikb\ComposerLicenseChecker\Traits\LicenseConstraintAwareTrait;
13
use Dominikb\ComposerLicenseChecker\Traits\LicenseLookupAwareTrait;
14
use Symfony\Component\Console\Command\Command;
15
use Symfony\Component\Console\Input\InputDefinition;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Input\InputOption;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Style\SymfonyStyle;
20
21
class CheckCommand extends Command implements LicenseLookupAware, LicenseConstraintAware, DependencyLoaderAware
22
{
23
    use LicenseLookupAwareTrait, LicenseConstraintAwareTrait, DependencyLoaderAwareTrait;
24
25
    /** @var SymfonyStyle */
26
    private $io;
27
28
    protected function configure()
29
    {
30
        $this->setDefinition(new InputDefinition([
31
            new InputOption(
32
                'project-path',
33
                'p',
34
                InputOption::VALUE_OPTIONAL,
35
                'Path to directory of composer.json file',
36
                realpath('.')
37
            ),
38
            new InputOption(
39
                'composer',
40
                'c',
41
                InputOption::VALUE_OPTIONAL,
42
                'Path to composer executable',
43
                'composer'
44
            ),
45
            new InputOption(
46
                'no-dev',
47
                null,
48
                InputOption::VALUE_OPTIONAL,
49
                'Do not include dev dependencies',
50
                'false'
51
            ),
52
            new InputOption(
53
                'allowlist',
54
                'a',
55
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
56
                'Set a license you want to permit for usage'
57
            ),
58
            new InputOption(
59
                'blocklist',
60
                'b',
61
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
62
                'Mark a specific license prohibited for usage'
63
            ),
64
            new InputOption(
65
                'allow',
66
                '',
67
                InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
68
                'Determine a vendor or package to always be allowed and never trigger violations'
69
            ),
70
        ]));
71
    }
72
73
    public static function getDefaultName(): ?string
74
    {
75
        return 'check';
76
    }
77
78
    /**
79
     * @throws CommandExecutionException
80
     */
81 3
    public function execute(InputInterface $input, OutputInterface $output): int
82
    {
83 3
        $this->io = new SymfonyStyle($input, $output);
84
85 3
        $this->io->title('Reading through dependencies and checking their licenses ...');
86
87 3
        $this->ensureCommandCanBeExecuted();
88
89 3
        $dependencies = $this->dependencyLoader->loadDependencies(
0 ignored issues
show
Bug introduced by
The method loadDependencies() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

89
        /** @scrutinizer ignore-call */ 
90
        $dependencies = $this->dependencyLoader->loadDependencies(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
90 3
            $input->getOption('composer'),
91 3
            $input->getOption('project-path'),
92 3
            ($input->getOption('no-dev') ?? 'true') === 'true'
93 3
        );
94
95 3
        $this->io->writeln(count($dependencies).' dependencies were found ...');
96 3
        $this->io->newLine();
97
98 3
        $violations = $this->determineViolations($dependencies,
99 3
            $this->resolveLicenses($input->getOption('blocklist')),
100 3
            $this->resolveLicenses($input->getOption('allowlist')),
101 3
            $input->getOption('allow')
102 3
        );
103
104
        try {
105 3
            $this->handleViolations($violations);
106 1
            $this->io->success('Command finished successfully. No violations detected!');
107 2
        } catch (CommandExecutionException $exception) {
108 2
            $this->io->error($exception->getMessage());
109
110 2
            return self::FAILURE;
111
        }
112
113 1
        return self::SUCCESS;
114
    }
115
116
    /**
117
     * @throws CommandExecutionException
118
     */
119 3
    private function ensureCommandCanBeExecuted(): void
120
    {
121 3
        if (! $this->licenseLookup) {
122
            throw new CommandExecutionException('LicenseLookup must be set via setLicenseLookup() before the command can be executed!');
123
        }
124
125 3
        if (! $this->dependencyLoader) {
126
            throw new CommandExecutionException('DependencyLoader must be set via setDependencyLoader() before the command can be executed!');
127
        }
128
    }
129
130 3
    private function resolveLicenses(array $licenses): array
131
    {
132 3
        $out = [];
133 3
        foreach ($licenses as $license) {
134 3
            if (strlen($license) <= 0) {
135
                continue;
136
            } // Ignore empty string
137
138
            // Suppress warnings about missing files. Simple file_exists checking would not work for URIs.
139 3
            if ($contents = @file($license, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)) {
140 2
                $out = array_merge($out, $contents);
141
            } else {
142 2
                $out[] = $license;
143
            }
144
        }
145
146 3
        return $out;
147
    }
148
149 3
    private function determineViolations(array $dependencies, array $blocklist, array $allowlist, array $allowed): array
150
    {
151 3
        $this->licenseConstraintHandler->setBlocklist($blocklist);
0 ignored issues
show
Bug introduced by
The method setBlocklist() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
        $this->licenseConstraintHandler->/** @scrutinizer ignore-call */ 
152
                                         setBlocklist($blocklist);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
152 3
        $this->licenseConstraintHandler->setAllowlist($allowlist);
153
154 3
        $this->licenseConstraintHandler->allow(array_map(function ($dependency) {
155
            return new Dependency($dependency);
156 3
        }, $allowed));
157
158 3
        return $this->licenseConstraintHandler->detectViolations($dependencies);
159
    }
160
161
    /**
162
     * @param  ConstraintViolation[]  $violations
163
     *
164
     * @throws CommandExecutionException
165
     */
166 3
    private function handleViolations(array $violations): void
167
    {
168 3
        $violationsFound = false;
169
170 3
        foreach ($violations as $violation) {
171 3
            if ($violation->hasViolators()) {
172 2
                $this->io->error($violation->getTitle());
173 2
                $this->reportViolators($violation->getViolators());
174 2
                $violationsFound = true;
175
            }
176
        }
177
178 3
        if ($violationsFound) {
179 2
            throw new CommandExecutionException('Violators found during execution!');
180
        }
181
    }
182
183
    /**
184
     * @param  Dependency[]  $violators
185
     */
186 2
    private function reportViolators(array $violators): void
187
    {
188 2
        $byLicense = [];
189 2
        foreach ($violators as $violator) {
190 2
            foreach ($violator->getLicenses() as $license) {
191 2
                if (! isset($byLicense[$license])) {
192 2
                    $byLicense[$license] = [];
193
                }
194 2
                $byLicense[$license][] = $violator;
195
            }
196
        }
197
198 2
        foreach ($byLicense as $license => $violators) {
199 2
            $violatorNames = array_map(function (Dependency $dependency) {
200 2
                return sprintf('"%s [%s]"', $dependency->getName(), $dependency->getVersion());
201 2
            }, $violators);
202
203 2
            $this->io->title($license);
204 2
            $this->io->writeln(implode(', ', $violatorNames));
205
        }
206
    }
207
}
208