Skip to content

Commit 6c43ad2

Browse files
committed
support for generic params on alias types
1 parent 4d4ad14 commit 6c43ad2

File tree

5 files changed

+119
-12
lines changed

5 files changed

+119
-12
lines changed

src/Ast/PhpDoc/TypeAliasTagValueNode.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\PhpDocParser\Ast\NodeAttributes;
66
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
7+
use function implode;
78
use function trim;
89

910
class TypeAliasTagValueNode implements PhpDocTagValueNode
@@ -15,23 +16,33 @@ class TypeAliasTagValueNode implements PhpDocTagValueNode
1516

1617
public TypeNode $type;
1718

18-
public function __construct(string $alias, TypeNode $type)
19+
/** @var TemplateTagValueNode[] */
20+
public array $templateTypes;
21+
22+
/**
23+
* @param TemplateTagValueNode[] $templateTypes
24+
*/
25+
public function __construct(string $alias, TypeNode $type, array $templateTypes = [])
1926
{
2027
$this->alias = $alias;
2128
$this->type = $type;
29+
$this->templateTypes = $templateTypes;
2230
}
2331

2432
public function __toString(): string
2533
{
26-
return trim("{$this->alias} {$this->type}");
34+
$templateTypes = $this->templateTypes !== []
35+
? '<' . implode(', ', $this->templateTypes) . '>'
36+
: '';
37+
return trim("{$this->alias}{$templateTypes} {$this->type}");
2738
}
2839

2940
/**
3041
* @param array<string, mixed> $properties
3142
*/
3243
public static function __set_state(array $properties): self
3344
{
34-
$instance = new self($properties['alias'], $properties['type']);
45+
$instance = new self($properties['alias'], $properties['type'], $properties['templateTypes'] ?? []);
3546
if (isset($properties['attributes'])) {
3647
foreach ($properties['attributes'] as $key => $value) {
3748
$instance->setAttribute($key, $value);

src/Parser/PhpDocParser.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,21 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA
10671067
$alias = $tokens->currentTokenValue();
10681068
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
10691069

1070+
$templateTypes = [];
1071+
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
1072+
do {
1073+
$startLine = $tokens->currentTokenLine();
1074+
$startIndex = $tokens->currentTokenIndex();
1075+
$templateTypes[] = $this->enrichWithAttributes(
1076+
$tokens,
1077+
$this->typeParser->parseTemplateTagValue($tokens),
1078+
$startLine,
1079+
$startIndex,
1080+
);
1081+
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
1082+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
1083+
}
1084+
10701085
// support phan-type/psalm-type syntax
10711086
$tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
10721087

@@ -1087,12 +1102,13 @@ private function parseTypeAliasTagValue(TokenIterator $tokens): Ast\PhpDoc\TypeA
10871102
}
10881103
}
10891104

1090-
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type);
1105+
return new Ast\PhpDoc\TypeAliasTagValueNode($alias, $type, $templateTypes);
10911106
} catch (ParserException $e) {
10921107
$this->parseOptionalDescription($tokens, false);
10931108
return new Ast\PhpDoc\TypeAliasTagValueNode(
10941109
$alias,
10951110
$this->enrichWithAttributes($tokens, new Ast\Type\InvalidTypeNode($e), $startLine, $startIndex),
1111+
$templateTypes,
10961112
);
10971113
}
10981114
}

