From 62986424db345bbe6fdbea788fb156fc1fe1bb86 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 8 Jun 2022 12:31:38 +0200 Subject: [PATCH 1/6] Allow omitting @param type --- doc/grammars/phpdoc-param.peg | 1 + src/Ast/PhpDoc/ParamTagValueNode.php | 7 +- src/Parser/PhpDocParser.php | 30 +++++++- tests/PHPStan/Parser/PhpDocParserTest.php | 85 +++++++++++++++++++++++ 4 files changed, 118 insertions(+), 5 deletions(-) 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 d0286d55..99e98943 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 31bb0247..f59872a9 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 + ) + ) + ), + ]), + ]; } From 9f7da2523fd572deba8c4cde900e46c65c29e54f Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 8 Jun 2022 14:17:58 +0200 Subject: [PATCH 2/6] Introduce TypelessParamTagValueNode --- src/Ast/PhpDoc/ParamTagValueNode.php | 7 +- src/Ast/PhpDoc/PhpDocNode.php | 14 ++ src/Ast/PhpDoc/TypelessParamTagValueNode.php | 41 ++++++ src/Parser/PhpDocParser.php | 15 ++- tests/PHPStan/Parser/PhpDocParserTest.php | 134 +++++++++---------- 5 files changed, 137 insertions(+), 74 deletions(-) create mode 100644 src/Ast/PhpDoc/TypelessParamTagValueNode.php diff --git a/src/Ast/PhpDoc/ParamTagValueNode.php b/src/Ast/PhpDoc/ParamTagValueNode.php index 9bb48c58..f93af0ea 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|null */ + /** @var TypeNode */ 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,10 +38,9 @@ public function __construct(?TypeNode $type, bool $isVariadic, string $parameter public function __toString(): string { - $type = $this->type !== null ? "{$this->type} " : ''; $reference = $this->isReference ? '&' : ''; $variadic = $this->isVariadic ? '...' : ''; - return trim("{$type}{$reference}{$variadic}{$this->parameterName} {$this->description}"); + return trim("{$this->type} {$reference}{$variadic}{$this->parameterName} {$this->description}"); } } diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 7390b0c3..45063528 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -76,6 +76,20 @@ static function (PhpDocTagValueNode $value): bool { } + /** + * @return ParamTagValueNode[] + */ + public function getTypelessParamTagValues(string $tagName = '@param'): array + { + return array_filter( + array_column($this->getTagsByName($tagName), 'value'), + static function (PhpDocTagValueNode $value): bool { + return $value instanceof TypelessParamTagValueNode; + } + ); + } + + /** * @return TemplateTagValueNode[] */ diff --git a/src/Ast/PhpDoc/TypelessParamTagValueNode.php b/src/Ast/PhpDoc/TypelessParamTagValueNode.php new file mode 100644 index 00000000..fc460a5b --- /dev/null +++ b/src/Ast/PhpDoc/TypelessParamTagValueNode.php @@ -0,0 +1,41 @@ +isReference = $isReference; + $this->isVariadic = $isVariadic; + $this->parameterName = $parameterName; + $this->description = $description; + } + + + public function __toString(): string + { + $reference = $this->isReference ? '&' : ''; + $variadic = $this->isVariadic ? '...' : ''; + return trim("{$reference}{$variadic}{$this->parameterName} {$this->description}"); + } + +} diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 99e98943..d33f5879 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -231,7 +231,10 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph } - private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamTagValueNode + /** + * @return Ast\PhpDoc\ParamTagValueNode|Ast\PhpDoc\TypelessParamTagValueNode + */ + private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode { if ( $tokens->isCurrentTokenType(Lexer::TOKEN_REFERENCE) @@ -246,8 +249,14 @@ private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\ParamTagV $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); $parameterName = $this->parseRequiredVariableName($tokens); - $description = $type === null ? $this->parseRequiredDescription($tokens) : $this->parseOptionalDescription($tokens); - return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference); + + if ($type !== null) { + $description = $this->parseOptionalDescription($tokens); + return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference); + } + + $description = $this->parseRequiredDescription($tokens); + return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference); } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index f59872a9..d9646c86 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -27,6 +27,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ThrowsTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -64,6 +65,7 @@ protected function setUp(): void /** * @dataProvider provideTagsWithNumbers * @dataProvider provideParamTagsData + * @dataProvider provideTypelessParamTagsData * @dataProvider provideVarTagsData * @dataProvider provideReturnTagsData * @dataProvider provideThrowsTagsData @@ -242,72 +244,6 @@ 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 */', @@ -459,6 +395,71 @@ public function provideParamTagsData(): Iterator ), ]), ]; + } + + public function provideTypelessParamTagsData(): Iterator + { + yield [ + 'OK', + '/** @param $foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new TypelessParamTagValueNode( + false, + '$foo', + 'description' + ) + ), + ]), + ]; + + yield [ + 'OK reference', + '/** @param &$foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new TypelessParamTagValueNode( + false, + '$foo', + 'description', + true + ) + ), + ]), + ]; + + yield [ + 'OK variadic', + '/** @param ...$foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new TypelessParamTagValueNode( + true, + '$foo', + 'description' + ) + ), + ]), + ]; + + yield [ + 'OK reference variadic', + '/** @param &...$foo description */', + new PhpDocNode([ + new PhpDocTagNode( + '@param', + new TypelessParamTagValueNode( + true, + '$foo', + 'description', + true + ) + ), + ]), + ]; yield [ 'invalid without type and description', @@ -480,7 +481,6 @@ public function provideParamTagsData(): Iterator ]; } - public function provideVarTagsData(): Iterator { yield [ From 184e7309568a43b833932531df6241267d46becc Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 8 Jun 2022 14:22:52 +0200 Subject: [PATCH 3/6] Fix return type --- src/Ast/PhpDoc/PhpDocNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ast/PhpDoc/PhpDocNode.php b/src/Ast/PhpDoc/PhpDocNode.php index 45063528..a0caef68 100644 --- a/src/Ast/PhpDoc/PhpDocNode.php +++ b/src/Ast/PhpDoc/PhpDocNode.php @@ -77,7 +77,7 @@ static function (PhpDocTagValueNode $value): bool { /** - * @return ParamTagValueNode[] + * @return TypelessParamTagValueNode[] */ public function getTypelessParamTagValues(string $tagName = '@param'): array { From d9b660cdc35633ee4adb3826c80baaeb79e43c2c Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 8 Jun 2022 18:32:40 +0200 Subject: [PATCH 4/6] Do not require description --- src/Ast/PhpDoc/TypelessParamTagValueNode.php | 2 +- src/Parser/PhpDocParser.php | 20 +------------------- tests/PHPStan/Parser/PhpDocParserTest.php | 13 +++++-------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/Ast/PhpDoc/TypelessParamTagValueNode.php b/src/Ast/PhpDoc/TypelessParamTagValueNode.php index fc460a5b..8b982954 100644 --- a/src/Ast/PhpDoc/TypelessParamTagValueNode.php +++ b/src/Ast/PhpDoc/TypelessParamTagValueNode.php @@ -19,7 +19,7 @@ class TypelessParamTagValueNode implements PhpDocTagValueNode /** @var string */ public $parameterName; - /** @var string */ + /** @var string (may be empty) */ public $description; public function __construct(bool $isVariadic, string $parameterName, string $description, bool $isReference = false) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index d33f5879..0679ff5c 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -249,13 +249,12 @@ private function parseParamTagValue(TokenIterator $tokens): Ast\PhpDoc\PhpDocTag $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); $parameterName = $this->parseRequiredVariableName($tokens); + $description = $this->parseOptionalDescription($tokens); if ($type !== null) { - $description = $this->parseOptionalDescription($tokens); return new Ast\PhpDoc\ParamTagValueNode($type, $isVariadic, $parameterName, $description, $isReference); } - $description = $this->parseRequiredDescription($tokens); return new Ast\PhpDoc\TypelessParamTagValueNode($isVariadic, $parameterName, $description, $isReference); } @@ -482,23 +481,6 @@ 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 { if ($limitStartToken) { diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index d9646c86..e91a4042 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -462,19 +462,16 @@ public function provideTypelessParamTagsData(): Iterator ]; yield [ - 'invalid without type and description', + 'OK without type and description', '/** @param $foo */', new PhpDocNode([ new PhpDocTagNode( '@param', - new InvalidTagValueNode( + new TypelessParamTagValueNode( + false, '$foo', - new ParserException( - '*/', - Lexer::TOKEN_CLOSE_PHPDOC, - 16, - Lexer::TOKEN_OTHER - ) + '', + false ) ), ]), From 6c30fbdc23434a10287efae5bd5650aab437cf7f Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 8 Jun 2022 18:36:21 +0200 Subject: [PATCH 5/6] Update phpdoc-param.peg --- doc/grammars/phpdoc-param.peg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/grammars/phpdoc-param.peg b/doc/grammars/phpdoc-param.peg index e543bd93..f16cb948 100644 --- a/doc/grammars/phpdoc-param.peg +++ b/doc/grammars/phpdoc-param.peg @@ -1,6 +1,5 @@ PhpDocParam - = AnnotationName Type IsReference? IsVariadic? ParameterName Description? - / AnnotationName Type? IsReference? IsVariadic? ParameterName Description + = AnnotationName Type? IsReference? IsVariadic? ParameterName Description? AnnotationName = '@param' From b8563293ee2e4bdb222c3a081779b37212a3a108 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 8 Jun 2022 18:38:28 +0200 Subject: [PATCH 6/6] Fix CS --- src/Parser/PhpDocParser.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index 0679ff5c..1e9abc05 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -8,7 +8,6 @@ use PHPStan\ShouldNotHappenException; use function array_values; use function count; -use function strlen; use function trim; class PhpDocParser