diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 25f1939c..4c509f41 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -187,6 +187,31 @@ static function (PhpDocTagValueNode $value): bool { ); } + /** + * @return RequireExtendsTagValueNode[] + */ + public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof RequireExtendsTagValueNode; + } + ); + } + + /** + * @return RequireImplementsTagValueNode[] + */ + public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof RequireImplementsTagValueNode; + } + ); + } /** * @return DeprecatedTagValueNode[] diff --git a/src/Ast/PhpDoc/RequireExtendsTagValueNode.php b/src/Ast/PhpDoc/RequireExtendsTagValueNode.php new file mode 100644 index 00000000..91c26892 --- /dev/null +++ b/src/Ast/PhpDoc/RequireExtendsTagValueNode.php @@ -0,0 +1,32 @@ +type = $type; + $this->description = $description; + } + + + public function __toString(): string + { + return trim("{$this->type} {$this->description}"); + } + +} diff --git a/src/Ast/PhpDoc/RequireImplementsTagValueNode.php b/src/Ast/PhpDoc/RequireImplementsTagValueNode.php new file mode 100644 index 00000000..65c9213f --- /dev/null +++ b/src/Ast/PhpDoc/RequireImplementsTagValueNode.php @@ -0,0 +1,32 @@ +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 15a2aa5c..e87d92c4 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -408,6 +408,16 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph $tagValue = $this->parseMixinTagValue($tokens); break; + case '@psalm-require-extends': + case '@phpstan-require-extends': + $tagValue = $this->parseRequireExtendsTagValue($tokens); + break; + + case '@psalm-require-implements': + case '@phpstan-require-implements': + $tagValue = $this->parseRequireImplementsTagValue($tokens); + break; + case '@deprecated': $tagValue = $this->parseDeprecatedTagValue($tokens); break; @@ -877,6 +887,20 @@ private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagV return new Ast\PhpDoc\MixinTagValueNode($type, $description); } + private function parseRequireExtendsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireExtendsTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\RequireExtendsTagValueNode($type, $description); + } + + private function parseRequireImplementsTagValue(TokenIterator $tokens): Ast\PhpDoc\RequireImplementsTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\RequireImplementsTagValueNode($type, $description); + } + private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode { $description = $this->parseOptionalDescription($tokens); diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 57e652eb..0093e6ca 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -28,6 +28,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -283,6 +285,14 @@ private function printTagValue(PhpDocTagValueNode $node): string $type = $this->printType($node->type); return trim("{$type} {$node->description}"); } + if ($node instanceof RequireExtendsTagValueNode) { + $type = $this->printType($node->type); + return trim("{$type} {$node->description}"); + } + if ($node instanceof RequireImplementsTagValueNode) { + $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/Ast/ToString/PhpDocToStringTest.php b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php index faac0616..b57b7db6 100644 --- a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php +++ b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php @@ -28,6 +28,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -213,6 +215,15 @@ public static function provideClassCases(): Generator ['Foo\\Bar Baz', new MixinTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], ]; + yield from [ + ['PHPUnit\\TestCase', new RequireExtendsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')], + ['Foo\\Bar Baz', new RequireExtendsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], + ]; + yield from [ + ['PHPUnit\\TestCase', new RequireImplementsTagValueNode(new IdentifierTypeNode('PHPUnit\\TestCase'), '')], + ['Foo\\Bar Baz', new RequireImplementsTagValueNode(new IdentifierTypeNode('Foo\\Bar'), 'Baz')], + ]; + yield from [ ['Foo array', new TypeAliasTagValueNode('Foo', $arrayOfStrings)], ['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)], diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 3b293f8f..67a9d123 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -35,6 +35,8 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\SelfOutTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; @@ -100,6 +102,8 @@ protected function setUp(): void * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData * @dataProvider provideMixinTagsData + * @dataProvider provideRequireExtendsTagsData + * @dataProvider provideRequireImplementsTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData @@ -1908,6 +1912,138 @@ public function provideMixinTagsData(): Iterator ]; } + public function provideRequireExtendsTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-require-extends Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-require-extends Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-require-extends Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-require-extends', + new RequireExtendsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-require-extends */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-extends', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 29, + Lexer::TOKEN_IDENTIFIER, + null, + 1 + ) + ) + ), + ]), + ]; + } + + public function provideRequireImplementsTagsData(): Iterator + { + yield [ + 'OK without description', + '/** @phpstan-require-implements Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @phpstan-require-implements Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'OK with psalm-prefix description', + '/** @psalm-require-implements Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@psalm-require-implements', + new RequireImplementsTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @phpstan-require-implements */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-require-implements', + new InvalidTagValueNode( + '', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 32, + Lexer::TOKEN_IDENTIFIER, + null, + 1 + ) + ) + ), + ]), + ]; + } + public function provideDeprecatedTagsData(): Iterator { yield [