Skip to content

Commit 9b88cef

Browse files
authored
Add rule that checks for invalid and unrecognized annotations
1 parent 68017cc commit 9b88cef

9 files changed

+467
-0
lines changed

extension.neon

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ services:
5353
- phpstan.broker.dynamicMethodReturnTypeExtension
5454
-
5555
class: PHPStan\Rules\PHPUnit\CoversHelper
56+
-
57+
class: PHPStan\Rules\PHPUnit\AnnotationHelper
5658

5759
conditionalTags:
5860
PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension:

rules.neon

+6
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ rules:
88
services:
99
- class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule
1010
- class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule
11+
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule
12+
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule
1113

1214
conditionalTags:
1315
PHPStan\Rules\PHPUnit\ClassCoversExistsRule:
1416
phpstan.rules.rule: %featureToggles.bleedingEdge%
1517
PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule:
1618
phpstan.rules.rule: %featureToggles.bleedingEdge%
19+
PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule:
20+
phpstan.rules.rule: %featureToggles.bleedingEdge%
21+
PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule:
22+
phpstan.rules.rule: %featureToggles.bleedingEdge%
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PhpParser\Comment\Doc;
6+
use PHPStan\Rules\RuleError;
7+
use PHPStan\Rules\RuleErrorBuilder;
8+
use function array_key_exists;
9+
use function in_array;
10+
use function preg_match;
11+
use function preg_split;
12+
13+
class AnnotationHelper
14+
{
15+
16+
private const ANNOTATIONS_WITH_PARAMS = [
17+
'backupGlobals',
18+
'backupStaticAttributes',
19+
'covers',
20+
'coversDefaultClass',
21+
'dataProvider',
22+
'depends',
23+
'group',
24+
'preserveGlobalState',
25+
'requires',
26+
'testDox',
27+
'testWith',
28+
'ticket',
29+
'uses',
30+
];
31+
32+
/**
33+
* @return RuleError[] errors
34+
*/
35+
public function processDocComment(Doc $docComment): array
36+
{
37+
$errors = [];
38+
$docCommentLines = preg_split("/((\r?\n)|(\r\n?))/", $docComment->getText());
39+
if ($docCommentLines === false) {
40+
return [];
41+
}
42+
43+
foreach ($docCommentLines as $docCommentLine) {
44+
// These annotations can't be retrieved using the getResolvedPhpDoc method on the FileTypeMapper as they are not present when they are invalid
45+
$annotation = preg_match('/(?<annotation>@(?<property>[a-zA-Z]+)(?<whitespace>\s*)(?<value>.*))/', $docCommentLine, $matches);
46+
if ($annotation === false) {
47+
continue; // Line without annotation
48+
}
49+
50+
if (array_key_exists('property', $matches) === false || array_key_exists('whitespace', $matches) === false || array_key_exists('annotation', $matches) === false) {
51+
continue;
52+
}
53+
54+
if (!in_array($matches['property'], self::ANNOTATIONS_WITH_PARAMS, true) || $matches['whitespace'] !== '') {
55+
continue;
56+
}
57+
58+
$errors[] = RuleErrorBuilder::message(
59+
'Annotation "' . $matches['annotation'] . '" is invalid, "@' . $matches['property'] . '" should be followed by a space and a value.'
60+
)->build();
61+
}
62+
63+
return $errors;
64+
}
65+
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Node\InClassNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* @implements Rule<InClassNode>
13+
*/
14+
class NoMissingSpaceInClassAnnotationRule implements Rule
15+
{
16+
17+
/**
18+
* Covers helper.
19+
*
20+
* @var AnnotationHelper
21+
*/
22+
private $annotationHelper;
23+
24+
public function __construct(AnnotationHelper $annotationHelper)
25+
{
26+
$this->annotationHelper = $annotationHelper;
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return InClassNode::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
$classReflection = $scope->getClassReflection();
37+
if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) {
38+
return [];
39+
}
40+
41+
$docComment = $node->getDocComment();
42+
if ($docComment === null) {
43+
return [];
44+
}
45+
46+
return $this->annotationHelper->processDocComment($docComment);
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Node\InClassMethodNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* @implements Rule<InClassMethodNode>
13+
*/
14+
class NoMissingSpaceInMethodAnnotationRule implements Rule
15+
{
16+
17+
/**
18+
* Covers helper.
19+
*
20+
* @var AnnotationHelper
21+
*/
22+
private $annotationHelper;
23+
24+
public function __construct(AnnotationHelper $annotationHelper)
25+
{
26+
$this->annotationHelper = $annotationHelper;
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return InClassMethodNode::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
$classReflection = $scope->getClassReflection();
37+
if ($classReflection === null || $classReflection->isSubclassOf(TestCase::class) === false) {
38+
return [];
39+
}
40+
41+
$docComment = $node->getDocComment();
42+
if ($docComment === null) {
43+
return [];
44+
}
45+
46+
return $this->annotationHelper->processDocComment($docComment);
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<NoMissingSpaceInClassAnnotationRule>
10+
*/
11+
class NoMissingSpaceInClassAnnotationRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new NoMissingSpaceInClassAnnotationRule(new AnnotationHelper());
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/InvalidClassCoversAnnotation.php'], [
22+
[
23+
'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.',
24+
36,
25+
],
26+
[
27+
'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.',
28+
36,
29+
],
30+
[
31+
'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.',
32+
36,
33+
],
34+
[
35+
'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.',
36+
36,
37+
],
38+
[
39+
'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.',
40+
36,
41+
],
42+
[
43+
'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.',
44+
36,
45+
],
46+
[
47+
'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.',
48+
36,
49+
],
50+
[
51+
'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.',
52+
36,
53+
],
54+
[
55+
'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.',
56+
36,
57+
],
58+
[
59+
'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.',
60+
36,
61+
],
62+
[
63+
'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.',
64+
36,
65+
],
66+
[
67+
'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.',
68+
36,
69+
],
70+
[
71+
'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.',
72+
36,
73+
],
74+
]);
75+
}
76+
77+
/**
78+
* @return string[]
79+
*/
80+
public static function getAdditionalConfigFiles(): array
81+
{
82+
return [
83+
__DIR__ . '/../../../extension.neon',
84+
];
85+
}
86+
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<NoMissingSpaceInMethodAnnotationRule>
10+
*/
11+
class NoMissingSpaceInMethodAnnotationRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new NoMissingSpaceInMethodAnnotationRule(new AnnotationHelper());
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/InvalidMethodCoversAnnotation.php'], [
22+
[
23+
'Annotation "@backupGlobals" is invalid, "@backupGlobals" should be followed by a space and a value.',
24+
12,
25+
],
26+
[
27+
'Annotation "@backupStaticAttributes" is invalid, "@backupStaticAttributes" should be followed by a space and a value.',
28+
19,
29+
],
30+
[
31+
'Annotation "@covers\Dummy\Foo::assertSame" is invalid, "@covers" should be followed by a space and a value.',
32+
27,
33+
],
34+
[
35+
'Annotation "@covers::assertSame" is invalid, "@covers" should be followed by a space and a value.',
36+
27,
37+
],
38+
[
39+
'Annotation "@coversDefaultClass\Dummy\Foo" is invalid, "@coversDefaultClass" should be followed by a space and a value.',
40+
33,
41+
],
42+
[
43+
'Annotation "@dataProvider" is invalid, "@dataProvider" should be followed by a space and a value.',
44+
39,
45+
],
46+
[
47+
'Annotation "@depends" is invalid, "@depends" should be followed by a space and a value.',
48+
45,
49+
],
50+
[
51+
'Annotation "@preserveGlobalState" is invalid, "@preserveGlobalState" should be followed by a space and a value.',
52+
52,
53+
],
54+
[
55+
'Annotation "@requires" is invalid, "@requires" should be followed by a space and a value.',
56+
58,
57+
],
58+
[
59+
'Annotation "@testDox" is invalid, "@testDox" should be followed by a space and a value.',
60+
64,
61+
],
62+
[
63+
'Annotation "@testWith" is invalid, "@testWith" should be followed by a space and a value.',
64+
70,
65+
],
66+
[
67+
'Annotation "@ticket" is invalid, "@ticket" should be followed by a space and a value.',
68+
76,
69+
],
70+
[
71+
'Annotation "@uses" is invalid, "@uses" should be followed by a space and a value.',
72+
82,
73+
],
74+
]);
75+
}
76+
77+
/**
78+
* @return string[]
79+
*/
80+
public static function getAdditionalConfigFiles(): array
81+
{
82+
return [
83+
__DIR__ . '/../../../extension.neon',
84+
];
85+
}
86+
87+
}

0 commit comments

Comments
 (0)