Skip to content

Commit 249f15f

Browse files
jiripudilondrejmirtes
authored andcommitted
PhpDocParser: support template type lower bounds
1 parent a5e938b commit 249f15f

File tree

7 files changed

+109
-19
lines changed

7 files changed

+109
-19
lines changed

doc/grammars/type.abnf

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ CallableTemplate
4141
= TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose
4242

4343
CallableTemplateArgument
44-
= TokenIdentifier [1*ByteHorizontalWs TokenOf Type]
44+
= TokenIdentifier [1*ByteHorizontalWs TokenOf Type] [1*ByteHorizontalWs TokenSuper Type] ["=" Type]
4545

4646
CallableParameters
4747
= CallableParameter *(TokenComma CallableParameter)
@@ -201,6 +201,9 @@ TokenNot
201201
TokenOf
202202
= %s"of" 1*ByteHorizontalWs
203203

204+
TokenSuper
205+
= %s"super" 1*ByteHorizontalWs
206+
204207
TokenContravariant
205208
= %s"contravariant" 1*ByteHorizontalWs
206209

src/Ast/PhpDoc/TemplateTagValueNode.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class TemplateTagValueNode implements PhpDocTagValueNode
1717
/** @var TypeNode|null */
1818
public $bound;
1919

20+
/** @var TypeNode|null */
21+
public $lowerBound;
22+
2023
/** @var TypeNode|null */
2124
public $default;
2225

@@ -26,20 +29,22 @@ class TemplateTagValueNode implements PhpDocTagValueNode
2629
/**
2730
* @param non-empty-string $name
2831
*/
29-
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null)
32+
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null)
3033
{
3134
$this->name = $name;
3235
$this->bound = $bound;
36+
$this->lowerBound = $lowerBound;
3337
$this->default = $default;
3438
$this->description = $description;
3539
}
3640

3741

3842
public function __toString(): string
3943
{
40-
$bound = $this->bound !== null ? " of {$this->bound}" : '';
44+
$upperBound = $this->bound !== null ? " of {$this->bound}" : '';
45+
$lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : '';
4146
$default = $this->default !== null ? " = {$this->default}" : '';
42-
return trim("{$this->name}{$bound}{$default} {$this->description}");
47+
return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}");
4348
}
4449

4550
}

src/Parser/TypeParser.php

+7-4
Original file line numberDiff line numberDiff line change
@@ -491,11 +491,14 @@ public function parseTemplateTagValue(
491491
$name = $tokens->currentTokenValue();
492492
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
493493

494+
$upperBound = $lowerBound = null;
495+
494496
if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
495-
$bound = $this->parse($tokens);
497+
$upperBound = $this->parse($tokens);
498+
}
496499

497-
} else {
498-
$bound = null;
500+
if ($tokens->tryConsumeTokenValue('super')) {
501+
$lowerBound = $this->parse($tokens);
499502
}
500503

501504
if ($tokens->tryConsumeTokenValue('=')) {
@@ -514,7 +517,7 @@ public function parseTemplateTagValue(
514517
throw new LogicException('Template tag name cannot be empty.');
515518
}
516519

517-
return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
520+
return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound);
518521
}
519522

520523

src/Printer/Printer.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,10 @@ private function printTagValue(PhpDocTagValueNode $node): string
335335
return trim($type . ' ' . $node->description);
336336
}
337337
if ($node instanceof TemplateTagValueNode) {
338-
$bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
338+
$upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
339+
$lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : '';
339340
$default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
340-
return trim("{$node->name}{$bound}{$default} {$node->description}");
341+
return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}");
341342
}
342343
if ($node instanceof ThrowsTagValueNode) {
343344
$type = $this->printType($node->type);

tests/PHPStan/Ast/ToString/PhpDocToStringTest.php

+6-3
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,15 @@ public static function provideOtherCases(): Generator
155155
$baz = new IdentifierTypeNode('Foo\\Baz');
156156

157157
yield from [
158-
['TValue', new TemplateTagValueNode('TValue', null, '', null)],
159-
['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '', null)],
158+
['TValue', new TemplateTagValueNode('TValue', null, '')],
159+
['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '')],
160+
['TValue super Foo\\Bar', new TemplateTagValueNode('TValue', null, '', null, $bar)],
160161
['TValue = Foo\\Bar', new TemplateTagValueNode('TValue', null, '', $bar)],
161162
['TValue of Foo\\Bar = Foo\\Baz', new TemplateTagValueNode('TValue', $bar, '', $baz)],
162-
['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.', null)],
163+
['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.')],
163164
['TValue of Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', $baz)],
165+
['TValue super Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', null, 'Description.', $baz, $bar)],
166+
['TValue of Foo\\Bar super Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', null, $baz)],
164167
];
165168
}
166169

