Skip to content

Commit

Permalink
Add generics support to @method definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
tyabu12 committed Dec 7, 2022
1 parent c7c2609 commit 2a4686e
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 7 deletions.
10 changes: 8 additions & 2 deletions src/Ast/PhpDoc/MethodTagValueNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use function count;
use function implode;

class MethodTagValueNode implements PhpDocTagValueNode
Expand All @@ -20,19 +21,23 @@ class MethodTagValueNode implements PhpDocTagValueNode
/** @var string */
public $methodName;

/** @var TemplateTagValueNode[] */
public $templateTypes;

/** @var MethodTagValueParameterNode[] */
public $parameters;

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

public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description)
public function __construct(bool $isStatic, ?TypeNode $returnType, string $methodName, array $parameters, string $description, array $templateTypes = [])
{
$this->isStatic = $isStatic;
$this->returnType = $returnType;
$this->methodName = $methodName;
$this->parameters = $parameters;
$this->description = $description;
$this->templateTypes = $templateTypes;
}


Expand All @@ -42,7 +47,8 @@ public function __toString(): string
$returnType = $this->returnType !== null ? "{$this->returnType} " : '';
$parameters = implode(', ', $this->parameters);
$description = $this->description !== '' ? " {$this->description}" : '';
return "{$static}{$returnType}{$this->methodName}({$parameters}){$description}";
$templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : '';
return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}";
}

}
21 changes: 16 additions & 5 deletions src/Parser/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph
case '@template-contravariant':
case '@phpstan-template-contravariant':
case '@psalm-template-contravariant':
$tagValue = $this->parseTemplateTagValue($tokens);
$tagValue = $this->parseTemplateTagValue($tokens, true);
break;

case '@extends':
Expand Down Expand Up @@ -346,6 +346,14 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa
exit;
}

$templateTypes = [];
if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
do {
$templateTypes[] = $this->parseTemplateTagValue($tokens, false);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
}

$parameters = [];
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
Expand All @@ -357,10 +365,9 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);

$description = $this->parseOptionalDescription($tokens);
return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description);
return new Ast\PhpDoc\MethodTagValueNode($isStatic, $returnType, $methodName, $parameters, $description, $templateTypes);
}


private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc\MethodTagValueParameterNode
{
switch ($tokens->currentTokenType()) {
Expand Down Expand Up @@ -390,7 +397,7 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc
return new Ast\PhpDoc\MethodTagValueParameterNode($parameterType, $isReference, $isVariadic, $parameterName, $defaultValue);
}

private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode
private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
{
$name = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
Expand All @@ -408,7 +415,11 @@ private function parseTemplateTagValue(TokenIterator $tokens): Ast\PhpDoc\Templa
$default = null;
}

$description = $this->parseOptionalDescription($tokens);
if ($parseDescription) {
$description = $this->parseOptionalDescription($tokens);
} else {
$description = '';
}

return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
}
Expand Down
133 changes: 133 additions & 0 deletions tests/PHPStan/Parser/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\PhpDocParser\Parser;

use Iterator;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayItemNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
Expand Down Expand Up @@ -44,6 +45,7 @@
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
Expand Down Expand Up @@ -2201,6 +2203,98 @@ public function provideMethodTagsData(): Iterator
),
]),
];

yield [
'OK non-static, with return type and parameter with generic type',
'/** @method ?T randomElement<T = string>(array<array-key, T> $array = [\'a\', \'b\']) */',
new PhpDocNode([
new PhpDocTagNode(
'@method',
new MethodTagValueNode(
false,
new NullableTypeNode(new IdentifierTypeNode('T')),
'randomElement',
[
new MethodTagValueParameterNode(
new GenericTypeNode(
new IdentifierTypeNode('array'),
[
new IdentifierTypeNode('array-key'),
new IdentifierTypeNode('T'),
]
),
false,
false,
'$array',
new ConstExprArrayNode([
new ConstExprArrayItemNode(
null,
new ConstExprStringNode('\'a\'')
),
new ConstExprArrayItemNode(
null,
new ConstExprStringNode('\'b\'')
),
])
),
],
'',
[
new TemplateTagValueNode(
'T',
null,
'',
new IdentifierTypeNode('string')
),
]
)
),
]),
];

yield [
'OK static, with return type and multiple parameters with generic type',
'/** @method static bool compare<T1, T2 of Bar, T3 as Baz>(T1 $t1, T2 $t2, T3 $t3) */',
new PhpDocNode([
new PhpDocTagNode(
'@method',
new MethodTagValueNode(
true,
new IdentifierTypeNode('bool'),
'compare',
[
new MethodTagValueParameterNode(
new IdentifierTypeNode('T1'),
false,
false,
'$t1',
null
),
new MethodTagValueParameterNode(
new IdentifierTypeNode('T2'),
false,
false,
'$t2',
null
),
new MethodTagValueParameterNode(
new IdentifierTypeNode('T3'),
false,
false,
'$t3',
null
),
],
'',
[
new TemplateTagValueNode('T1', null, ''),
new TemplateTagValueNode('T2', new IdentifierTypeNode('Bar'), ''),
new TemplateTagValueNode('T3', new IdentifierTypeNode('Baz'), ''),
]
)
),
]),
];
}


Expand Down Expand Up @@ -3078,6 +3172,45 @@ public function provideMultiLinePhpDocData(): array
),
]),
],
[
'OK with template method',
'/**
* @template TKey as array-key
* @template TValue
* @method TKey|null find(TValue $v) find index of $v
*/',
new PhpDocNode([
new PhpDocTagNode(
'@template',
new TemplateTagValueNode('TKey', new IdentifierTypeNode('array-key'), '')
),
new PhpDocTagNode(
'@template',
new TemplateTagValueNode('TValue', null, '')
),
new PhpDocTagNode(
'@method',
new MethodTagValueNode(
false,
new UnionTypeNode([
new IdentifierTypeNode('TKey'),
new IdentifierTypeNode('null'),
]),
'find',
[
new MethodTagValueParameterNode(
new IdentifierTypeNode('TValue'),
false,
false,
'$v',
null
),
],
'find index of $v'
)
),
]),
],
[
'OK with multiline conditional return type',
'/**
Expand Down

0 comments on commit 2a4686e

Please sign in to comment.