Skip to content

Commit 6b7e332

Browse files
authored
Merge pull request #17 from script-development/armorer/rule-author-contract-tests
test: add rule-author contract tests (F-6 + F-7)
2 parents 4f50136 + ada4b5b commit 6b7e332

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace ScriptDevelopment\PhpstanWarroomRules\Tests\Rules;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
use ReflectionClass;
10+
11+
/**
12+
* Locks the package's own rule-author convention: every class under `src/Rules/`
13+
* must declare a `Doctrine source:` line in its class-level docblock so the rule's
14+
* authority (ADR or war-room principle) is visible at the source.
15+
*
16+
* Doctrine source: ADR-0021 §Doctrine source in docblock.
17+
*
18+
* This test enforces *how rules are written*, not *what rules check*. It does not
19+
* contradict the package's "static-analysis library only" stance (CLAUDE.md
20+
* §"What this territory does NOT do") because PHPUnit reflection on rule classes
21+
* is rule-authoring discipline, not rule enforcement.
22+
*/
23+
final class RuleDocblockContractTest extends TestCase
24+
{
25+
#[Test]
26+
public function every_rule_class_declares_doctrine_source_in_class_docblock(): void
27+
{
28+
$ruleFiles = glob(__DIR__ . '/../../src/Rules/*.php');
29+
30+
self::assertNotEmpty($ruleFiles, 'No rule files found under src/Rules');
31+
32+
foreach ($ruleFiles as $file) {
33+
$namespace = 'ScriptDevelopment\PhpstanWarroomRules\Rules';
34+
$class = $namespace . '\\' . basename($file, '.php');
35+
36+
self::assertTrue(class_exists($class), "Rule class not autoloadable: {$class}");
37+
38+
$docblock = (new ReflectionClass($class))->getDocComment();
39+
40+
self::assertNotFalse(
41+
$docblock,
42+
"{$class} has no class-level docblock (ADR-0021 §Doctrine source in docblock).",
43+
);
44+
45+
self::assertStringContainsString(
46+
'Doctrine source:',
47+
$docblock,
48+
"{$class} class-level docblock does not name its doctrine source. ADR-0021 §Doctrine source in docblock requires every rule to cite its ADR or war-room principle.",
49+
);
50+
}
51+
}
52+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace ScriptDevelopment\PhpstanWarroomRules\Tests\Rules;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
10+
use function sprintf;
11+
12+
/**
13+
* Locks the package's own rule-author convention: every
14+
* `RuleErrorBuilder::message()->identifier(...)` call in `src/Rules/*.php` must
15+
* use a `cameLCase.cameLCase` identifier so consumers see a uniform shape across
16+
* every rule the package ships.
17+
*
18+
* Doctrine source: ADR-0021 §Identifier convention.
19+
*
20+
* Like RuleDocblockContractTest, this enforces rule-authoring discipline (a
21+
* lexical contract on identifier strings), not rule enforcement.
22+
*/
23+
final class RuleIdentifierConventionTest extends TestCase
24+
{
25+
private const string IDENTIFIER_PATTERN = '/->identifier\(\s*[\'"]([^\'"]+)[\'"]\s*\)/';
26+
27+
private const string CONVENTION_PATTERN = '/^[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*$/';
28+
29+
#[Test]
30+
public function every_rule_identifier_follows_camel_dot_camel_convention(): void
31+
{
32+
$ruleFiles = glob(__DIR__ . '/../../src/Rules/*.php');
33+
34+
self::assertNotEmpty($ruleFiles, 'No rule files found under src/Rules');
35+
36+
$allIdentifiers = [];
37+
foreach ($ruleFiles as $file) {
38+
$source = file_get_contents($file);
39+
self::assertNotFalse($source, "Could not read {$file}");
40+
41+
preg_match_all(self::IDENTIFIER_PATTERN, $source, $matches);
42+
43+
foreach ($matches[1] as $identifier) {
44+
$allIdentifiers[] = [$file, $identifier];
45+
self::assertMatchesRegularExpression(
46+
self::CONVENTION_PATTERN,
47+
$identifier,
48+
sprintf(
49+
'Identifier "%s" in %s does not follow ADR-0021 §Identifier convention (cameLCase.cameLCase).',
50+
$identifier,
51+
basename($file),
52+
),
53+
);
54+
}
55+
}
56+
57+
self::assertNotEmpty(
58+
$allIdentifiers,
59+
'No identifiers found across rule files — regex broken or rules lack ->identifier() calls.',
60+
);
61+
}
62+
}

0 commit comments

Comments
 (0)