src/Printer/Printer.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,11 @@ private function printTagValue(PhpDocTagValueNode $node): string
383383
);
384384
}
385385
if ($node instanceof TypeAliasTagValueNode) {
386+
$templateTypes = $node->templateTypes !== []
387+
? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>'
388+
: '';
386389
$type = $this->printType($node->type);
387-
return trim("{$node->alias} {$type}");
390+
return trim("{$node->alias}{$templateTypes} {$type}");
388391
}
389392
if ($node instanceof UsesTagValueNode) {
390393
$type = $this->printType($node->type);

tests/PHPStan/Ast/ToString/PhpDocToStringTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ public static function provideClassCases(): Generator
229229

230230
yield from [
231231
['Foo array<string>', new TypeAliasTagValueNode('Foo', $arrayOfStrings)],
232+
['Wrapper<T> T', new TypeAliasTagValueNode('Wrapper', new IdentifierTypeNode('T'), [new TemplateTagValueNode('T', null, '')])],
233+
['Pair<TFirst, TSecond> TFirst', new TypeAliasTagValueNode('Pair', new IdentifierTypeNode('TFirst'), [new TemplateTagValueNode('TFirst', null, ''), new TemplateTagValueNode('TSecond', null, '')])],
234+
['Collection<T of object> array<string>', new TypeAliasTagValueNode('Collection', $arrayOfStrings, [new TemplateTagValueNode('T', new IdentifierTypeNode('object'), '')])],
235+
['WithDefault<T = string> T', new TypeAliasTagValueNode('WithDefault', new IdentifierTypeNode('T'), [new TemplateTagValueNode('T', null, '', new IdentifierTypeNode('string'))])],
232236
['Test from Foo\Bar', new TypeAliasImportTagValueNode('Test', $bar, null)],
233237
['Test from Foo\Bar as Foo', new TypeAliasImportTagValueNode('Test', $bar, 'Foo')],
234238
];

tests/PHPStan/Parser/PhpDocParserTest.php

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5145,16 +5145,89 @@ public function provideTypeAliasTagsData(): Iterator
51455145
Lexer::TOKEN_CLOSE_PHPDOC,
51465146
18,
51475147
Lexer::TOKEN_IDENTIFIER,
5148-
null,
5149-
1,
5150-
),
5148+
null,
5149+
1,
51515150
),
51525151
),
5153-
]),
5154-
];
5155-
}
5152+
),
5153+
]),
5154+
];
5155+
5156+
yield [
5157+
'OK with one template type',
5158+
'/** @phpstan-type Wrapper<T> T */',
5159+
new PhpDocNode([
5160+
new PhpDocTagNode(
5161+
'@phpstan-type',
5162+
new TypeAliasTagValueNode(
5163+
'Wrapper',
5164+
new IdentifierTypeNode('T'),
5165+
[
5166+
new TemplateTagValueNode('T', null, ''),
5167+
],
5168+
),
5169+
),
5170+
]),
5171+
];
5172+
5173+
yield [
5174+
'OK with two template types',
5175+
'/** @phpstan-type Pair<TFirst, TSecond> TFirst */',
5176+
new PhpDocNode([
5177+
new PhpDocTagNode(
5178+
'@phpstan-type',
5179+
new TypeAliasTagValueNode(
5180+
'Pair',
5181+
new IdentifierTypeNode('TFirst'),
5182+
[
5183+
new TemplateTagValueNode('TFirst', null, ''),
5184+
new TemplateTagValueNode('TSecond', null, ''),
5185+
],
5186+
),
5187+
),
5188+
]),
5189+
];
5190+
5191+
yield [
5192+
'OK with bounded template type',
5193+
'/** @phpstan-type Collection<T of object> list<T> */',
5194+
new PhpDocNode([
5195+
new PhpDocTagNode(
5196+
'@phpstan-type',
5197+
new TypeAliasTagValueNode(
5198+
'Collection',
5199+
new GenericTypeNode(
5200+
new IdentifierTypeNode('list'),
5201+
[new IdentifierTypeNode('T')],
5202+
[GenericTypeNode::VARIANCE_INVARIANT],
5203+
),
5204+
[
5205+
new TemplateTagValueNode('T', new IdentifierTypeNode('object'), ''),
5206+
],
5207+
),
5208+
),
5209+
]),
5210+
];
5211+
5212+
yield [
5213+
'OK with default template type',
5214+
'/** @phpstan-type WithDefault<T = string> T */',
5215+
new PhpDocNode([
5216+
new PhpDocTagNode(
5217+
'@phpstan-type',
5218+
new TypeAliasTagValueNode(
5219+
'WithDefault',
5220+
new IdentifierTypeNode('T'),
5221+
[
5222+
new TemplateTagValueNode('T', null, '', new IdentifierTypeNode('string')),
5223+
],
5224+
),
5225+
),
5226+
]),
5227+
];
5228+
}
51565229

5157-
public function provideTypeAliasImportTagsData(): Iterator
5230+
public function provideTypeAliasImportTagsData(): Iterator
51585231
{
51595232
yield [
51605233
'OK',

0 commit comments

Comments
 (0)