Skip to content

Commit 918a22f

Browse files
committed
Add rule that enforces PHPUnit naming conventions
1 parent 4b6ad7f commit 918a22f

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

Diff for: src/Rules/PHPUnit/ClassNamingRule.php

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Rules\RuleErrorBuilder;
9+
use function array_filter;
10+
use function sprintf;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Rules\IdentifierRuleError;
13+
14+
/**
15+
* @implements Rule<Node\Stmt\Class_>
16+
*/
17+
class ClassNamingRule implements Rule
18+
{
19+
private ReflectionProvider $reflectionProvider;
20+
21+
public function __construct(ReflectionProvider $reflectionProvider)
22+
{
23+
$this->reflectionProvider = $reflectionProvider;
24+
}
25+
26+
public function getNodeType(): string
27+
{
28+
return Node\Stmt\Class_::class;
29+
}
30+
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
if (!isset($node->namespacedName)) {
34+
return [];
35+
}
36+
37+
$className = $node->namespacedName->name;
38+
$class = $this->reflectionProvider->getClass($className);
39+
40+
if (!$class->isSubclassOf(\PHPUnit\Framework\TestCase::class)) {
41+
return [];
42+
}
43+
44+
$errors = [];
45+
46+
if ($class->isAbstract()) {
47+
$this->requireSuffix(
48+
$errors,
49+
$className,
50+
'TestCase',
51+
'Abstract test case class, \'%s\', should be named ending in \'%s\'.',
52+
);
53+
} else {
54+
$this->requireSuffix(
55+
$errors,
56+
$className,
57+
'Test',
58+
'Concrete test class, \'%s\', should be named ending in \'%s\'.',
59+
);
60+
61+
if (!$class->isFinal()) {
62+
$errors[] = RuleErrorBuilder::message(sprintf(
63+
'Concrete test class, \'%s\', should be declared final.',
64+
$className,
65+
))->identifier('phpunit.naming')->build();
66+
}
67+
}
68+
69+
return $errors;
70+
}
71+
72+
/**
73+
* @param list<\PHPStan\Rules\IdentifierRuleError> $errors
74+
*/
75+
private function requireSuffix(array &$errors, string $className, string $suffix, string $messageFormat): void
76+
{
77+
if (!\str_ends_with($className, $suffix)) { // @todo Case sensitivity??
78+
$errors[] = RuleErrorBuilder::message(sprintf(
79+
$messageFormat,
80+
$className,
81+
$suffix,
82+
))->identifier('phpunit.naming')->build();
83+
}
84+
}
85+
86+
}

Diff for: tests/Rules/PHPUnit/ClassNamingRuleTest.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPStan\Reflection\ReflectionProvider;
8+
9+
/**
10+
* @extends RuleTestCase<ClassNamingRule>
11+
*/
12+
class ClassNamingRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new ClassNamingRule(self::getContainer()->getByType(ReflectionProvider::class));
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/class-naming.php'], [
23+
['Concrete test class, \'ExampleTestCase\\IncorrectlyNamedTst\', should be named ending in \'Test\'.', 5],
24+
['Abstract test case class, \'ExampleTestCase\\IncorrectlyNamedTestCse\', should be named ending in \'TestCase\'.', 10],
25+
['Concrete test class, \'ExampleTestCase\\NotFinalTest\', should be declared final.', 15],
26+
['Concrete test class, \'ExampleTestCase\\NotFinalOrNamedCorrectly\', should be named ending in \'Test\'.', 20],
27+
['Concrete test class, \'ExampleTestCase\\NotFinalOrNamedCorrectly\', should be declared final.', 20],
28+
]);
29+
}
30+
31+
/**
32+
* @return string[]
33+
*/
34+
public static function getAdditionalConfigFiles(): array
35+
{
36+
return [
37+
__DIR__ . '/../../../extension.neon',
38+
];
39+
}
40+
41+
}

Diff for: tests/Rules/PHPUnit/data/class-naming.php

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ExampleTestCase;
4+
5+
final class IncorrectlyNamedTst extends \PHPUnit\Framework\TestCase
6+
{
7+
8+
}
9+
10+
abstract class IncorrectlyNamedTestCse extends \PHPUnit\Framework\TestCase
11+
{
12+
13+
}
14+
15+
class NotFinalTest extends \PHPUnit\Framework\TestCase
16+
{
17+
18+
}
19+
20+
class NotFinalOrNamedCorrectly extends \PHPUnit\Framework\TestCase
21+
{
22+
23+
}
24+
25+
new class() extends \PHPUnit\Framework\TestCase {
26+
27+
};
28+
29+
final class CorrectlyNamedAndFinalTest extends \PHPUnit\Framework\TestCase
30+
{
31+
32+
}
33+
34+
abstract class CorrectlyNamedTestCase extends \PHPUnit\Framework\TestCase
35+
{
36+
37+
}

0 commit comments

Comments
 (0)