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..cc7d7420 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..d0a7ba77 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..e9a507fc 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, + ]; } /**