4
4
5
5
use LogicException ;
6
6
use PHPStan \PhpDocParser \Ast ;
7
+ use PHPStan \PhpDocParser \Ast \PhpDoc \TemplateTagValueNode ;
7
8
use PHPStan \PhpDocParser \Lexer \Lexer ;
8
9
use function in_array ;
9
10
use function str_replace ;
@@ -164,13 +165,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
164
165
return $ type ;
165
166
}
166
167
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 );
168
172
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
+ }
171
176
}
172
177
} elseif ($ tokens ->isCurrentTokenType (Lexer::TOKEN_OPEN_PARENTHESES )) {
173
- $ type = $ this ->tryParseCallable ($ tokens , $ type );
178
+ $ type = $ this ->tryParseCallable ($ tokens , $ type, false );
174
179
175
180
} elseif ($ tokens ->isCurrentTokenType (Lexer::TOKEN_OPEN_SQUARE_BRACKET )) {
176
181
$ type = $ this ->tryParseArrayOrOffsetAccess ($ tokens , $ type );
@@ -464,10 +469,48 @@ public function parseGenericTypeArgument(TokenIterator $tokens): array
464
469
return [$ type , $ variance ];
465
470
}
466
471
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
+
467
506
468
507
/** @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
470
509
{
510
+ $ templates = $ hasTemplate
511
+ ? $ this ->parseCallableTemplates ($ tokens )
512
+ : [];
513
+
471
514
$ tokens ->consumeTokenType (Lexer::TOKEN_OPEN_PARENTHESES );
472
515
$ tokens ->tryConsumeTokenType (Lexer::TOKEN_PHPDOC_EOL );
473
516
@@ -492,7 +535,52 @@ private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNod
492
535
$ startIndex = $ tokens ->currentTokenIndex ();
493
536
$ returnType = $ this ->enrichWithAttributes ($ tokens , $ this ->parseCallableReturnType ($ tokens ), $ startLine , $ startIndex );
494
537
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
+ );
496
584
}
497
585
498
586
@@ -670,11 +758,11 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo
670
758
671
759
672
760
/** @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
674
762
{
675
763
try {
676
764
$ tokens ->pushSavePoint ();
677
- $ type = $ this ->parseCallable ($ tokens , $ identifier );
765
+ $ type = $ this ->parseCallable ($ tokens , $ identifier, $ hasTemplate );
678
766
$ tokens ->dropSavePoint ();
679
767
680
768
} catch (ParserException $ e ) {
0 commit comments