From 249f15fb843bf240cf058372dad29e100cee6c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Fri, 20 Sep 2024 15:32:55 +0200 Subject: [PATCH] PhpDocParser: support template type lower bounds --- doc/grammars/type.abnf | 5 +- src/Ast/PhpDoc/TemplateTagValueNode.php | 11 +++- src/Parser/TypeParser.php | 11 ++-- src/Printer/Printer.php | 5 +- .../Ast/ToString/PhpDocToStringTest.php | 9 ++- tests/PHPStan/Parser/PhpDocParserTest.php | 31 ++++++++-- tests/PHPStan/Printer/PrinterTest.php | 56 +++++++++++++++++++ 7 files changed, 109 insertions(+), 19 deletions(-) diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 36118d2b..ad955a9b 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -41,7 +41,7 @@ CallableTemplate = TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose CallableTemplateArgument - = TokenIdentifier [1*ByteHorizontalWs TokenOf Type] + = TokenIdentifier [1*ByteHorizontalWs TokenOf Type] [1*ByteHorizontalWs TokenSuper Type] ["=" Type] CallableParameters = CallableParameter *(TokenComma CallableParameter) @@ -201,6 +201,9 @@ TokenNot TokenOf = %s"of" 1*ByteHorizontalWs +TokenSuper + = %s"super" 1*ByteHorizontalWs + TokenContravariant = %s"contravariant" 1*ByteHorizontalWs diff --git a/src/Ast/PhpDoc/TemplateTagValueNode.php b/src/Ast/PhpDoc/TemplateTagValueNode.php index 78b311ee..8bc01f6e 100644 --- a/src/Ast/PhpDoc/TemplateTagValueNode.php +++ b/src/Ast/PhpDoc/TemplateTagValueNode.php @@ -17,6 +17,9 @@ class TemplateTagValueNode implements PhpDocTagValueNode /** @var TypeNode|null */ public $bound; + /** @var TypeNode|null */ + public $lowerBound; + /** @var TypeNode|null */ public $default; @@ -26,10 +29,11 @@ class TemplateTagValueNode implements PhpDocTagValueNode /** * @param non-empty-string $name */ - public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null) + public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null) { $this->name = $name; $this->bound = $bound; + $this->lowerBound = $lowerBound; $this->default = $default; $this->description = $description; } @@ -37,9 +41,10 @@ public function __construct(string $name, ?TypeNode $bound, string $description, public function __toString(): string { - $bound = $this->bound !== null ? " of {$this->bound}" : ''; + $upperBound = $this->bound !== null ? " of {$this->bound}" : ''; + $lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : ''; $default = $this->default !== null ? " = {$this->default}" : ''; - return trim("{$this->name}{$bound}{$default} {$this->description}"); + return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}"); } } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index c47ba10f..2be28398 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -491,11 +491,14 @@ public function parseTemplateTagValue( $name = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + $upperBound = $lowerBound = null; + if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { - $bound = $this->parse($tokens); + $upperBound = $this->parse($tokens); + } - } else { - $bound = null; + if ($tokens->tryConsumeTokenValue('super')) { + $lowerBound = $this->parse($tokens); } if ($tokens->tryConsumeTokenValue('=')) { @@ -514,7 +517,7 @@ public function parseTemplateTagValue( throw new LogicException('Template tag name cannot be empty.'); } - return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default); + return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound); } diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index c4b9c356..75500780 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -335,9 +335,10 @@ private function printTagValue(PhpDocTagValueNode $node): string return trim($type . ' ' . $node->description); } if ($node instanceof TemplateTagValueNode) { - $bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : ''; + $upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : ''; + $lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : ''; $default = $node->default !== null ? ' = ' . $this->printType($node->default) : ''; - return trim("{$node->name}{$bound}{$default} {$node->description}"); + return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}"); } if ($node instanceof ThrowsTagValueNode) { $type = $this->printType($node->type); diff --git a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php index b57b7db6..74d257ab 100644 --- a/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php +++ b/tests/PHPStan/Ast/ToString/PhpDocToStringTest.php @@ -155,12 +155,15 @@ public static function provideOtherCases(): Generator $baz = new IdentifierTypeNode('Foo\\Baz'); yield from [ - ['TValue', new TemplateTagValueNode('TValue', null, '', null)], - ['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '', null)], + ['TValue', new TemplateTagValueNode('TValue', null, '')], + ['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '')], + ['TValue super Foo\\Bar', new TemplateTagValueNode('TValue', null, '', null, $bar)], ['TValue = Foo\\Bar', new TemplateTagValueNode('TValue', null, '', $bar)], ['TValue of Foo\\Bar = Foo\\Baz', new TemplateTagValueNode('TValue', $bar, '', $baz)], - ['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.', null)], + ['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.')], ['TValue of Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', $baz)], + ['TValue super Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', null, 'Description.', $baz, $bar)], + ['TValue of Foo\\Bar super Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', null, $baz)], ]; } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index f765015c..f1efa5f6 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -3986,7 +3986,7 @@ public function provideTemplateTagsData(): Iterator ]; yield [ - 'OK with bound and description', + 'OK with upper bound and description', '/** @template T of DateTime the value type */', new PhpDocNode([ new PhpDocTagNode( @@ -4001,22 +4001,41 @@ public function provideTemplateTagsData(): Iterator ]; yield [ - 'OK with bound and description', - '/** @template T as DateTime the value type */', + 'OK with lower bound and description', + '/** @template T super DateTimeImmutable the value type */', new PhpDocNode([ new PhpDocTagNode( '@template', new TemplateTagValueNode( 'T', - new IdentifierTypeNode('DateTime'), - 'the value type' + null, + 'the value type', + null, + new IdentifierTypeNode('DateTimeImmutable') + ) + ), + ]), + ]; + + yield [ + 'OK with both bounds and description', + '/** @template T of DateTimeInterface super DateTimeImmutable the value type */', + new PhpDocNode([ + new PhpDocTagNode( + '@template', + new TemplateTagValueNode( + 'T', + new IdentifierTypeNode('DateTimeInterface'), + 'the value type', + null, + new IdentifierTypeNode('DateTimeImmutable') ) ), ]), ]; yield [ - 'invalid without bound and description', + 'invalid without bounds and description', '/** @template */', new PhpDocNode([ new PhpDocTagNode( diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index d9eab2d0..ee549c7e 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -947,6 +947,12 @@ public function enterNode(Node $node) $addTemplateTagBound, ]; + yield [ + '/** @template T super string */', + '/** @template T of int super string */', + $addTemplateTagBound, + ]; + $removeTemplateTagBound = new class extends AbstractNodeVisitor { public function enterNode(Node $node) @@ -966,6 +972,56 @@ public function enterNode(Node $node) $removeTemplateTagBound, ]; + $addTemplateTagLowerBound = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TemplateTagValueNode) { + $node->lowerBound = new IdentifierTypeNode('int'); + } + + return $node; + } + + }; + + yield [ + '/** @template T */', + '/** @template T super int */', + $addTemplateTagLowerBound, + ]; + + yield [ + '/** @template T super string */', + '/** @template T super int */', + $addTemplateTagLowerBound, + ]; + + yield [ + '/** @template T of string */', + '/** @template T of string super int */', + $addTemplateTagLowerBound, + ]; + + $removeTemplateTagLowerBound = new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof TemplateTagValueNode) { + $node->lowerBound = null; + } + + return $node; + } + + }; + + yield [ + '/** @template T super int */', + '/** @template T */', + $removeTemplateTagLowerBound, + ]; + $addKeyNameToArrayShapeItemNode = new class extends AbstractNodeVisitor { public function enterNode(Node $node)