From 0eb13830b5181fce4bee17296c9fadc322608dd3 Mon Sep 17 00:00:00 2001
From: Rudolph Gottesheim <r.gottesheim@midnight-design.at>
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<string, TypeNode|null> */
+	public $typeArguments;
+
+	/**
+	 * @param array<string, TypeNode|null> $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<T> 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<T of string> 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]
+					)
+				),
+			]),
+			<<<DOC
+/**
+ * @phpstan-type TypeAlias<T> 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')]
+					)
+				),
+			]),
+			<<<DOC
+/**
+ * @phpstan-type TypeAlias<T of string> callable(T): T
+ */
+DOC,
+		];
 	}
 
 	/**

From eccd31ebe1b1f62cee9cff7d76322f7e694b43d1 Mon Sep 17 00:00:00 2001
From: Rudolph Gottesheim <r.gottesheim@midnight-design.at>
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')
 						),