diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 6abad3d0..a3dd3e4c 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 SealedTagValueNode[] + */ + public function getSealedTagValues(string $tagName = '@phpstan-sealed'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static fn (PhpDocTagValueNode $value): bool => $value instanceof SealedTagValueNode, + ); + } + /** * @return DeprecatedTagValueNode[] */ diff --git a/src/Ast/PhpDoc/SealedTagValueNode.php b/src/Ast/PhpDoc/SealedTagValueNode.php new file mode 100644 index 00000000..230cf3e8 --- /dev/null +++ b/src/Ast/PhpDoc/SealedTagValueNode.php @@ -0,0 +1,31 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 559d8fd5..f36b9136 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-sealed': + $tagValue = $this->parseSealedTagValue($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 parseSealedTagValue(TokenIterator $tokens): Ast\PhpDoc\SealedTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\SealedTagValueNode($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..36f6ebe1 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -36,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; @@ -327,6 +328,10 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$node->description}"); } + if ($node instanceof SealedTagValueNode) { + $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..b2857b2b 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -42,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; @@ -108,6 +109,7 @@ protected function setUp(): void * @dataProvider provideMixinTagsData * @dataProvider provideRequireExtendsTagsData * @dataProvider provideRequireImplementsTagsData + * @dataProvider provideSealedTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData @@ -2210,6 +2212,81 @@ public function provideRequireImplementsTagsData(): Iterator ]; } + public function provideSealedTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-sealed Foo|Bar */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-sealed', + new SealedTagValueNode( + new UnionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), + '', + ), + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-sealed Foo|Bar optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-sealed', + new SealedTagValueNode( + new UnionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), + 'optional description', + ), + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-inheritors Foo|Bar optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-inheritors', + new SealedTagValueNode( + new UnionTypeNode([ + new IdentifierTypeNode('Foo'), + new IdentifierTypeNode('Bar'), + ]), + 'optional description', + ), + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-sealed */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-sealed', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 20, + Lexer::TOKEN_IDENTIFIER, + null, + 1, + ), + ), + ), + ]), + ]; + } + public function provideDeprecatedTagsData(): Iterator { yield [