From 0eb13830b5181fce4bee17296c9fadc322608dd3 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Thu, 15 Feb 2024 20:41:08 +0100 Subject: [PATCH 1/2] Generic type aliases --- src/Ast/PhpDoc/TypeAliasTagValueNode.php | 21 +++++++- src/Parser/PhpDocParser.php | 26 ++++++++-- src/Printer/Printer.php | 10 +++- tests/PHPStan/Parser/PhpDocParserTest.php | 54 ++++++++++++++++++++ tests/PHPStan/Printer/PrinterTest.php | 61 +++++++++++++++++++++++ 5 files changed, 166 insertions(+), 6 deletions(-) diff --git a/src/Ast/PhpDoc/TypeAliasTagValueNode.php b/src/Ast/PhpDoc/TypeAliasTagValueNode.php index 4ccaaac4..91f40683 100644 --- a/src/Ast/PhpDoc/TypeAliasTagValueNode.php +++ b/src/Ast/PhpDoc/TypeAliasTagValueNode.php @@ -4,6 +4,8 @@ use PHPStan\PhpDocParser\Ast\NodeAttributes; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use function count; +use function implode; use function trim; class TypeAliasTagValueNode implements PhpDocTagValueNode @@ -17,16 +19,31 @@ class TypeAliasTagValueNode implements PhpDocTagValueNode /** @var TypeNode */ public $type; - public function __construct(string $alias, TypeNode $type) + /** @var array */ + public $typeArguments; + + /** + * @param array $typeArguments + */ + public function __construct(string $alias, TypeNode $type, array $typeArguments = []) { $this->alias = $alias; $this->type = $type; + $this->typeArguments = $typeArguments; } public function __toString(): string { - return trim("{$this->alias} {$this->type}"); + $args = ''; + if (count($this->typeArguments) > 0) { + $printedArgs = []; + foreach ($this->typeArguments as $name => $bound) { + $printedArgs[] = $name . ($bound === null ? '' : ' of ' . $bound); + } + $args = '<' . implode(', ', $printedArgs) . '>'; + } + return trim("{$this->alias}{$args} {$this->type}"); } } diff --git a/src/Parser/PhpDocParser.php b/src/Parser/PhpDocParser.php index e87d92c4..6d442ef2 100644 --- a/src/Parser/PhpDocParser.php +++ b/src/Parser/PhpDocParser.php @@ -1061,6 +1061,25 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA $alias = $tokens->currentTokenValue(); $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + $typeArguments = []; + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { + while ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { + $name = $tokens->currentTokenValue(); + $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); + if ($tokens->tryConsumeTokenValue('of')) { + $bound = $this->typeParser->parse($tokens); + } else { + $bound = null; + } + $typeArguments[$name] = $bound; + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + continue; + } + break; + } + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + } + // support psalm-type syntax $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); @@ -1082,19 +1101,20 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA } } - return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); + return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type, $typeArguments); } catch (ParserException $e) { $this->parseOptionalDescription($tokens); return new Ast\PhpDoc\TypeAliasTagValueNode( $alias, - $this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex) + $this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex), + $typeArguments ); } } $type = $this->typeParser->parse($tokens); - return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type); + return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type, $typeArguments); } private function parseTypeAliasImportTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeAliasImportTagValueNode diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 0093e6ca..9e45e3b2 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -331,8 +331,16 @@ private function printTagValue(PhpDocTagValueNode $node): string ); } if ($node instanceof TypeAliasTagValueNode) { + $args = ''; + if (count($node->typeArguments) > 0) { + $printedArgs = []; + foreach ($node->typeArguments as $name => $bound) { + $printedArgs[] = $name . ($bound === null ? '' : (' of ' . $this->printType($bound))); + } + $args = '<' . implode(', ', $printedArgs) . '>'; + } $type = $this->printType($node->type); - return trim("{$node->alias} {$type}"); + return trim("{$node->alias}{$args} {$type}"); } if ($node instanceof UsesTagValueNode) { $type = $this->printType($node->type); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index 67a9d123..f37dbd90 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -4470,6 +4470,60 @@ public function provideTypeAliasTagsData(): Iterator ), ]), ]; + + yield [ + 'Type argument', + '/** @phpstan-type TypeAlias callable(T): T */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'TypeAlias', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('T'), + false, + false, + '', + false + ) + ], + new IdentifierTypeNode('T') + ), + ['T' => null] + ) + ), + ]), + ]; + + yield [ + 'Bound type argument', + '/** @phpstan-type TypeAlias callable(T): T */', + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'TypeAlias', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('T'), + false, + false, + '', + false + ) + ], + new IdentifierTypeNode('T') + ), + ['T' => new IdentifierTypeNode('string')] + ) + ), + ]), + ]; } public function provideTypeAliasImportTagsData(): Iterator diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 2307cdb2..6a0069fd 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -23,6 +23,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; @@ -1811,6 +1812,66 @@ public function dataPrintPhpDocNode(): iterable * @param int $a */', ]; + + yield [ + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'TypeAlias', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('T'), + false, + false, + '', + false + ) + ], + new IdentifierTypeNode('T') + ), + ['T' => null] + ) + ), + ]), + << callable(T): T + */ +DOC, + ]; + + yield [ + new PhpDocNode([ + new PhpDocTagNode( + '@phpstan-type', + new TypeAliasTagValueNode( + 'TypeAlias', + new CallableTypeNode( + new IdentifierTypeNode('callable'), + [ + new CallableTypeParameterNode( + new IdentifierTypeNode('T'), + false, + false, + '', + false + ) + ], + new IdentifierTypeNode('T') + ), + ['T' => new IdentifierTypeNode('string')] + ) + ), + ]), + << callable(T): T + */ +DOC, + ]; } /** From eccd31ebe1b1f62cee9cff7d76322f7e694b43d1 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Thu, 15 Feb 2024 20:46:10 +0100 Subject: [PATCH 2/2] Code style fixes --- src/Printer/Printer.php | 2 +- tests/PHPStan/Parser/PhpDocParserTest.php | 4 ++-- tests/PHPStan/Printer/PrinterTest.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 9e45e3b2..cc7d7420 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -335,7 +335,7 @@ private function printTagValue(PhpDocTagValueNode $node): string if (count($node->typeArguments) > 0) { $printedArgs = []; foreach ($node->typeArguments as $name => $bound) { - $printedArgs[] = $name . ($bound === null ? '' : (' of ' . $this->printType($bound))); + $printedArgs[] = $name . ($bound === null ? '' : ' of ' . $this->printType($bound)); } $args = '<' . implode(', ', $printedArgs) . '>'; } diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index f37dbd90..d0a7ba77 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -4488,7 +4488,7 @@ public function provideTypeAliasTagsData(): Iterator false, '', false - ) + ), ], new IdentifierTypeNode('T') ), @@ -4515,7 +4515,7 @@ public function provideTypeAliasTagsData(): Iterator false, '', false - ) + ), ], new IdentifierTypeNode('T') ), diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 6a0069fd..e9a507fc 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -1828,7 +1828,7 @@ public function dataPrintPhpDocNode(): iterable false, '', false - ) + ), ], new IdentifierTypeNode('T') ), @@ -1858,7 +1858,7 @@ public function dataPrintPhpDocNode(): iterable false, '', false - ) + ), ], new IdentifierTypeNode('T') ),