Skip to content

Commit c23674d

Browse files
authored
Parse generic callables
1 parent e7f0d8f commit c23674d

File tree

7 files changed

+267
-47
lines changed

7 files changed

+267
-47
lines changed

doc/grammars/type.abnf

+13-7
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ GenericTypeArgument
3535
/ TokenWildcard
3636

3737
Callable
38-
= TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType
38+
= [CallableTemplate] TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType
39+
40+
CallableTemplate
41+
= TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose
42+
43+
CallableTemplateArgument
44+
= TokenIdentifier [1*ByteHorizontalWs TokenOf Type]
3945

4046
CallableParameters
4147
= CallableParameter *(TokenComma CallableParameter)
@@ -192,6 +198,9 @@ TokenIs
192198
TokenNot
193199
= %s"not" 1*ByteHorizontalWs
194200

201+
TokenOf
202+
= %s"of" 1*ByteHorizontalWs
203+
195204
TokenContravariant
196205
= %s"contravariant" 1*ByteHorizontalWs
197206

@@ -211,7 +220,7 @@ TokenIdentifier
211220

212221
ByteHorizontalWs
213222
= %x09 ; horizontal tab
214-
/ %x20 ; space
223+
/ " "
215224

216225
ByteNumberSign
217226
= "+"
@@ -238,11 +247,8 @@ ByteIdentifierFirst
238247
/ %x80-FF
239248

240249
ByteIdentifierSecond
241-
= %x30-39 ; 0-9
242-
/ %x41-5A ; A-Z
243-
/ "_"
244-
/ %x61-7A ; a-z
245-
/ %x80-FF
250+
= ByteIdentifierFirst
251+
/ %x30-39 ; 0-9
246252

247253
ByteSingleQuote
248254
= %x27 ; '

src/Ast/Type/CallableTypeNode.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\PhpDocParser\Ast\Type;
44

55
use PHPStan\PhpDocParser\Ast\NodeAttributes;
6+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
67
use function implode;
78

89
class CallableTypeNode implements TypeNode
@@ -13,6 +14,9 @@ class CallableTypeNode implements TypeNode
1314
/** @var IdentifierTypeNode */
1415
public $identifier;
1516

17+
/** @var TemplateTagValueNode[] */
18+
public $templateTypes;
19+
1620
/** @var CallableTypeParameterNode[] */
1721
public $parameters;
1822

@@ -21,12 +25,14 @@ class CallableTypeNode implements TypeNode
2125

2226
/**
2327
* @param CallableTypeParameterNode[] $parameters
28+
* @param TemplateTagValueNode[] $templateTypes
2429
*/
25-
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType)
30+
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes = [])
2631
{
2732
$this->identifier = $identifier;
2833
$this->parameters = $parameters;
2934
$this->returnType = $returnType;
35+
$this->templateTypes = $templateTypes;
3036
}
3137

3238

@@ -36,8 +42,11 @@ public function __toString(): string
3642
if ($returnType instanceof self) {
3743
$returnType = "({$returnType})";
3844
}
45+
$template = $this->templateTypes !== []
46+
? '<' . implode(', ', $this->templateTypes) . '>'
47+
: '';
3948
$parameters = implode(', ', $this->parameters);
40-
return "{$this->identifier}({$parameters}): {$returnType}";
49+
return "{$this->identifier}{$template}({$parameters}): {$returnType}";
4150
}
4251

4352
}

src/Parser/PhpDocParser.php

+12-29
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,12 @@ public function parseTagValue(TokenIterator $tokens, string $tag): Ast\PhpDoc\Ph
449449
case '@template-contravariant':
450450
case '@phpstan-template-contravariant':
451451
case '@psalm-template-contravariant':
452-
$tagValue = $this->parseTemplateTagValue($tokens, true);
452+
$tagValue = $this->typeParser->parseTemplateTagValue(
453+
$tokens,
454+
function ($tokens) {
455+
return $this->parseOptionalDescription($tokens);
456+
}
457+
);
453458
break;
454459

455460
case '@extends':
@@ -947,7 +952,12 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa
947952
do {
948953
$startLine = $tokens->currentTokenLine();
949954
$startIndex = $tokens->currentTokenIndex();
950-
$templateTypes[] = $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens, false), $startLine, $startIndex);
955+
$templateTypes[] = $this->enrichWithAttributes(
956+
$tokens,
957+
$this->typeParser->parseTemplateTagValue($tokens),
958+
$startLine,
959+
$startIndex
960+
);
951961
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
952962
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
953963
}
@@ -1003,33 +1013,6 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc
10031013
);
10041014
}
10051015

1006-
private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
1007-
{
1008-
$name = $tokens->currentTokenValue();
1009-
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1010-
1011-
if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
1012-
$bound = $this->typeParser->parse($tokens);
1013-
1014-
} else {
1015-
$bound = null;
1016-
}
1017-
1018-
if ($tokens->tryConsumeTokenValue('=')) {
1019-
$default = $this->typeParser->parse($tokens);
1020-
} else {
1021-
$default = null;
1022-
}
1023-
1024-
if ($parseDescription) {
1025-
$description = $this->parseOptionalDescription($tokens);
1026-
} else {
1027-
$description = '';
1028-
}
1029-
1030-
return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
1031-
}
1032-
10331016
private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
10341017
{
10351018
$startLine = $tokens->currentTokenLine();

src/Parser/TypeParser.php

+96-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use LogicException;
66
use PHPStan\PhpDocParser\Ast;
7+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
78
use PHPStan\PhpDocParser\Lexer\Lexer;
89
use function in_array;
910
use function str_replace;
@@ -164,13 +165,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
164165
return $type;
165166
}
166167

