Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PhpDocParser: support template type lower bounds #254

Merged
merged 1 commit into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading