Skip to content

Commit a915171

Browse files
committed
Lint SPDX conventions
The `reuse lint` command only checks for REUSE compliance, which will accept all sorts of SPDX headers. In this project, however, we have also other conventions. For example, we require all PHP and docs files from the project to have a specific license (not just any license) and also a specific File Copyright Text (not just any copyright). This commit introduces a command to solve this problem, validating the headers more thoroughly. The introduced command also does some dogfooding, using validators from the library itself to perform some of its tasks, namely: Call, Each, Contains, Templated and Named, showcasing potential different use cases for the project.
1 parent a107b8c commit a915171

File tree

4 files changed

+116
-1
lines changed

4 files changed

+116
-1
lines changed

.github/workflows/reuse.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,20 @@ jobs:
2121
with:
2222
python-version: '3.x'
2323

24+
- name: Install PHP
25+
uses: shivammathur/setup-php@v2
26+
with:
27+
php-version: 8.5
28+
coverage: none
29+
30+
- name: Install dependencies
31+
run: composer install --prefer-dist
32+
2433
- name: Install REUSE tool
2534
run: pip install reuse
2635

2736
- name: Run REUSE check
28-
run: reuse lint
37+
run: reuse lint
38+
39+
- name: Run SPDX conventions check
40+
run: bin/console spdx:lint

bin/console

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require __DIR__ . '/../vendor/autoload.php';
1313
use Respect\Dev\Commands\CreateMixinCommand;
1414
use Respect\Dev\Commands\DocsLintCommand;
1515
use Respect\Dev\Commands\SmokeTestsCheckCompleteCommand;
16+
use Respect\Dev\Commands\SpdxLintCommand;
1617
use Respect\Dev\Commands\UpdateDomainSuffixesCommand;
1718
use Respect\Dev\Commands\UpdateDomainToplevelCommand;
1819
use Respect\Dev\Commands\UpdatePostalCodesCommand;
@@ -43,6 +44,7 @@ return (static function () {
4344
$application->addCommand(new UpdateDomainToplevelCommand());
4445
$application->addCommand(new UpdatePostalCodesCommand());
4546
$application->addCommand(new SmokeTestsCheckCompleteCommand());
47+
$application->addCommand(new SpdxLintCommand());
4648

4749
return $application->run();
4850
})();

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@
8383
"phpstan": "vendor/bin/phpstan analyze",
8484
"phpunit": "vendor/bin/phpunit --testsuite=unit",
8585
"smoke-complete": "bin/console smoke-tests:check-complete",
86+
"spdx-lint": "bin/console spdx:lint",
8687
"qa": [
88+
"@spdx-lint",
8789
"@phpcs",
8890
"@phpstan",
8991
"@phpunit",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Dev\Commands;
11+
12+
use Respect\Validation\Exceptions\ValidationException;
13+
use Respect\Validation\ValidatorBuilder;
14+
use Symfony\Component\Console\Attribute\AsCommand;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Finder\Finder;
19+
use Symfony\Component\Finder\SplFileInfo;
20+
21+
use function array_filter;
22+
use function array_keys;
23+
use function array_map;
24+
use function dirname;
25+
use function explode;
26+
use function file_get_contents;
27+
use function preg_match;
28+
use function sprintf;
29+
use function trim;
30+
31+
#[AsCommand(
32+
name: 'spdx:lint',
33+
description: 'Apply SPDX linters to source and documentation files',
34+
)]
35+
final class SpdxLintCommand extends Command
36+
{
37+
public const array HEADERS = [
38+
'License-Identifier: MIT',
39+
'FileCopyrightText: (c) Respect Project Contributors',
40+
];
41+
public const array EXTENSIONS = [
42+
'php' => '/\/\*+(.*?)\*\//s',
43+
'md' => '/<!--+(.*?)-->/s',
44+
];
45+
public const array SEARCH_DIRS = [
46+
'/src',
47+
'/src-dev',
48+
'/tests',
49+
'/bin',
50+
'/docs',
51+
];
52+
53+
public static function extractFileHeader(SplFileInfo $file): string
54+
{
55+
preg_match(
56+
self::EXTENSIONS[$file->getExtension()],
57+
file_get_contents($file->getRealPath()),
58+
$matches,
59+
);
60+
61+
return $matches[1] ?? '';
62+
}
63+
64+
protected function execute(InputInterface $input, OutputInterface $output): int
65+
{
66+
$pass = true;
67+
$root = dirname(__DIR__, 2);
68+
$finder = (new Finder())->in(array_map(static fn($dir) => $root . $dir, self::SEARCH_DIRS))->files();
69+
$validator = ValidatorBuilder::call(
70+
static fn($input) => array_filter(explode("\n", trim($input))),
71+
ValidatorBuilder::templated(
72+
'Each header line of {{subject}}',
73+
ValidatorBuilder::each(ValidatorBuilder::stringType()->contains('SPDX')),
74+
),
75+
);
76+
77+
foreach (array_keys(self::EXTENSIONS) as $extension) {
78+
$finder = $finder->name('*.' . $extension);
79+
}
80+
81+
foreach (self::HEADERS as $headerLine) {
82+
$validator = $validator->contains(sprintf('SPDX-%s', $headerLine));
83+
}
84+
85+
foreach ($finder as $file) {
86+
try {
87+
ValidatorBuilder::named(
88+
sprintf('File "%s" SPDX header', $file->getRelativePathname()),
89+
$validator,
90+
)->assert(static::extractFileHeader($file));
91+
} catch (ValidationException $e) {
92+
$output->writeln($e->getFullMessage());
93+
$pass = false;
94+
}
95+
}
96+
97+
return $pass ? Command::SUCCESS : Command::FAILURE;
98+
}
99+
}

0 commit comments

Comments
 (0)