Skip to content

Commit

Permalink
PhpDocParser: support template type lower bounds
Browse files Browse the repository at this point in the history
  • Loading branch information
jiripudil authored and ondrejmirtes committed Sep 22, 2024
1 parent a5e938b commit 249f15f
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 19 deletions.
5 changes: 4 additions & 1 deletion doc/grammars/type.abnf
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ CallableTemplate
= TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose

CallableTemplateArgument
= TokenIdentifier [1*ByteHorizontalWs TokenOf Type]
= TokenIdentifier [1*ByteHorizontalWs TokenOf Type] [1*ByteHorizontalWs TokenSuper Type] ["=" Type]

CallableParameters
= CallableParameter *(TokenComma CallableParameter)
Expand Down Expand Up @@ -201,6 +201,9 @@ TokenNot
TokenOf
= %s"of" 1*ByteHorizontalWs

TokenSuper
= %s"super" 1*ByteHorizontalWs

TokenContravariant
= %s"contravariant" 1*ByteHorizontalWs

Expand Down
11 changes: 8 additions & 3 deletions src/Ast/PhpDoc/TemplateTagValueNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class TemplateTagValueNode implements PhpDocTagValueNode
/** @var TypeNode|null */
public $bound;

/** @var TypeNode|null */
public $lowerBound;

/** @var TypeNode|null */
public $default;

Expand All @@ -26,20 +29,22 @@ class TemplateTagValueNode implements PhpDocTagValueNode
/**
* @param non-empty-string $name
*/
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null)
public function __construct(string $name, ?TypeNode $bound, string $description, ?TypeNode $default = null, ?TypeNode $lowerBound = null)
{
$this->name = $name;
$this->bound = $bound;
$this->lowerBound = $lowerBound;
$this->default = $default;
$this->description = $description;
}


public function __toString(): string
{
$bound = $this->bound !== null ? " of {$this->bound}" : '';
$upperBound = $this->bound !== null ? " of {$this->bound}" : '';
$lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : '';
$default = $this->default !== null ? " = {$this->default}" : '';
return trim("{$this->name}{$bound}{$default} {$this->description}");
return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}");
}

}
11 changes: 7 additions & 4 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,11 +491,14 @@ public function parseTemplateTagValue(
$name = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

$upperBound = $lowerBound = null;

if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
$bound = $this->parse($tokens);
$upperBound = $this->parse($tokens);
}

} else {
$bound = null;
if ($tokens->tryConsumeTokenValue('super')) {
$lowerBound = $this->parse($tokens);
}

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

return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound);
}


Expand Down
5 changes: 3 additions & 2 deletions src/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,10 @@ private function printTagValue(PhpDocTagValueNode $node): string
return trim($type . ' ' . $node->description);
}
if ($node instanceof TemplateTagValueNode) {
$bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
$upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
$lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : '';
$default = $node->default !== null ? ' = ' . $this->printType($node->default) : '';
return trim("{$node->name}{$bound}{$default} {$node->description}");
return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}");
}
if ($node instanceof ThrowsTagValueNode) {
$type = $this->printType($node->type);
Expand Down
9 changes: 6 additions & 3 deletions tests/PHPStan/Ast/ToString/PhpDocToStringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,15 @@ public static function provideOtherCases(): Generator
$baz = new IdentifierTypeNode('Foo\\Baz');

yield from [
['TValue', new TemplateTagValueNode('TValue', null, '', null)],
['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '', null)],
['TValue', new TemplateTagValueNode('TValue', null, '')],
['TValue of Foo\\Bar', new TemplateTagValueNode('TValue', $bar, '')],
['TValue super Foo\\Bar', new TemplateTagValueNode('TValue', null, '', null, $bar)],
['TValue = Foo\\Bar', new TemplateTagValueNode('TValue', null, '', $bar)],
['TValue of Foo\\Bar = Foo\\Baz', new TemplateTagValueNode('TValue', $bar, '', $baz)],
['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.', null)],
['TValue Description.', new TemplateTagValueNode('TValue', null, 'Description.')],
['TValue of Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', $baz)],
['TValue super Foo\\Bar = Foo\\Baz Description.', new TemplateTagValueNode('TValue', null, 'Description.', $baz, $bar)],
['TValue of Foo\\Bar super Foo\\Baz Description.', new TemplateTagValueNode('TValue', $bar, 'Description.', null, $baz)],
];
}

Expand Down
31 changes: 25 additions & 6 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3986,7 +3986,7 @@ public function provideTemplateTagsData(): Iterator
];

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

yield [
'OK with bound and description',
'/** @template T as DateTime the value type */',
'OK with lower bound and description',
'/** @template T super DateTimeImmutable the value type */',
new PhpDocNode([
new PhpDocTagNode(
'@template',
new TemplateTagValueNode(
'T',
new IdentifierTypeNode('DateTime'),
'the value type'
null,
'the value type',
null,
new IdentifierTypeNode('DateTimeImmutable')
)
),
]),
];

yield [
'OK with both bounds and description',
'/** @template T of DateTimeInterface super DateTimeImmutable the value type */',
new PhpDocNode([
new PhpDocTagNode(
'@template',
new TemplateTagValueNode(
'T',
new IdentifierTypeNode('DateTimeInterface'),
'the value type',
null,
new IdentifierTypeNode('DateTimeImmutable')
)
),
]),
];

yield [
'invalid without bound and description',
'invalid without bounds and description',
'/** @template */',
new PhpDocNode([
new PhpDocTagNode(
Expand Down
56 changes: 56 additions & 0 deletions tests/PHPStan/Printer/PrinterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,12 @@ public function enterNode(Node $node)
$addTemplateTagBound,
];

yield [
'/** @template T super string */',
'/** @template T of int super string */',
$addTemplateTagBound,
];

$removeTemplateTagBound = new class extends AbstractNodeVisitor {

public function enterNode(Node $node)
Expand All @@ -966,6 +972,56 @@ public function enterNode(Node $node)
$removeTemplateTagBound,
];

$addTemplateTagLowerBound = new class extends AbstractNodeVisitor {

public function enterNode(Node $node)
{
if ($node instanceof TemplateTagValueNode) {
$node->lowerBound = new IdentifierTypeNode('int');
}

return $node;
}

};

yield [
'/** @template T */',
'/** @template T super int */',
$addTemplateTagLowerBound,
];

yield [
'/** @template T super string */',
'/** @template T super int */',
$addTemplateTagLowerBound,
];

yield [
'/** @template T of string */',
'/** @template T of string super int */',
$addTemplateTagLowerBound,
];

$removeTemplateTagLowerBound = new class extends AbstractNodeVisitor {

public function enterNode(Node $node)
{
if ($node instanceof TemplateTagValueNode) {
$node->lowerBound = null;
}

return $node;
}

};

yield [
'/** @template T super int */',
'/** @template T */',
$removeTemplateTagLowerBound,
];

$addKeyNameToArrayShapeItemNode = new class extends AbstractNodeVisitor {

public function enterNode(Node $node)
Expand Down

0 comments on commit 249f15f

Please sign in to comment.