44
55use LogicException ;
66use PHPStan \PhpDocParser \Ast ;
7+ use PHPStan \PhpDocParser \Ast \PhpDoc \TemplateTagValueNode ;
78use PHPStan \PhpDocParser \Lexer \Lexer ;
89use function in_array ;
910use 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 ) {
0 commit comments