tests/PHPStan/Parser/PhpDocParserTest.php

+25-6
Original file line numberDiff line numberDiff line change
@@ -3986,7 +3986,7 @@ public function provideTemplateTagsData(): Iterator
39863986
];
39873987

39883988
yield [
3989-
'OK with bound and description',
3989+
'OK with upper bound and description',
39903990
'/** @template T of DateTime the value type */',
39913991
new PhpDocNode([
39923992
new PhpDocTagNode(
@@ -4001,22 +4001,41 @@ public function provideTemplateTagsData(): Iterator
40014001
];
40024002

40034003
yield [
4004-
'OK with bound and description',
4005-
'/** @template T as DateTime the value type */',
4004+
'OK with lower bound and description',
4005+
'/** @template T super DateTimeImmutable the value type */',
40064006
new PhpDocNode([
40074007
new PhpDocTagNode(
40084008
'@template',
40094009
new TemplateTagValueNode(
40104010
'T',
4011-
new IdentifierTypeNode('DateTime'),
4012-
'the value type'
4011+
null,
4012+
'the value type',
4013+
null,
4014+
new IdentifierTypeNode('DateTimeImmutable')
4015+
)
4016+
),
4017+
]),
4018+
];
4019+
4020+
yield [
4021+
'OK with both bounds and description',
4022+
'/** @template T of DateTimeInterface super DateTimeImmutable the value type */',
4023+
new PhpDocNode([
4024+
new PhpDocTagNode(
4025+
'@template',
4026+
new TemplateTagValueNode(
4027+
'T',
4028+
new IdentifierTypeNode('DateTimeInterface'),
4029+
'the value type',
4030+
null,
4031+
new IdentifierTypeNode('DateTimeImmutable')
40134032
)
40144033
),
40154034
]),
40164035
];
40174036

40184037
yield [
4019-
'invalid without bound and description',
4038+
'invalid without bounds and description',
40204039
'/** @template */',
40214040
new PhpDocNode([
40224041
new PhpDocTagNode(

tests/PHPStan/Printer/PrinterTest.php

+56
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,12 @@ public function enterNode(Node $node)
947947
$addTemplateTagBound,
948948
];
949949

950+
yield [
951+
'/** @template T super string */',
952+
'/** @template T of int super string */',
953+
$addTemplateTagBound,
954+
];
955+
950956
$removeTemplateTagBound = new class extends AbstractNodeVisitor {
951957

952958
public function enterNode(Node $node)
@@ -966,6 +972,56 @@ public function enterNode(Node $node)
966972
$removeTemplateTagBound,
967973
];
968974

975+
$addTemplateTagLowerBound = new class extends AbstractNodeVisitor {
976+
977+
public function enterNode(Node $node)
978+
{
979+
if ($node instanceof TemplateTagValueNode) {
980+
$node->lowerBound = new IdentifierTypeNode('int');
981+
}
982+
983+
return $node;
984+
}
985+
986+
};
987+
988+
yield [
989+
'/** @template T */',
990+
'/** @template T super int */',
991+
$addTemplateTagLowerBound,
992+
];
993+
994+
yield [
995+
'/** @template T super string */',
996+
'/** @template T super int */',
997+
$addTemplateTagLowerBound,
998+
];
999+
1000+
yield [
1001+
'/** @template T of string */',
1002+
'/** @template T of string super int */',
1003+
$addTemplateTagLowerBound,
1004+
];
1005+
1006+
$removeTemplateTagLowerBound = new class extends AbstractNodeVisitor {
1007+
1008+
public function enterNode(Node $node)
1009+
{
1010+
if ($node instanceof TemplateTagValueNode) {
1011+
$node->lowerBound = null;
1012+
}
1013+
1014+
return $node;
1015+
}
1016+
1017+
};
1018+
1019+
yield [
1020+
'/** @template T super int */',
1021+
'/** @template T */',
1022+
$removeTemplateTagLowerBound,
1023+
];
1024+
9691025
$addKeyNameToArrayShapeItemNode = new class extends AbstractNodeVisitor {
9701026

9711027
public function enterNode(Node $node)

0 commit comments

Comments
 (0)