diff --git a/doc/grammars/phpdoc-param.peg b/doc/grammars/phpdoc-param.peg index 436c20fc..e543bd93 100644 --- a/doc/grammars/phpdoc-param.peg +++ b/doc/grammars/phpdoc-param.peg @@ -1,5 +1,6 @@ PhpDocParam = AnnotationName Type IsReference? IsVariadic? ParameterName Description? + / AnnotationName Type? IsReference? IsVariadic? ParameterName Description AnnotationName = '@param' diff --git a/src/Ast/PhpDoc/ParamTagValueNode.php b/src/Ast/PhpDoc/ParamTagValueNode.php index f93af0ea..9bb48c58 100644 --- a/src/Ast/PhpDoc/ParamTagValueNode.php +++ b/src/Ast/PhpDoc/ParamTagValueNode.php @@ -11,7 +11,7 @@ class ParamTagValueNode implements PhpDocTagValueNode use NodeAttributes; - /** @var TypeNode */ + /** @var TypeNode|null */ public $type; /** @var bool */ @@ -26,7 +26,7 @@ class ParamTagValueNode implements PhpDocTagValueNode /** @var string (may be empty) */ public $description; - public function __construct(TypeNode $type, bool $isVariadic, string $parameterName, string $description, bool $isReference = false) + public function __construct(?TypeNode $type, bool $isVariadic, string $parameterName, string $description, bool $isReference = false) { $this->type = $type; $this->isReference = $isReference; @@ -38,9 +38,10 @@ public function __construct(TypeNode $type, bool $isVariadic, string $parameterN public function __toString(): string { + $type = $this->type !== null ? "{$this->type} " : ''; $reference = $this->isReference ? '&' : ''; $variadic = $this->isVariadic ? '...' : ''; - return trim("{$this->type} {$reference}{$variadic}{$this->parameterName} {$this->description}"); + return trim("{$type}{$reference}{$variadic}{$this->parameterName} {$this->description}"); } } diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index fd034d7a..3b6f4ff6 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -8,6 +8,7 @@ use PHPStan\ShouldNotHappenException; use function array_values; use function count; +use function strlen; use function trim; class PhpDocParser @@ -232,11 +233,20 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamTagValueNode { - $type = $this->typeParser->parse($tokens); + if ( + $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE) + || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIADIC) + || $tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE) + ) { + $type = null; + } else { + $type = $this->typeParser->parse($tokens); + } + $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); $parameterName = $this->parseRequiredVariableName($tokens); - $description = $this->parseOptionalDescription($tokens); + $description = $type === null ? $this->parseRequiredDescription($tokens) : $this->parseOptionalDescription($tokens); return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference); } @@ -463,6 +473,22 @@ private function parseRequiredVariableName(TokenIterator $tokens): string return $parameterName; } + private function parseRequiredDescription(TokenIterator $tokens): string + { + $tokens->pushSavePoint(); + + $description = $this->parseOptionalDescription($tokens); + + if (strlen($description) === 0) { + $tokens->rollback(); + + $tokens->consumeTokenType(Lexer::TOKEN_OTHER); + } + + $tokens->dropSavePoint(); + + return $description; + } private function parseOptionalDescription(TokenIterator $tokens, bool $limitStartToken = false): string { diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 20451b7c..cecadde0 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -242,6 +242,72 @@ public function provideParamTagsData(): Iterator ]), ]; + yield [ + 'OK without type', + '/** @param $foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + null, + false, + '$foo', + 'description' + ) + ), + ]), + ]; + + yield [ + 'OK reference without type', + '/** @param &$foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + null, + false, + '$foo', + 'description', + true + ) + ), + ]), + ]; + + yield [ + 'OK variadic without type', + '/** @param ...$foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + null, + true, + '$foo', + 'description' + ) + ), + ]), + ]; + + yield [ + 'OK reference variadic without type', + '/** @param &...$foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new ParamTagValueNode( + null, + true, + '$foo', + 'description', + true + ) + ), + ]), + ]; + yield [ 'invalid without type, parameter name and description', '/** @param */', @@ -393,6 +459,25 @@ public function provideParamTagsData(): Iterator ), ]), ]; + + yield [ + 'invalid without type and description', + '/** @param $foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new InvalidTagValueNode( + '$foo', + new ParserException( + '*/', + Lexer::TOKEN_CLOSE_PHPDOC, + 16, + Lexer::TOKEN_OTHER + ) + ) + ), + ]), + ]; }