Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit b278de7

Browse files
committed
Add callable template support
1 parent c2b8bbf commit b278de7

File tree

7 files changed

+273
-18
lines changed

7 files changed

+273
-18
lines changed

‎doc/grammars/type.abnf

Lines changed: 13 additions & 7 deletions
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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ class CallableTypeNode implements TypeNode
1313
/** @var IdentifierTypeNode */
1414
public $identifier;
1515

16+
/** @var CallableTypeTemplateNode[] */
17+
public $templates;
18+
1619
/** @var CallableTypeParameterNode[] */
1720
public $parameters;
1821

@@ -21,12 +24,14 @@ class CallableTypeNode implements TypeNode
2124

2225
/**
2326
* @param CallableTypeParameterNode[] $parameters
27+
* @param CallableTypeTemplateNode[] $templates
2428
*/
25-
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType)
29+
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array$templates = [])
2630
{
2731
$this->identifier = $identifier;
2832
$this->parameters = $parameters;
2933
$this->returnType = $returnType;
34+
$this->templates = $templates;
3035
}
3136

3237

@@ -36,8 +41,11 @@ public function __toString(): string
3641
if ($returnType instanceof self) {
3742
$returnType = "({$returnType})";
3843
}
44+
$template = $this->templates !== []
45+
? '<' . implode(', ', $this->templates) . '>'
46+
: '';
3947
$parameters = implode(', ', $this->parameters);
40-
return "{$this->identifier}({$parameters}): {$returnType}";
48+
return "{$this->identifier}{$template}({$parameters}): {$returnType}";
4149
}
4250

4351
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDocParser\Ast\Type;
4+
5+
use PHPStan\PhpDocParser\Ast\Node;
6+
use PHPStan\PhpDocParser\Ast\NodeAttributes;
7+
8+
class CallableTypeTemplateNode implements Node
9+
{
10+
11+
use NodeAttributes;
12+
13+
/** @var IdentifierTypeNode */
14+
public $identifier;
15+
16+
/** @var TypeNode|null */
17+
public $bound;
18+
19+
public function __construct(IdentifierTypeNode $identifier, ?TypeNode $bound)
20+
{
21+
$this->identifier = $identifier;
22+
$this->bound = $bound;
23+
}
24+
25+
public function __toString(): string
26+
{
27+
$res = (string) $this->identifier;
28+
if ($this->bound !== null) {
29+
$res .= ' of ' . $this->bound;
30+
}
31+
32+
return $res;
33+
}
34+
35+
}

‎src/Parser/TypeParser.php

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
162162
return $type;
163163
}
164164

165-
$type = $this->parseGeneric($tokens, $type);
165+
$origType = $type;
166+
$type = $this->tryParseCallable($tokens, $type, true);
167+
if ($type === $origType) {
168+
$type = $this->parseGeneric($tokens, $type);
166169

167-
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
168-
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
170+
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
171+
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
172+
}
169173
}
170174
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
171-
$type = $this->tryParseCallable($tokens, $type);
175+
$type = $this->tryParseCallable($tokens, $type, false);
172176

173177
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
174178
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
@@ -458,8 +462,12 @@ public function parseGenericTypeArgument(TokenIterator $tokens): array
458462

459463

460464
/** @phpstan-impure */
461-
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
465+
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool$hasTemplate): Ast\Type\TypeNode
462466
{
467+
$templates = $hasTemplate
468+
? $this->parseCallableTemplates($tokens)
469+
: [];
470+
463471
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
464472
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
465473

@@ -484,7 +492,65 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod
484492
$startIndex = $tokens->currentTokenIndex();
485493
$returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex);
486494

487-
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
495+
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates);
496+
}
497+
498+
499+
/**
500+
* @return Ast\Type\CallableTypeTemplateNode[]
501+
*
502+
* @phpstan-impure
503+
*/
504+
private function parseCallableTemplates(TokenIterator $tokens): array
505+
{
506+
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
507+
508+
$templates = [];
509+
510+
$isFirst = true;
511+
while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
512+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
513+
514+
// trailing comma case
515+
if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
516+
break;
517+
}
518+
$isFirst = false;
519+
520+
$templates[] = $this->parseCallableTemplateArgument($tokens);
521+
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
522+
}
523+
524+
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
525+
526+
return $templates;
527+
}
528+
529+
530+
private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\Type\CallableTypeTemplateNode
531+
{
532+
$startLine = $tokens->currentTokenLine();
533+
$startIndex = $tokens->currentTokenIndex();
534+
535+
$identifier = $this->enrichWithAttributes(
536+
$tokens,
537+
new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()),
538+
$startLine,
539+
$startIndex
540+
);
541+
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
542+
543+
$bound = null;
544+
if ($tokens->tryConsumeTokenValue('of')) {
545+
$bound = $this->parse($tokens);
546+
}
547+
548+
return $this->enrichWithAttributes(
549+
$tokens,
550+
new Ast\Type\CallableTypeTemplateNode($identifier, $bound),
551+
$startLine,
552+
$startIndex
553+
);
488554
}
489555

490556

@@ -662,11 +728,11 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo
662728

663729

