diff --git a/src/Ast/PhpDoc/MixinTagValueNode.php b/src/Ast/PhpDoc/MixinTagValueNode.php new file mode 100644 index 0000000..8290181 --- /dev/null +++ b/src/Ast/PhpDoc/MixinTagValueNode.php @@ -0,0 +1,28 @@ +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 bb14d0d..64a57af 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -154,6 +154,20 @@ class PhpDocNode implements Node } + /** + * @return MixinTagValueNode[] + */ + public function getMixinTagValues(string $tagName = '@mixin'): array + { + return array_column( + array_filter($this->getTagsByName($tagName), static function (PhpDocTagNode $tag): bool { + return $tag->value instanceof MixinTagValueNode; + }), + 'value' + ); + } + + /** * @return \PHPStan\PhpDocParser\Ast\PhpDoc\DeprecatedTagValueNode[] */ diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 17280b8..ecfbc3a 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -136,6 +136,10 @@ class PhpDocParser $tagValue = $this->parseThrowsTagValue($tokens); break; + case '@mixin': + $tagValue = $this->parseMixinTagValue($tokens); + break; + case '@deprecated': $tagValue = $this->parseDeprecatedTagValue($tokens); break; @@ -235,6 +239,13 @@ class PhpDocParser return new Ast\PhpDoc\ThrowsTagValueNode($type, $description); } + private function parseMixinTagValue(TokenIterator $tokens): Ast\PhpDoc\MixinTagValueNode + { + $type = $this->typeParser->parse($tokens); + $description = $this->parseOptionalDescription($tokens, true); + return new Ast\PhpDoc\MixinTagValueNode($type, $description); + } + private function parseDeprecatedTagValue(TokenIterator $tokens): Ast\PhpDoc\DeprecatedTagValueNode { $description = $this->parseOptionalDescription($tokens); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 79dd248..19bcfb1 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -12,6 +12,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ImplementsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\MethodTagValueParameterNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; @@ -54,6 +55,7 @@ class PhpDocParserTest extends \PHPUnit\Framework\TestCase * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData + * @dataProvider provideMixinTagsData * @dataProvider provideDeprecatedTagsData * @dataProvider providePropertyTagsData * @dataProvider provideMethodTagsData @@ -1064,6 +1066,105 @@ class PhpDocParserTest extends \PHPUnit\Framework\TestCase ]; } + public function provideMixinTagsData(): \Iterator + { + yield [ + 'OK without description', + '/** @mixin Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@mixin', + new MixinTagValueNode( + new IdentifierTypeNode('Foo'), + '' + ) + ), + ]), + ]; + + yield [ + 'OK with description', + '/** @mixin Foo optional description */', + new PhpDocNode([ + new PhpDocTagNode( + '@mixin', + new MixinTagValueNode( + new IdentifierTypeNode('Foo'), + 'optional description' + ) + ), + ]), + ]; + + yield [ + 'OK with description that starts with TOKEN_OPEN_SQUARE_BRACKET', + '/** @mixin Foo [Bar] */', + new PhpDocNode([ + new PhpDocTagNode( + '@mixin', + new MixinTagValueNode( + new IdentifierTypeNode('Foo'), + '[Bar]' + ) + ), + ]), + ]; + + yield [ + 'invalid without type and description', + '/** @mixin */', + new PhpDocNode([ + new PhpDocTagNode( + '@mixin', + new InvalidTagValueNode( + '', + new \PHPStan\PhpDocParser\Parser\ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 11, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + + yield [ + 'invalid with type and disallowed description start token', + '/** @mixin Foo | #desc */', + new PhpDocNode([ + new PhpDocTagNode( + '@mixin', + new InvalidTagValueNode( + 'Foo | #desc', + new \PHPStan\PhpDocParser\Parser\ParserException( + '#desc', + Lexer::TOKEN_OTHER, + 17, + Lexer::TOKEN_IDENTIFIER + ) + ) + ), + ]), + ]; + + yield [ + 'generic @mixin', + '/** @mixin Foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@mixin', + new MixinTagValueNode( + new GenericTypeNode(new IdentifierTypeNode('Foo'), [ + new IdentifierTypeNode('Bar'), + ]), + '' + ) + ), + ]), + ]; + } + public function provideDeprecatedTagsData(): \Iterator { yield [