Skip to content

Commit cc479c9

Browse files
committed
Merge remote-tracking branch 'origin/1.23.x' into 2.0.x
2 parents b78c136 + 249f15f commit cc479c9

File tree

7 files changed

+109
-20
lines changed

7 files changed

+109
-20
lines changed

Diff for: 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

Diff for: src/Ast/PhpDoc/TemplateTagValueNode.php

+9-5
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,34 @@ class TemplateTagValueNode implements PhpDocTagValueNode
1414
/** @var non-empty-string */
1515
public string $name;
1616

17-
public ?TypeNode $bound = null;
17+
public ?TypeNode $bound;
1818

19-
public ?TypeNode $default = null;
19+
public ?TypeNode $default;
20+
21+
public ?TypeNode $lowerBound;
2022

2123
/** @var string (may be empty) */
2224
public string $description;
2325

2426
/**
2527
* @param non-empty-string $name
2628
*/
27-
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null)
29+
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null)
2830
{
2931
$this->name = $name;
3032
$this->bound = $bound;
33+
$this->lowerBound = $lowerBound;
3134
$this->default = $default;
3235
$this->description = $description;
3336
}
3437

3538

3639
public function __toString(): string
3740
{
38-
$bound = $this->bound !== null ? " of {$this->bound}" : '';
41+
$upperBound = $this->bound !== null ? " of {$this->bound}" : '';
42+
$lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : '';
3943
$default = $this->default !== null ? " = {$this->default}" : '';
40-
return trim("{$this->name}{$bound}{$default} {$this->description}");
44+
return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}");
4145
}
4246

4347
}

Diff for: src/Parser/TypeParser.php

+7-4
Original file line numberDiff line numberDiff line change
@@ -466,11 +466,14 @@ public function parseTemplateTagValue(
466466
$name = $tokens->currentTokenValue();
467467
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
468468

469+
$upperBound = $lowerBound = null;
470+
469471
if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
470-
$bound = $this->parse($tokens);
472+
$upperBound = $this->parse($tokens);
473+
}
471474

472-
} else {
473-
$bound = null;
475+
if ($tokens->tryConsumeTokenValue('super')) {
476+
$lowerBound = $this->parse($tokens);
474477
}
475478

476479
if ($tokens->tryConsumeTokenValue('=')) {
@@ -489,7 +492,7 @@ public function parseTemplateTagValue(
489492
throw new LogicException('Template tag name cannot be empty.');
490493
}
491494

492-
return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
495+
return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound);
493496
}
494497

495498

Diff for: src/Printer/Printer.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,10 @@ private function printTagValue(PhpDocTagValueNode $node): string
355355
return trim($type . ' ' . $node->description);
356356
}
357357
if ($node instanceof TemplateTagValueNode) {
358-
$bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
358+
$upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
359+
$lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : '';
359360
$default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
360-
return trim("{$node->name}{$bound}{$default} {$node->description}");
361+
return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}");
361362
}
362363
if ($node instanceof ThrowsTagValueNode) {
363364
$type = $this->printType($node->type);

Diff for: 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

Diff for: tests/PHPStan/Parser/PhpDocParserTest.php

+24-5
Original file line numberDiff line numberDiff line change
@@ -4058,7 +4058,7 @@ public function provideTemplateTagsData(): Iterator
40584058
];
40594059

40604060
yield [
4061-
'OK with bound and description',
4061+
'OK with upper bound and description',
40624062
'/** @template T of DateTime the value type */',
40634063
new PhpDocNode([
40644064
new PhpDocTagNode(
@@ -4073,22 +4073,41 @@ public function provideTemplateTagsData(): Iterator
40734073
];
40744074

40754075
yield [
4076-
'OK with bound and description',
4077-
'/** @template T as DateTime the value type */',
4076+
'OK with lower bound and description',
4077+
'/** @template T super DateTimeImmutable the value type */',
40784078
new PhpDocNode([
40794079
new PhpDocTagNode(
40804080
'@template',
40814081
new TemplateTagValueNode(
40824082
'T',
4083-
new IdentifierTypeNode('DateTime'),
4083+
null,
4084+
'the value type',
4085+
null,
4086+
new IdentifierTypeNode('DateTimeImmutable'),
4087+
),
4088+
),
4089+
]),
4090+
];
4091+
4092+
yield [
4093+
'OK with both bounds and description',
4094+
'/** @template T of DateTimeInterface super DateTimeImmutable the value type */',
4095+
new PhpDocNode([
4096+
new PhpDocTagNode(
4097+
'@template',
4098+
new TemplateTagValueNode(
4099+
'T',
4100+
new IdentifierTypeNode('DateTimeInterface'),
40844101
'the value type',
4102+
null,
4103+
new IdentifierTypeNode('DateTimeImmutable'),
40854104
),
40864105
),
40874106
]),
40884107
];
40894108

40904109
yield [
4091-
'invalid without bound and description',
4110+
'invalid without bounds and description',
40924111
'/** @template */',
40934112
new PhpDocNode([
40944113
new PhpDocTagNode(

Diff for: tests/PHPStan/Printer/PrinterTest.php

+56
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,12 @@ public function enterNode(Node $node)
943943
$addTemplateTagBound,
944944
];
945945

946+
yield [
947+
'/** @template T super string */',
948+
'/** @template T of int super string */',
949+
$addTemplateTagBound,
950+
];
951+
946952
$removeTemplateTagBound = new class extends AbstractNodeVisitor {
947953

948954
public function enterNode(Node $node)
@@ -962,6 +968,56 @@ public function enterNode(Node $node)
962968
$removeTemplateTagBound,
963969
];
964970

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

9671023
public function enterNode(Node $node)

0 commit comments

Comments
 (0)