Skip to content

Commit b792b97

Browse files
committed
feat: add rule to prevent comments after attributes
1 parent ae7fd77 commit b792b97

File tree

3 files changed

+203
-0
lines changed

3 files changed

+203
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use LogicException;
6+
use Override;
7+
use PhpParser\Node;
8+
use PhpParser\Node\AttributeGroup;
9+
use PhpParser\Node\Param;
10+
use PhpParser\Node\Stmt\ClassConst;
11+
use PhpParser\Node\Stmt\Property;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\DependencyInjection\RegisteredRule;
14+
use PHPStan\Node\VirtualNode;
15+
use PHPStan\Rules\Rule;
16+
use PHPStan\Rules\RuleErrorBuilder;
17+
use function array_map;
18+
use function assert;
19+
use function max;
20+
use function min;
21+
use function property_exists;
22+
23+
/** @implements Rule<Node> */
24+
#[RegisteredRule(level: 0)]
25+
final class NoCommentsAfterAttributesRule implements Rule
26+
{
27+
28+
#[Override]
29+
public function getNodeType(): string
30+
{
31+
return Node::class;
32+
}
33+
34+
#[Override]
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if ($node instanceof VirtualNode) {
38+
return [];
39+
}
40+
41+
if ($node->getDocComment() !== null) {
42+
return [];
43+
}
44+
45+
if (! property_exists($node, 'attrGroups')) {
46+
return [];
47+
}
48+
49+
$attrGroups = $node->attrGroups;
50+
51+
if ($attrGroups === []) {
52+
return [];
53+
}
54+
55+
$attrGroupEndLine = max(array_map(static fn (AttributeGroup $g) => $g->getEndLine(), $attrGroups));
56+
57+
if (property_exists($node, 'name')) {
58+
$name = $node->name;
59+
assert($name instanceof Node);
60+
$startLine = $name->getStartLine();
61+
} elseif ($node instanceof ClassConst) {
62+
$startLine = min(array_map(static fn ($c) => $c->getStartLine(), $node->consts));
63+
} elseif ($node instanceof Property) {
64+
$startLine = min(array_map(static fn ($c) => $c->getStartLine(), $node->props));
65+
} elseif ($node instanceof Param) {
66+
$startLine = $node->var->getStartLine();
67+
} else {
68+
throw new LogicException('Unexpected node type: ' . $node::class);
69+
}
70+
71+
if ($startLine - $attrGroupEndLine <= 1) {
72+
return [];
73+
}
74+
75+
return [
76+
RuleErrorBuilder::message('No comments after attributes.')
77+
->identifier('node.noCommentsAfterAttributes')
78+
->line($attrGroupEndLine + 1)
79+
->nonIgnorable()
80+
->build(),
81+
];
82+
}
83+
84+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPUnit\Framework\Attributes\CoversNothing;
8+
9+
/** @extends RuleTestCase<NoCommentsAfterAttributesRule> */
10+
#[CoversNothing]
11+
class NoCommentsAfterAttributesRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new NoCommentsAfterAttributesRule();
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$message = 'No comments after attributes.';
22+
23+
$this->analyse([__DIR__ . '/data/no-comments-after-attributes.php'], [
24+
[$message, 37],
25+
[$message, 41],
26+
[$message, 45],
27+
[$message, 50],
28+
[$message, 53],
29+
[$message, 58],
30+
[$message, 62],
31+
[$message, 71],
32+
[$message, 81],
33+
]);
34+
}
35+
36+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace NoCommentsAfterAttributes;
4+
5+
/** This is a doc comment. */
6+
#[Good]
7+
class Good
8+
{
9+
/** @var array<int, string> */
10+
#[Good]
11+
public const array FOO = [];
12+
13+
/** @var array<int, string> */
14+
#[Good]
15+
private array $foo = [];
16+
17+
public function __construct(
18+
/** @var array<int, string> */
19+
#[Good]
20+
private array $bar,
21+
/** @var array<int, string> */
22+
#[Good]
23+
array $baz,
24+
#[Good] string $qux,
25+
) {}
26+
27+
// This is a comment.
28+
#[Good]
29+
public function foo(): void {}
30+
31+
/** This is a doc comment. */
32+
#[Good]
33+
public function bar(): void {}
34+
}
35+
36+
#[Bad]
37+
/** This is a doc comment. */
38+
class Bad
39+
{
40+
#[Bad]
41+
/** @var array<int, string> */
42+
public const array BAR = [];
43+
44+
#[Bad]
45+
/** @var array<int, string> */
46+
private array $foo = [];
47+
48+
public function __construct(
49+
#[Bad]
50+
/** @var array<int, string> */
51+
private array $bar,
52+
#[Bad]
53+
/** @var array<int, string> */
54+
array $baz,
55+
) {}
56+
57+
#[Bad]
58+
// This is a comment after attributes.
59+
public function foo(): void {}
60+
61+
#[Bad]
62+
/** This is a doc comment after attributes. */
63+
public function bar(): void {}
64+
}
65+
66+
/** This is a doc comment. */
67+
#[Good]
68+
function foo(): void {}
69+
70+
#[Bad]
71+
/** This is a doc comment. */
72+
function bar(): void {}
73+
74+
enum Foo
75+
{
76+
/** This is a doc comment before attributes. */
77+
#[Good]
78+
case Foo;
79+
80+
#[Bad]
81+
/** This is a doc comment after attributes. */
82+
case Baz;
83+
}

0 commit comments

Comments
 (0)