From 65f6b8cecd2f538523aa35e42e90158bd7b41adf Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 12 Jul 2025 11:21:34 +0200 Subject: [PATCH 1/4] Add support for inheritors tag --- src/Ast/PhpDoc/InheritorsTagValueNode.php | 31 +++++++++++ src/Ast/PhpDoc/PhpDocNode.php | 11 ++++ src/Parser/PhpDocParser.php | 12 ++++ src/Printer/Printer.php | 5 ++ tests/PHPStan/Parser/PhpDocParserTest.php | 67 +++++++++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 src/Ast/PhpDoc/InheritorsTagValueNode.php diff --git a/src/Ast/PhpDoc/InheritorsTagValueNode.php b/src/Ast/PhpDoc/InheritorsTagValueNode.php new file mode 100644 index 00000000..de33e89f --- /dev/null +++ b/src/Ast/PhpDoc/InheritorsTagValueNode.php @@ -0,0 +1,31 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 6abad3d0..0d24655d 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -231,6 +231,17 @@ public function getRequireImplementsTagValues(string $tagName = '@phpstan-requir ); } + /** + * @return InheritorsTagValueNode[] + */ + public function getInheritorsTagValues(string $tagName = '@phpstan-inheritors'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static fn (PhpDocTagValueNode $value): bool => $value instanceof InheritorsTagValueNode, + ); + } + /** * @return DeprecatedTagValueNode[] */ diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 559d8fd5..8bf00488 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -403,6 +403,11 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseRequireImplementsTagValue($tokens); break; + case '@psalm-inheritors': + case '@phpstan-inheritors': + $tagValue = $this->parseInheritorsTagValue($tokens); + break; + case '@deprecated': $tagValue = $this->parseDeprecatedTagValue($tokens); break; @@ -933,6 +938,13 @@ private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpD return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description); } + private function parseInheritorsTagValue(TokenIterator $tokens): Ast\PhpDoc\InheritorsTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\InheritorsTagValueNode($type, $description); + } + private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode { $description = $this->parseOptionalDescription($tokens, false); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index af920f8d..7d0da30a 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -18,6 +18,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\InheritorsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; @@ -327,6 +328,10 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$node->description}"); } + if ($node instanceof InheritorsTagValueNode) { + $type = $this->printType($node->type); + return trim("{$type} {$node->description}"); + } if ($node instanceof ParamOutTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->parameterName} {$node->description}"); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index abe69b8e..1ee6c8c8 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -108,6 +108,7 @@ protected function setUp(): void * @dataProvider provideMixinTagsData * @dataProvider provideRequireExtendsTagsData * @dataProvider provideRequireImplementsTagsData + * @dataProvider provideInheritorsTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData @@ -2210,6 +2211,72 @@ public function provideRequireImplementsTagsData(): Iterator ]; } + public function provideInheritorsTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-inheritors Foo|Bar */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-inheritors', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo|Bar'), + '', + ), + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-inheritors Foo|Bar optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-inheritors', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo|Bar'), + 'optional description', + ), + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-inheritors Foo|Bar optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-inheritors', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo|Bar'), + 'optional description', + ), + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-inheritors */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-inheritors', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 32, + Lexer::TOKEN_IDENTIFIER, + null, + 1, + ), + ), + ), + ]), + ]; + } + public function provideDeprecatedTagsData(): Iterator { yield [ From 39ae8d3c98ca6f8b7a9e13d801554437bcfe668b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 12 Jul 2025 12:31:46 +0200 Subject: [PATCH 2/4] Fix --- tests/PHPStan/Parser/PhpDocParserTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 1ee6c8c8..058c36fd 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -25,6 +25,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\InheritorsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; @@ -2219,7 +2220,7 @@ public function provideInheritorsTagsData(): Iterator new PhpDocNode([ new PhpDocTagNode( '@phpstan-inheritors', - new RequireImplementsTagValueNode( + new InheritorsTagValueNode( new IdentifierTypeNode('Foo|Bar'), '', ), @@ -2233,7 +2234,7 @@ public function provideInheritorsTagsData(): Iterator new PhpDocNode([ new PhpDocTagNode( '@phpstan-inheritors', - new RequireImplementsTagValueNode( + new InheritorsTagValueNode( new IdentifierTypeNode('Foo|Bar'), 'optional description', ), @@ -2247,7 +2248,7 @@ public function provideInheritorsTagsData(): Iterator new PhpDocNode([ new PhpDocTagNode( '@psalm-inheritors', - new RequireImplementsTagValueNode( + new InheritorsTagValueNode( new IdentifierTypeNode('Foo|Bar'), 'optional description', ), @@ -2266,7 +2267,7 @@ public function provideInheritorsTagsData(): Iterator new ParserException( '*/', Lexer::TOKEN_CLOSE_PHPDOC, - 32, + 24, Lexer::TOKEN_IDENTIFIER, null, 1, From 3c000f21d8796a1bd0565582bb9df7c00ee2b69d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 12 Jul 2025 13:44:20 +0200 Subject: [PATCH 3/4] Rename --- src/Ast/PhpDoc/PhpDocNode.php | 6 ++--- ...agValueNode.php => SealedTagValueNode.php} | 2 +- src/Parser/PhpDocParser.php | 8 +++--- src/Printer/Printer.php | 4 +-- tests/PHPStan/Parser/PhpDocParserTest.php | 26 +++++++++---------- 5 files changed, 23 insertions(+), 23 deletions(-) rename src/Ast/PhpDoc/{InheritorsTagValueNode.php => SealedTagValueNode.php} (90%) diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 0d24655d..a3dd3e4c 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -232,13 +232,13 @@ public function getRequireImplementsTagValues(string $tagName = '@phpstan-requir } /** - * @return InheritorsTagValueNode[] + * @return SealedTagValueNode[] */ - public function getInheritorsTagValues(string $tagName = '@phpstan-inheritors'): array + public function getSealedTagValues(string $tagName = '@phpstan-sealed'): array { return array_filter( array_column($this->getTagsByName($tagName), 'value'), - static fn (PhpDocTagValueNode $value): bool => $value instanceof InheritorsTagValueNode, + static fn (PhpDocTagValueNode $value): bool => $value instanceof SealedTagValueNode, ); } diff --git a/src/Ast/PhpDoc/InheritorsTagValueNode.php b/src/Ast/PhpDoc/SealedTagValueNode.php similarity index 90% rename from src/Ast/PhpDoc/InheritorsTagValueNode.php rename to src/Ast/PhpDoc/SealedTagValueNode.php index de33e89f..230cf3e8 100644 --- a/src/Ast/PhpDoc/InheritorsTagValueNode.php +++ b/src/Ast/PhpDoc/SealedTagValueNode.php @@ -6,7 +6,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use function trim; -class InheritorsTagValueNode implements PhpDocTagValueNode +class SealedTagValueNode implements PhpDocTagValueNode { use NodeAttributes; diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 8bf00488..f36b9136 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -404,8 +404,8 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph break; case '@psalm-inheritors': - case '@phpstan-inheritors': - $tagValue = $this->parseInheritorsTagValue($tokens); + case '@phpstan-sealed': + $tagValue = $this->parseSealedTagValue($tokens); break; case '@deprecated': @@ -938,11 +938,11 @@ private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpD return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description); } - private function parseInheritorsTagValue(TokenIterator $tokens): Ast\PhpDoc\InheritorsTagValueNode + private function parseSealedTagValue(TokenIterator $tokens): Ast\PhpDoc\SealedTagValueNode { $type = $this->typeParser->parse($tokens); $description = $this->parseOptionalDescription($tokens, true); - return new Ast\PhpDoc\InheritorsTagValueNode($type, $description); + return new Ast\PhpDoc\SealedTagValueNode($type, $description); } private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 7d0da30a..54f98896 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -18,7 +18,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\InheritorsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; @@ -328,7 +328,7 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$node->description}"); } - if ($node instanceof InheritorsTagValueNode) { + if ($node instanceof SealedTagValueNode) { $type = $this->printType($node->type); return trim("{$type} {$node->description}"); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 058c36fd..c8b93986 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -25,7 +25,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\InheritorsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; @@ -109,7 +109,7 @@ protected function setUp(): void * @dataProvider provideMixinTagsData * @dataProvider provideRequireExtendsTagsData * @dataProvider provideRequireImplementsTagsData - * @dataProvider provideInheritorsTagsData + * @dataProvider provideSealedTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData @@ -2212,15 +2212,15 @@ public function provideRequireImplementsTagsData(): Iterator ]; } - public function provideInheritorsTagsData(): Iterator + public function provideSealedTagsData(): Iterator { yield [ 'OK without description', - '/** @phpstan-inheritors Foo|Bar */', + '/** @phpstan-sealed Foo|Bar */', new PhpDocNode([ new PhpDocTagNode( - '@phpstan-inheritors', - new InheritorsTagValueNode( + '@phpstan-sealed', + new SealedTagValueNode( new IdentifierTypeNode('Foo|Bar'), '', ), @@ -2230,11 +2230,11 @@ public function provideInheritorsTagsData(): Iterator yield [ 'OK with description', - '/** @phpstan-inheritors Foo|Bar optional description */', + '/** @phpstan-sealed Foo|Bar optional description */', new PhpDocNode([ new PhpDocTagNode( - '@phpstan-inheritors', - new InheritorsTagValueNode( + '@phpstan-sealed', + new SealedTagValueNode( new IdentifierTypeNode('Foo|Bar'), 'optional description', ), @@ -2248,7 +2248,7 @@ public function provideInheritorsTagsData(): Iterator new PhpDocNode([ new PhpDocTagNode( '@psalm-inheritors', - new InheritorsTagValueNode( + new SealedTagValueNode( new IdentifierTypeNode('Foo|Bar'), 'optional description', ), @@ -2258,16 +2258,16 @@ public function provideInheritorsTagsData(): Iterator yield [ 'invalid without type and description', - '/** @phpstan-inheritors */', + '/** @phpstan-sealed */', new PhpDocNode([ new PhpDocTagNode( - '@phpstan-inheritors', + '@phpstan-sealed', new InvalidTagValueNode( '', new ParserException( '*/', Lexer::TOKEN_CLOSE_PHPDOC, - 24, + 20, Lexer::TOKEN_IDENTIFIER, null, 1, From 0a0ff8a590177f4615f23b0ced5594911b497cc4 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 12 Jul 2025 13:46:41 +0200 Subject: [PATCH 4/4] Fix --- src/Printer/Printer.php | 2 +- tests/PHPStan/Parser/PhpDocParserTest.php | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 54f98896..36f6ebe1 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -18,7 +18,6 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; @@ -37,6 +36,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index c8b93986..b2857b2b 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -25,7 +25,6 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; @@ -43,6 +42,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\SealedTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; @@ -2221,7 +2221,10 @@ public function provideSealedTagsData(): Iterator new PhpDocTagNode( '@phpstan-sealed', new SealedTagValueNode( - new IdentifierTypeNode('Foo|Bar'), + new UnionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), '', ), ), @@ -2235,7 +2238,10 @@ public function provideSealedTagsData(): Iterator new PhpDocTagNode( '@phpstan-sealed', new SealedTagValueNode( - new IdentifierTypeNode('Foo|Bar'), + new UnionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), 'optional description', ), ), @@ -2249,7 +2255,10 @@ public function provideSealedTagsData(): Iterator new PhpDocTagNode( '@psalm-inheritors', new SealedTagValueNode( - new IdentifierTypeNode('Foo|Bar'), + new UnionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), 'optional description', ), ),