From 2825633f0a6cffab5a62d56c74864a668a250147 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Wed, 16 Jul 2025 23:09:27 -0500 Subject: [PATCH] feat: add rule to prevent comments after attributes --- .../PhpDoc/NoCommentsAfterAttributesRule.php | 84 +++++++++++++++++++ .../NoCommentsAfterAttributesRuleTest.php | 36 ++++++++ .../data/no-comments-after-attributes.php | 83 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 src/Rules/PhpDoc/NoCommentsAfterAttributesRule.php create mode 100644 tests/PHPStan/Rules/PhpDoc/NoCommentsAfterAttributesRuleTest.php create mode 100644 tests/PHPStan/Rules/PhpDoc/data/no-comments-after-attributes.php diff --git a/src/Rules/PhpDoc/NoCommentsAfterAttributesRule.php b/src/Rules/PhpDoc/NoCommentsAfterAttributesRule.php new file mode 100644 index 0000000000..61a3f4636f --- /dev/null +++ b/src/Rules/PhpDoc/NoCommentsAfterAttributesRule.php @@ -0,0 +1,84 @@ + */ +#[RegisteredRule(level: 0)] +final class NoCommentsAfterAttributesRule implements Rule +{ + + #[Override] + public function getNodeType(): string + { + return Node::class; + } + + #[Override] + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof VirtualNode) { + return []; + } + + if ($node->getDocComment() !== null) { + return []; + } + + if (! property_exists($node, 'attrGroups')) { + return []; + } + + $attrGroups = $node->attrGroups; + + if ($attrGroups === []) { + return []; + } + + $attrGroupEndLine = max(array_map(static fn (AttributeGroup $g) => $g->getEndLine(), $attrGroups)); + + if (property_exists($node, 'name')) { + $name = $node->name; + assert($name instanceof Node); + $startLine = $name->getStartLine(); + } elseif ($node instanceof ClassConst) { + $startLine = min(array_map(static fn ($c) => $c->getStartLine(), $node->consts)); + } elseif ($node instanceof Property) { + $startLine = min(array_map(static fn ($c) => $c->getStartLine(), $node->props)); + } elseif ($node instanceof Param) { + $startLine = $node->var->getStartLine(); + } else { + throw new LogicException('Unexpected node type: ' . $node::class); + } + + if ($startLine - $attrGroupEndLine <= 1) { + return []; + } + + return [ + RuleErrorBuilder::message('No comments after attributes.') + ->identifier('node.noCommentsAfterAttributes') + ->line($attrGroupEndLine + 1) + ->nonIgnorable() + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/NoCommentsAfterAttributesRuleTest.php b/tests/PHPStan/Rules/PhpDoc/NoCommentsAfterAttributesRuleTest.php new file mode 100644 index 0000000000..725da3cee0 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/NoCommentsAfterAttributesRuleTest.php @@ -0,0 +1,36 @@ + */ +#[CoversNothing] +class NoCommentsAfterAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new NoCommentsAfterAttributesRule(); + } + + public function testRule(): void + { + $message = 'No comments after attributes.'; + + $this->analyse([__DIR__ . '/data/no-comments-after-attributes.php'], [ + [$message, 37], + [$message, 41], + [$message, 45], + [$message, 50], + [$message, 53], + [$message, 58], + [$message, 62], + [$message, 71], + [$message, 81], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/no-comments-after-attributes.php b/tests/PHPStan/Rules/PhpDoc/data/no-comments-after-attributes.php new file mode 100644 index 0000000000..63a4abc540 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/no-comments-after-attributes.php @@ -0,0 +1,83 @@ + */ + #[Good] + private array $foo = []; + + public function __construct( + /** @var array */ + #[Good] + private array $bar, + /** @var array */ + #[Good] + array $baz, + #[Good] string $qux, + ) {} + + // This is a comment. + #[Good] + public function foo(): void {} + + /** This is a doc comment. */ + #[Good] + public function bar(): void {} +} + +#[Bad] +/** This is a doc comment. */ +class Bad +{ + #[Bad] + /** @var class-string */ + public const BAR = 'Bar'; + + #[Bad] + /** @var array */ + private array $foo = []; + + public function __construct( + #[Bad] + /** @var array */ + private array $bar, + #[Bad] + /** @var array */ + array $baz, + ) {} + + #[Bad] + // This is a comment after attributes. + public function foo(): void {} + + #[Bad] + /** This is a doc comment after attributes. */ + public function bar(): void {} +} + +/** This is a doc comment. */ +#[Good] +function foo(): void {} + +#[Bad] +/** This is a doc comment. */ +function bar(): void {} + +enum Foo +{ + /** This is a doc comment before attributes. */ + #[Good] + case Foo; + + #[Bad] + /** This is a doc comment after attributes. */ + case Baz; +}