167-
$type = $this->parseGeneric($tokens, $type);
168+
$origType = $type;
169+
$type = $this->tryParseCallable($tokens, $type, true);
170+
if ($type === $origType) {
171+
$type = $this->parseGeneric($tokens, $type);
168172

169-
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
170-
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
173+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
174+
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
175+
}
171176
}
172177
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
173-
$type = $this->tryParseCallable($tokens, $type);
178+
$type = $this->tryParseCallable($tokens, $type, false);
174179

175180
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
176181
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
@@ -464,10 +469,48 @@ public function parseGenericTypeArgument(TokenIterator $tokens): array
464469
return [$type, $variance];
465470
}
466471

472+
/**
473+
* @throws ParserException
474+
* @param ?callable(TokenIterator): string $parseDescription
475+
*/
476+
public function parseTemplateTagValue(
477+
TokenIterator $tokens,
478+
?callable $parseDescription = null
479+
): TemplateTagValueNode
480+
{
481+
$name = $tokens->currentTokenValue();
482+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
483+
484+
if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
485+
$bound = $this->parse($tokens);
486+
487+
} else {
488+
$bound = null;
489+
}
490+
491+
if ($tokens->tryConsumeTokenValue('=')) {
492+
$default = $this->parse($tokens);
493+
} else {
494+
$default = null;
495+
}
496+
497+
if ($parseDescription !== null) {
498+
$description = $parseDescription($tokens);
499+
} else {
500+
$description = '';
501+
}
502+
503+
return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
504+
}
505+
467506

468507
/** @phpstan-impure */
469-
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
508+
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
470509
{
510+
$templates = $hasTemplate
511+
? $this->parseCallableTemplates($tokens)
512+
: [];
513+
471514
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
472515
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
473516

@@ -492,7 +535,52 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod
492535
$startIndex = $tokens->currentTokenIndex();
493536
$returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex);
494537

495-
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
538+
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates);
539+
}
540+
541+
542+
/**
543+
* @return Ast\PhpDoc\TemplateTagValueNode[]
544+
*
545+
* @phpstan-impure
546+
*/
547+
private function parseCallableTemplates(TokenIterator $tokens): array
548+
{
549+
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
550+
551+
$templates = [];
552+
553+
$isFirst = true;
554+
while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
555+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
556+
557+
// trailing comma case
558+
if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
559+
break;
560+
}
561+
$isFirst = false;
562+
563+
$templates[] = $this->parseCallableTemplateArgument($tokens);
564+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
565+
}
566+
567+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
568+
569+
return $templates;
570+
}
571+
572+
573+
private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode
574+
{
575+
$startLine = $tokens->currentTokenLine();
576+
$startIndex = $tokens->currentTokenIndex();
577+
578+
return $this->enrichWithAttributes(
579+
$tokens,
580+
$this->parseTemplateTagValue($tokens),
581+
$startLine,
582+
$startIndex
583+
);
496584
}
497585

498586

@@ -670,11 +758,11 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo
670758

671759

672760
/** @phpstan-impure */
673-
private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
761+
private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
674762
{
675763
try {
676764
$tokens->pushSavePoint();
677-
$type = $this->parseCallable($tokens, $identifier);
765+
$type = $this->parseCallable($tokens, $identifier, $hasTemplate);
678766
$tokens->dropSavePoint();
679767

680768
} catch (ParserException $e) {

src/Printer/Printer.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ final class Printer
9999
ArrayShapeNode::class . '->items' => ', ',
100100
ObjectShapeNode::class . '->items' => ', ',
101101
CallableTypeNode::class . '->parameters' => ', ',
102+
CallableTypeNode::class . '->templateTypes' => ', ',
102103
GenericTypeNode::class . '->genericTypes' => ', ',
103104
ConstExprArrayNode::class . '->items' => ', ',
104105
MethodTagValueNode::class . '->parameters' => ', ',
@@ -380,10 +381,15 @@ private function printType(TypeNode $node): string
380381
} else {
381382
$returnType = $this->printType($node->returnType);
382383
}
384+
$template = $node->templateTypes !== []
385+
? '<' . implode(', ', array_map(function (TemplateTagValueNode $templateNode): string {
386+
return $this->print($templateNode);
387+
}, $node->templateTypes)) . '>'
388+
: '';
383389
$parameters = implode(', ', array_map(function (CallableTypeParameterNode $parameterNode): string {
384390
return $this->print($parameterNode);
385391
}, $node->parameters));
386-
return "{$node->identifier}({$parameters}): {$returnType}";
392+
return "{$node->identifier}{$template}({$parameters}): {$returnType}";
387393
}
388394
if ($node instanceof ConditionalTypeForParameterNode) {
389395
return sprintf(

0 commit comments

Comments
 (0)