Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Lines changed: 4 additions & 1 deletion
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*ByteHorizontalWsTokenSuperType] ["="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

Lines changed: 8 additions & 3 deletions
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

Lines changed: 7 additions & 4 deletions
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

Lines changed: 3 additions & 2 deletions
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

Lines changed: 6 additions & 3 deletions
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

Lines changed: 25 additions & 6 deletions
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

Lines changed: 56 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /