diff --git a/doc/grammars/phpdoc-block.pp3 b/doc/grammars/phpdoc-block.pp3 index 9a654614..bbb74384 100644 --- a/doc/grammars/phpdoc-block.pp3 +++ b/doc/grammars/phpdoc-block.pp3 @@ -592,8 +592,17 @@ DoctrineArgumentList : DoctrineArgument() Eols() ( Eols() ( & | DoctrineArgument() Eols() ) )* ; +/** + * An argument of an annotation, like: + * - bar="baz" + * - "bar"="baz" + * + * Doctrine reads only a name before the "=", but a key written in quotes is + * read too, as the name of the argument. + */ DoctrineArgument : Identifier() Eols() Eols() DoctrineValue() + | DoctrineString() Eols() Eols() DoctrineValue() | DoctrineValue() ; diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 29fb4be9..581a8b1e 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -587,7 +587,8 @@ private function parseDoctrineArguments(TokenIterator $tokens, bool $deep): arra private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\DoctrineArgument { - if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { + // a quoted key, e.g. "key"="value", is not valid Doctrine syntax, but is accepted as the key + if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER, Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) { $startLine = $tokens->currentTokenLine(); $startIndex = $tokens->currentTokenIndex(); @@ -604,8 +605,12 @@ private function parseDoctrineArgument(TokenIterator $tokens): Doctrine\Doctrine try { $tokens->pushSavePoint(); + $isIdentifier = $tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER); $currentValue = $tokens->currentTokenValue(); - $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + $tokens->next(); + if (!$isIdentifier) { + $currentValue = $this->doctrineConstantExprParser->parseDoctrineString($currentValue, $tokens)->value; + } $key = $this->enrichWithAttributes( $tokens, diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 95d045c5..b9b59ba6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -7523,6 +7523,34 @@ public function provideDoctrineWithoutDoctrineCheckData(): Iterator new PhpDocTagNode('@package', new GenericTagValueNode('foo')), ]), ]; + yield [ + 'Quoted argument key', + '/** @DummyAnnotation(targetEntity=Training::class, "indexBy" = "id", "a""b"="c") */', + new PhpDocNode([ + new PhpDocTagNode('@DummyAnnotation', new DoctrineTagValueNode( + new DoctrineAnnotation('@DummyAnnotation', [ + new DoctrineArgument(new IdentifierTypeNode('targetEntity'), new ConstFetchNode('Training', 'class')), + new DoctrineArgument(new IdentifierTypeNode('indexBy'), new DoctrineConstExprStringNode('id')), + new DoctrineArgument(new IdentifierTypeNode('a"b'), new DoctrineConstExprStringNode('c')), + ]), + '', + )), + ]), + ]; + + yield [ + 'Quoted positional argument', + '/** @DummyAnnotation("foo", "bar") */', + new PhpDocNode([ + new PhpDocTagNode('@DummyAnnotation', new DoctrineTagValueNode( + new DoctrineAnnotation('@DummyAnnotation', [ + new DoctrineArgument(null, new DoctrineConstExprStringNode('foo')), + new DoctrineArgument(null, new DoctrineConstExprStringNode('bar')), + ]), + '', + )), + ]), + ]; } public function provideSpecializedTags(): Iterator diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 5261f5ab..4288e1bc 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -10,6 +10,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\DoctrineConstExprStringNode; use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Ast\NodeVisitor; @@ -2231,6 +2232,23 @@ public function enterNode(Node $node) }, ]; + yield [ + '/** @Foo("indexBy" = "id", "name"="foo") */', + '/** @Foo("indexBy" = "id", "name"="bar") */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof DoctrineArgument && $node->key !== null && $node->key->name === 'name') { + $node->value = new DoctrineConstExprStringNode('bar'); + } + + return $node; + } + + }, + ]; + yield [ '/** @Foo() */', '/** @Bar() */',