Passed
Pull Request — master (#51)
by Dominik
04:57 queued 02:28
created

CheckCommand   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Test Coverage

Coverage 54.95%

Importance

Changes 6
Bugs 1 Features 1
Metric Value
wmc 22
eloc 98
c 6
b 1
f 1
dl 0
loc 189
ccs 61
cts 111
cp 0.5495
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getDefaultName() 0 3 1
A configure() 0 41 1
A ensureCommandCanBeExecuted() 0 8 3
A getDefaultDescription() 0 3 1
A determineViolations() 0 10 1
A execute() 0 33 2
A reportViolators() 0 19 5
A resolveLicenses() 0 17 4
A handleViolations() 0 14 4
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
    public static function getDefaultDescription(): ?string
79
    {
80
        return 'Check the licenses of all dependencies and fail on violations';
81
    }
82
83
    /**
84
     * @throws CommandExecutionException
85
     */
86 3
    public function execute(InputInterface $input, OutputInterface $output): int
87
    {
88 3
        $this->io = new SymfonyStyle($input, $output);
89
90 3
        $this->io->title('Reading through dependencies and checking their licenses ...');
91
92 3
        $this->ensureCommandCanBeExecuted();
93
94 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

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

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