diff --git a/README.md b/README.md index a9e6cb5..e09cd6a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This tool can be used to check the licenses of the used packages. Our package can be installed with composer with the following command: ```bash -composer require best-it/license-check --dev --prefer-dist +composer require best-it/license-check --dev ``` ## Usage diff --git a/src/Checker.php b/src/Checker.php index 8f06183..797c978 100644 --- a/src/Checker.php +++ b/src/Checker.php @@ -30,15 +30,38 @@ public function __construct(iterable $loaders) $this->loaders = $loaders; } + /** + * Check if the given package is allowed. + * + * @param string $package + * @param string[] $allowedPackages + * + * @return bool + */ + private function isPackageAllowed(string $package, array $allowedPackages): bool + { + $allowed = false; + + foreach ($allowedPackages as $allowedPackage) { + if (preg_match($allowedPackage, $package) === 1) { + $allowed = true; + break; + } + } + + return $allowed; + } + /** * Start the validation. * * @param Configuration $configuration * @param string $path + * @param int|null $depth * * @return Result */ - public function validate(Configuration $configuration, string $path): Result + public function validate(Configuration $configuration, string $path, ?int $depth = null): Result { $result = new Result(); @@ -46,7 +69,7 @@ public function validate(Configuration $configuration, string $path): Result foreach ($this->loaders as $loader) { $type = $loader->getName(); $allowedPackages = $configuration->getAllowedPackages($type); - foreach ($loader->getLicenses($path) as $package => $licenses) { + foreach ($loader->getLicenses($path, $depth) as $package => $licenses) { if (!$this->isPackageAllowed($package, $allowedPackages)) { if (count($licenses) === 0) { $result->addViolation( @@ -84,26 +107,4 @@ public function validate(Configuration $configuration, string $path): Result return $result; } - - /** - * Check if the given package is allowed. - * - * @param string $package - * @param string[] $allowedPackages - * - * @return bool - */ - private function isPackageAllowed(string $package, array $allowedPackages): bool - { - $allowed = false; - - foreach ($allowedPackages as $allowedPackage) { - if (preg_match($allowedPackage, $package) === 1) { - $allowed = true; - break; - } - } - - return $allowed; - } } diff --git a/src/Command/LicenseCheckCommand.php b/src/Command/LicenseCheckCommand.php index 05460fb..9f4a4f6 100644 --- a/src/Command/LicenseCheckCommand.php +++ b/src/Command/LicenseCheckCommand.php @@ -38,6 +38,13 @@ class LicenseCheckCommand extends Command */ private const OPTION_CONFIGURATION = 'configuration'; + /** + * Constant for the depth cli option. + * + * @var string OPTION_IGNORE_ERRORS + */ + private const OPTION_DEPTH = 'depth'; + /** * Constant for the ignore-errors cli option. * @@ -98,6 +105,12 @@ protected function configure(): void null, InputOption::VALUE_NONE, 'Don\'t return an error code.', + ) + ->addOption( + self::OPTION_DEPTH, + null, + InputOption::VALUE_REQUIRED, + 'Set the maximum depth of the directory iterators.', ); } @@ -129,7 +142,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int assert(is_string($configurationPath)); $configuration = $this->configurationLoader->load($configurationPath); - $resultSet = $this->checker->validate($configuration, $workingDirectory); + if (($depth = $input->getOption(self::OPTION_DEPTH)) !== null) { + assert(is_string($depth)); + $depth = (int) $depth; + } + + $resultSet = $this->checker->validate($configuration, $workingDirectory, $depth); $resultCode = defined('static::SUCCESS') ? static::SUCCESS : 0; diff --git a/src/LicenseLoader/AbstractLicenseLoader.php b/src/LicenseLoader/AbstractLicenseLoader.php new file mode 100644 index 0000000..40e1f87 --- /dev/null +++ b/src/LicenseLoader/AbstractLicenseLoader.php @@ -0,0 +1,93 @@ + + * @package BestIt\LicenseCheck\LicenseLoader + */ +abstract class AbstractLicenseLoader implements LicenseLoaderInterface +{ + /** + * Finder to get composer.json files. + * + * @var Finder $finder + */ + private Finder $finder; + + /** + * Search pattern to get license files. + * + * @var string|null $searchPattern + */ + protected ?string $searchPattern = null; + + /** + * ComposerLicenseLoader constructor. + * + * @param Finder $finder + */ + public function __construct(Finder $finder) + { + $this->finder = $finder; + } + + /** + * Get used licenses in the specified path. + * + * @param string $path Search path. + * @param int|null $depth (Optional) Search depth. + * + * @return array> + */ + public function getLicenses(string $path, ?int $depth = null): array + { + $result = new Result(); + + if ($this->searchPattern === null) { + throw new PatternNotDefinedException(); + } + + $iterator = $this->finder->path($this->searchPattern)->in($path); + + if ($depth !== null) { + $iterator->depth($depth); + } + + foreach ($iterator as $file) { + $filePath = $file->getPathname(); + $content = $file->getContents(); + if (!is_string($content)) { + throw new LicenseLoaderException(sprintf('Cannot read content of file %s', $filePath)); + } + + $decodedContent = json_decode($content, true); + if (!is_array($decodedContent)) { + throw new LicenseLoaderException(sprintf('Cannot decode content of file %s', $filePath)); + } + + $this->parseFile($decodedContent, $result); + } + + return $result->toArray(); + } + + /** + * Decode file contents. + * + * @param mixed[] $decodedContent Array of json file content. + * @param Result $result License result. + * + * @return void + */ + abstract protected function parseFile(array $decodedContent, Result $result): void; +} diff --git a/src/LicenseLoader/ComposerLicenseLoader.php b/src/LicenseLoader/ComposerLicenseLoader.php index 871c8f4..d19e5cf 100644 --- a/src/LicenseLoader/ComposerLicenseLoader.php +++ b/src/LicenseLoader/ComposerLicenseLoader.php @@ -4,8 +4,7 @@ namespace BestIt\LicenseCheck\LicenseLoader; -use BestIt\LicenseCheck\LicenseLoader\Exception\LicenseLoaderException; -use Symfony\Component\Finder\Finder; +use BestIt\LicenseCheck\LicenseLoader\Result\Result; /** * License loader for composer packages. @@ -13,55 +12,32 @@ * @author best it AG * @package BestIt\LicenseCheck\LicenseLoader */ -class ComposerLicenseLoader implements LicenseLoaderInterface +class ComposerLicenseLoader extends AbstractLicenseLoader { /** - * Finder to get composer.json files. + * Pattern for composer files. * - * @var Finder $finder + * @var string|null */ - private Finder $finder; + protected ?string $searchPattern = 'composer.lock'; /** - * ComposerLicenseLoader constructor. + * Parse composer.lock content. * - * @param Finder $finder - */ - public function __construct(Finder $finder) - { - $this->finder = $finder; - } - - /** - * Get used licenses in the specified path. + * @param mixed[] $decodedContent + * @param Result $result * - * @param string $path - * - * @return array> + * @return void */ - public function getLicenses(string $path): array + protected function parseFile(array $decodedContent, Result $result): void { - $result = []; - - foreach ($this->finder->name('composer.lock')->in($path) as $file) { - $filePath = $file->getPathname(); - $content = $file->getContents(); - if (!is_string($content)) { - throw new LicenseLoaderException(sprintf('Cannot read content of file %s', $filePath)); - } - - $decodedContent = json_decode($content, true); - if (!is_array($decodedContent)) { - throw new LicenseLoaderException(sprintf('Cannot decode content of file %s', $filePath)); - } - - // ToDo: Check json structure. - foreach ($decodedContent['packages'] as $package) { - $result[$package['name']] = $package['license'] ?? []; - } + // ToDo: Check json structure. + foreach ($decodedContent['packages'] as $package) { + $result->add( + (string) $package['name'], + $package['license'] ?? [], + ); } - - return $result; } /** diff --git a/src/LicenseLoader/Exception/PatternNotDefinedException.php b/src/LicenseLoader/Exception/PatternNotDefinedException.php new file mode 100644 index 0000000..aab8ffe --- /dev/null +++ b/src/LicenseLoader/Exception/PatternNotDefinedException.php @@ -0,0 +1,17 @@ + + * @package BestIt\LicenseCheck\LicenseLoader\Exception + */ +class PatternNotDefinedException extends LicenseCheckException +{ +} diff --git a/src/LicenseLoader/LicenseLoaderInterface.php b/src/LicenseLoader/LicenseLoaderInterface.php index b90a13f..29ab3aa 100644 --- a/src/LicenseLoader/LicenseLoaderInterface.php +++ b/src/LicenseLoader/LicenseLoaderInterface.php @@ -15,11 +15,12 @@ interface LicenseLoaderInterface /** * Get used licenses in the specified path. * - * @param string $path + * @param string $path Search path. + * @param int|null $depth (Optional) Search depth. * * @return array> */ - public function getLicenses(string $path): array; + public function getLicenses(string $path, ?int $depth = null): array; /** * Get name of the license loader. diff --git a/src/LicenseLoader/NodeLicenseLoader.php b/src/LicenseLoader/NodeLicenseLoader.php index 6bc148f..b1ab76a 100644 --- a/src/LicenseLoader/NodeLicenseLoader.php +++ b/src/LicenseLoader/NodeLicenseLoader.php @@ -4,8 +4,7 @@ namespace BestIt\LicenseCheck\LicenseLoader; -use BestIt\LicenseCheck\LicenseLoader\Exception\LicenseLoaderException; -use Symfony\Component\Finder\Finder; +use BestIt\LicenseCheck\LicenseLoader\Result\Result; /** * License loader for node packages. @@ -13,53 +12,30 @@ * @author best it AG * @package BestIt\LicenseCheck\LicenseLoader */ -class NodeLicenseLoader implements LicenseLoaderInterface +class NodeLicenseLoader extends AbstractLicenseLoader { /** - * Finder to get composer.json files. + * Pattern for node files. * - * @var Finder $finder + * @var string|null */ - private Finder $finder; + protected ?string $searchPattern = '/node_modules\/([A-Za-z0-9]|-|_)*\/package.json/'; /** - * ComposerLicenseLoader constructor. + * Parse package.json content. * - * @param Finder $finder - */ - public function __construct(Finder $finder) - { - $this->finder = $finder; - } - - /** - * Get used licenses in the specified path. + * @param mixed[] $decodedContent + * @param Result $result * - * @param string $path - * - * @return array> + * @return void */ - public function getLicenses(string $path): array + protected function parseFile(array $decodedContent, Result $result): void { - $result = []; - - foreach ($this->finder->path('/node_modules\/([A-Za-z0-9]|-|_)*\/package.json/')->in($path) as $file) { - $filePath = $file->getPathname(); - $content = $file->getContents(); - if (!is_string($content)) { - throw new LicenseLoaderException(sprintf('Cannot read content of file %s', $filePath)); - } - - $decodedContent = json_decode($content, true); - if (!is_array($decodedContent)) { - throw new LicenseLoaderException(sprintf('Cannot decode content of file %s', $filePath)); - } - - // ToDo: Check json structure. - $result[$decodedContent['name']] = isset($decodedContent['license']) ? [$decodedContent['license']] : []; - } - - return $result; + // ToDo: Check json structure. + $result->add( + (string) $decodedContent['name'], + isset($decodedContent['license']) ? [$decodedContent['license']] : [], + ); } /** diff --git a/src/LicenseLoader/Result/Result.php b/src/LicenseLoader/Result/Result.php new file mode 100644 index 0000000..a56d921 --- /dev/null +++ b/src/LicenseLoader/Result/Result.php @@ -0,0 +1,49 @@ + + * @package BestIt\LicenseCheck\LicenseLoader\Result + */ +class Result +{ + /** + * Internal data storage. + * + * @var array $data + */ + private array $data = []; + + /** + * Add licenses to result set. + * + * @param string $package Name of package. + * @param string[] $licenses Array of package licenses. + * + * @return $this + */ + public function add(string $package, array $licenses): self + { + $this->data[$package] = $licenses; + + return $this; + } + + /** + * Transform object to array. + * + * @return array + */ + public function toArray(): array + { + return $this->data; + } +} diff --git a/tests/Integration/IntegrationTest.php b/tests/Integration/IntegrationTest.php index 97f5eee..f7358c5 100644 --- a/tests/Integration/IntegrationTest.php +++ b/tests/Integration/IntegrationTest.php @@ -25,31 +25,43 @@ public function getTestDataProvider(): array [ __DIR__ . '/../fixtures/configuration/config1.yml', __DIR__ . '/../fixtures/composer/fixture1/', + null, false, ], [ __DIR__ . '/../fixtures/configuration/config2.yml', __DIR__ . '/../fixtures/composer/fixture1/', + null, true, ], [ __DIR__ . '/../fixtures/configuration/config3.yml', __DIR__ . '/../fixtures/node/fixture1/', + null, true, ], [ __DIR__ . '/../fixtures/configuration/config4.yml', __DIR__ . '/../fixtures/node/fixture1/', + null, true, ], [ __DIR__ . '/../fixtures/configuration/config3.yml', __DIR__ . '/../fixtures/node/fixture2/', + null, false, ], [ - __DIR__ . '/../fixtures/configuration/config4.yml', - __DIR__ . '/../fixtures/node/fixture2/', + __DIR__ . '/../fixtures/configuration/config1.yml', + __DIR__ . '/../fixtures/composer/fixture3/', + null, + false, + ], + [ + __DIR__ . '/../fixtures/configuration/config1.yml', + __DIR__ . '/../fixtures/composer/fixture3/', + 0, true, ], ]; @@ -62,17 +74,27 @@ public function getTestDataProvider(): array * * @param string $configuration * @param string $directory + * @param int|null $depth * @param bool $valid * * @return void */ - public function test(string $configuration, string $directory, bool $valid): void + public function test(string $configuration, string $directory, ?int $depth, bool $valid): void { - $command = sprintf( - __DIR__ . '/../../bin/license-check %s --configuration %s', - $directory, - $configuration, - ); + if ($depth === null) { + $command = sprintf( + __DIR__ . '/../../bin/license-check %s --configuration %s', + $directory, + $configuration, + ); + } else { + $command = sprintf( + __DIR__ . '/../../bin/license-check %s --configuration %s --depth %d', + $directory, + $configuration, + $depth, + ); + } $output = null; $resultCode = null; diff --git a/tests/Unit/CheckerTest.php b/tests/Unit/CheckerTest.php index 9f2870c..e8a8149 100644 --- a/tests/Unit/CheckerTest.php +++ b/tests/Unit/CheckerTest.php @@ -61,6 +61,7 @@ protected function setUp(): void */ public function testValidate(): void { + $depth = random_int(0, 100); $path = '/test-directory'; $this @@ -71,7 +72,7 @@ public function testValidate(): void $this ->loader1 ->method('getLicenses') - ->with($path) + ->with($path, $depth) ->willReturn([ 'vendorA/package1' => [ 'MIT', @@ -95,7 +96,7 @@ public function testValidate(): void $this ->loader2 ->method('getLicenses') - ->with($path) + ->with($path, $depth) ->willReturn([ 'vendorD/package1' => [ 'MIT', @@ -110,7 +111,7 @@ public function testValidate(): void $configuration = (new ConfigurationLoader())->load(__DIR__ . '/../fixtures/configuration/config1.yml'); - $result = $this->fixture->validate($configuration, $path); + $result = $this->fixture->validate($configuration, $path, $depth); self::assertEquals( [ diff --git a/tests/Unit/Command/LicenseCheckCommandTest.php b/tests/Unit/Command/LicenseCheckCommandTest.php index 875257b..aa01450 100644 --- a/tests/Unit/Command/LicenseCheckCommandTest.php +++ b/tests/Unit/Command/LicenseCheckCommandTest.php @@ -57,6 +57,7 @@ public function getExecutionDataProvider(): array getcwd(), getcwd() . '/license-check.yml', [], + null, ], // Actual working directory. Errors not ignores. No custom config. Result has violations. [ @@ -65,6 +66,7 @@ public function getExecutionDataProvider(): array getcwd(), getcwd() . '/license-check.yml', ['VIOLATION'], + null, ], // Actual working directory. Errors are ignores. No custom config. Result has violations. [ @@ -75,6 +77,7 @@ public function getExecutionDataProvider(): array getcwd(), getcwd() . '/license-check.yml', ['VIOLATION'], + null, ], // Custom working directory. Errors not ignores. No custom config. Result has no violations. [ @@ -85,6 +88,7 @@ public function getExecutionDataProvider(): array '/test-directory', '/test-directory/license-check.yml', [], + null, ], // Custom working directory. Errors not ignores. No custom config. Result has violations. [ @@ -95,6 +99,7 @@ public function getExecutionDataProvider(): array '/test-directory', '/test-directory/license-check.yml', ['VIOLATION'], + null, ], // Custom working directory. Errors are ignores. No custom config. Result has violations. [ @@ -106,6 +111,7 @@ public function getExecutionDataProvider(): array '/test-directory', '/test-directory/license-check.yml', ['VIOLATION'], + null, ], // Custom working directory. Errors not ignores. Custom config. Result has no violations. [ @@ -117,6 +123,7 @@ public function getExecutionDataProvider(): array '/test-directory', '/testconfig.yml', [], + null, ], // Custom working directory. Errors not ignores. No custom config. Result has violations. [ @@ -128,6 +135,7 @@ public function getExecutionDataProvider(): array '/test-directory', '/testconfig.yml', ['VIOLATION'], + null, ], // Custom working directory. Errors are ignores. No custom config. Result has violations. [ @@ -140,6 +148,21 @@ public function getExecutionDataProvider(): array '/test-directory', '/testconfig.yml', ['VIOLATION'], + null, + ], + // Custom working directory. Errors are ignores. No custom config. Result has violations. Optional depth + [ + [ + 'directory' => '/test-directory', + '--ignore-errors' => true, + '--configuration' => '/testconfig.yml', + '--depth' => '10', + ], + 0, + '/test-directory', + '/testconfig.yml', + ['VIOLATION'], + 10, ], ]; } @@ -175,6 +198,9 @@ public function testDefinition(): void self::assertTrue($this->fixture->getDefinition()->hasOption('ignore-errors')); self::assertFalse($this->fixture->getDefinition()->getOption('ignore-errors')->isValueOptional()); self::assertFalse($this->fixture->getDefinition()->getOption('ignore-errors')->isValueRequired()); + + self::assertTrue($this->fixture->getDefinition()->hasOption('depth')); + self::assertFalse($this->fixture->getDefinition()->getOption('depth')->isValueOptional()); } /** @@ -187,6 +213,7 @@ public function testDefinition(): void * @param string $directory * @param string $configPath * @param string[] $violations + * @param int|null $depth * * @return void */ @@ -196,6 +223,7 @@ public function testExecution( string $directory, string $configPath, array $violations, + ?int $depth, ): void { $this ->configurationLoader @@ -212,7 +240,7 @@ public function testExecution( $this ->checker ->method('validate') - ->with($configuration, $directory) + ->with($configuration, $directory, $depth) ->willReturn($resultSet); $commandTester = new CommandTester($this->fixture); diff --git a/tests/Unit/LicenseLoader/ComposerLicenseLoaderTest.php b/tests/Unit/LicenseLoader/ComposerLicenseLoaderTest.php index 2db7d98..17af223 100644 --- a/tests/Unit/LicenseLoader/ComposerLicenseLoaderTest.php +++ b/tests/Unit/LicenseLoader/ComposerLicenseLoaderTest.php @@ -53,7 +53,7 @@ public function testGetLicenses(): void { $this ->finder - ->method('name') + ->method('path') ->with('composer.lock') ->willReturnSelf(); @@ -63,6 +63,12 @@ public function testGetLicenses(): void ->with($path = '/directory') ->willReturnSelf(); + $this + ->finder + ->method('depth') + ->with($depth = random_int(0, 100)) + ->willReturnSelf(); + $iterator = new ArrayIterator([ $file1 = $this->createMock(SplFileInfo::class), $file2 = $this->createMock(SplFileInfo::class), @@ -91,7 +97,7 @@ public function testGetLicenses(): void 'vendorC/package2' => ['Apache-2.0'], 'vendorD/package1' => ['AGPL-3.0-only'], ], - $this->fixture->getLicenses($path), + $this->fixture->getLicenses($path, $depth), ); } @@ -104,7 +110,7 @@ public function testGetLicensesWithEmptyFile(): void { $this ->finder - ->method('name') + ->method('path') ->with('composer.lock') ->willReturnSelf(); @@ -144,7 +150,7 @@ public function testGetLicensesWithInaccessibleFile(): void { $this ->finder - ->method('name') + ->method('path') ->with('composer.lock') ->willReturnSelf(); diff --git a/tests/Unit/LicenseLoader/LicenseLoaderInterfaceTest.php b/tests/Unit/LicenseLoader/LicenseLoaderInterfaceTest.php index 0793df7..4c58370 100644 --- a/tests/Unit/LicenseLoader/LicenseLoaderInterfaceTest.php +++ b/tests/Unit/LicenseLoader/LicenseLoaderInterfaceTest.php @@ -42,11 +42,18 @@ public function testDefinition(): void $reflection = new ReflectionObject($this->fixture); self::assertTrue($reflection->hasMethod('getLicenses')); + self::assertArrayHasKey('0', $reflection->getMethod('getLicenses')->getParameters()); self::assertEquals('path', $reflection->getMethod('getLicenses')->getParameters()[0]->getName()); self::assertEquals('string', (string) $reflection->getMethod('getLicenses')->getParameters()[0]->getType()); self::assertEquals('array', (string) $reflection->getMethod('getLicenses')->getReturnType()); + self::assertArrayHasKey('1', $reflection->getMethod('getLicenses')->getParameters()); + self::assertEquals('depth', $reflection->getMethod('getLicenses')->getParameters()[1]->getName()); + self::assertEquals('?int', (string) $reflection->getMethod('getLicenses')->getParameters()[1]->getType()); + + self::assertEquals('array', (string) $reflection->getMethod('getLicenses')->getReturnType()); + self::assertTrue($reflection->hasMethod('getName')); self::assertEquals('string', (string) $reflection->getMethod('getName')->getReturnType()); } diff --git a/tests/Unit/LicenseLoader/NodeLicenseLoaderTest.php b/tests/Unit/LicenseLoader/NodeLicenseLoaderTest.php index 7f0b0f3..59daa23 100644 --- a/tests/Unit/LicenseLoader/NodeLicenseLoaderTest.php +++ b/tests/Unit/LicenseLoader/NodeLicenseLoaderTest.php @@ -63,6 +63,12 @@ public function testGetLicenses(): void ->with($path = '/directory') ->willReturnSelf(); + $this + ->finder + ->method('depth') + ->with($depth = random_int(0, 100)) + ->willReturnSelf(); + $iterator = new ArrayIterator([ $file1 = $this->createMock(SplFileInfo::class), $file2 = $this->createMock(SplFileInfo::class), @@ -92,7 +98,7 @@ public function testGetLicenses(): void 'b' => ['MIT'], 'c' => ['BSD'], ], - $this->fixture->getLicenses($path), + $this->fixture->getLicenses($path, $depth), ); } diff --git a/tests/fixtures/composer/fixture3/composer.lock b/tests/fixtures/composer/fixture3/composer.lock new file mode 100644 index 0000000..0f9701e --- /dev/null +++ b/tests/fixtures/composer/fixture3/composer.lock @@ -0,0 +1,10 @@ +{ + "packages": [ + { + "name": "vendorA/package1", + "license": [ + "MIT" + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/composer/fixture3/sub/composer.lock b/tests/fixtures/composer/fixture3/sub/composer.lock new file mode 100644 index 0000000..31b0099 --- /dev/null +++ b/tests/fixtures/composer/fixture3/sub/composer.lock @@ -0,0 +1,10 @@ +{ + "packages": [ + { + "name": "vendorA/package2", + "license": [ + "GPL-3.0-or-later" + ] + } + ] +} \ No newline at end of file