664730
/** @phpstan-impure */
665-
private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
731+
private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool$hasTemplate): Ast\Type\TypeNode
666732
{
667733
try {
668734
$tokens->pushSavePoint();
669-
$type = $this->parseCallable($tokens, $identifier);
735+
$type = $this->parseCallable($tokens, $identifier, $hasTemplate);
670736
$tokens->dropSavePoint();
671737

672738
} catch (ParserException $e) {

‎src/Printer/Printer.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
4242
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
4343
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
44+
use PHPStan\PhpDocParser\Ast\Type\CallableTypeTemplateNode;
4445
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
4546
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
4647
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
@@ -97,6 +98,7 @@ final class Printer
9798
ArrayShapeNode::class . '->items' => ', ',
9899
ObjectShapeNode::class . '->items' => ', ',
99100
CallableTypeNode::class . '->parameters' => ', ',
101+
CallableTypeNode::class . '->templates' => ', ',
100102
GenericTypeNode::class . '->genericTypes' => ', ',
101103
ConstExprArrayNode::class . '->items' => ', ',
102104
MethodTagValueNode::class . '->parameters' => ', ',
@@ -224,6 +226,11 @@ function (PhpDocChildNode $child): string {
224226
$isOptional = $node->isOptional ? '=' : '';
225227
return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional;
226228
}
229+
if ($node instanceof CallableTypeTemplateNode) {
230+
$identifier = $this->printType($node->identifier);
231+
$bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
232+
return "{$identifier}{$bound}";
233+
}
227234
if ($node instanceof DoctrineAnnotation) {
228235
return (string) $node;
229236
}
@@ -370,10 +377,15 @@ private function printType(TypeNode $node): string
370377
} else {
371378
$returnType = $this->printType($node->returnType);
372379
}
380+
$template = $node->templates !== []
381+
? '<' . implode(', ', array_map(function (CallableTypeTemplateNode $templateNode): string {
382+
return $this->print($templateNode);
383+
}, $node->templates)) . '>'
384+
: '';
373385
$parameters = implode(', ', array_map(function (CallableTypeParameterNode $parameterNode): string {
374386
return $this->print($parameterNode);
375387
}, $node->parameters));
376-
return "{$node->identifier}({$parameters}): {$returnType}";
388+
return "{$node->identifier}{$template}({$parameters}): {$returnType}";
377389
}
378390
if ($node instanceof ConditionalTypeForParameterNode) {
379391
return sprintf(

‎tests/PHPStan/Parser/TypeParserTest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
1616
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
1717
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
18+
use PHPStan\PhpDocParser\Ast\Type\CallableTypeTemplateNode;
1819
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
1920
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
2021
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
@@ -897,6 +898,104 @@ public function provideParseData(): array
897898
new IdentifierTypeNode('Foo')
898899
),
899900
],
901+
[
902+
'callable<A>(B): C',
903+
new CallableTypeNode(
904+
new IdentifierTypeNode('callable'),
905+
[
906+
new CallableTypeParameterNode(
907+
new IdentifierTypeNode('B'),
908+
false,
909+
false,
910+
'',
911+
false
912+
),
913+
],
914+
new IdentifierTypeNode('C'),
915+
[
916+
new CallableTypeTemplateNode(new IdentifierTypeNode('A'), null),
917+
]
918+
),
919+
],
920+
[
921+
'callable<>(): void',
922+
new ParserException(
923+
'>',
924+
Lexer::TOKEN_END,
925+
9,
926+
Lexer::TOKEN_IDENTIFIER
927+
),
928+
],
929+
[
930+
'Closure<T of Model>(T, int): (T|false)',
931+
new CallableTypeNode(
932+
new IdentifierTypeNode('Closure'),
933+
[
934+
new CallableTypeParameterNode(
935+
new IdentifierTypeNode('T'),
936+
false,
937+
false,
938+
'',
939+
false
940+
),
941+
new CallableTypeParameterNode(
942+
new IdentifierTypeNode('int'),
943+
false,
944+
false,
945+
'',
946+
false
947+
),
948+
],
949+
new UnionTypeNode([
950+
new IdentifierTypeNode('T'),
951+
new IdentifierTypeNode('false'),
952+
]),
953+
[
954+
new CallableTypeTemplateNode(new IdentifierTypeNode('T'), new IdentifierTypeNode('Model')),
955+
]
956+
),
957+
],
958+
[
959+
'\Closure<Tx of X|Z, Ty of Y>(Tx, Ty): array{ Ty, Tx }',
960+
new CallableTypeNode(
961+
new IdentifierTypeNode('\Closure'),
962+
[
963+
new CallableTypeParameterNode(
964+
new IdentifierTypeNode('Tx'),
965+
false,
966+
false,
967+
'',
968+
false
969+
),
970+
new CallableTypeParameterNode(
971+
new IdentifierTypeNode('Ty'),
972+
false,
973+
false,
974+
'',
975+
false
976+
),
977+
],
978+
new ArrayShapeNode([
979+
new ArrayShapeItemNode(
980+
null,
981+
false,
982+
new IdentifierTypeNode('Ty')
983+
),
984+
new ArrayShapeItemNode(
985+
null,
986+
false,
987+
new IdentifierTypeNode('Tx')
988+
),
989+
]),
990+
[
991+
new CallableTypeTemplateNode(new IdentifierTypeNode('Tx'), new UnionTypeNode([
992+
new IdentifierTypeNode('X'),
993+
new IdentifierTypeNode('Z'),
994+
])),
995+
new CallableTypeTemplateNode(new IdentifierTypeNode('Ty'), new IdentifierTypeNode('Y')),
996+
]
997+
),
998+
],
900999
[
9011000
'(Foo\\Bar<array<mixed, string>, (int | (string<foo> & bar)[])> | Lorem)',
9021001
new UnionTypeNode([

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /