Skip to content

Commit

Permalink
Parse generic callables
Browse files Browse the repository at this point in the history
  • Loading branch information
mad-briller committed Feb 23, 2024
1 parent e7f0d8f commit c23674d
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 47 deletions.
20 changes: 13 additions & 7 deletions doc/grammars/type.abnf
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ GenericTypeArgument
/ TokenWildcard

Callable
= TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType
= [CallableTemplate] TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType

CallableTemplate
= TokenAngleBracketOpen CallableTemplateArgument *(TokenComma CallableTemplateArgument) TokenAngleBracketClose

CallableTemplateArgument
= TokenIdentifier [1*ByteHorizontalWs TokenOf Type]

CallableParameters
= CallableParameter *(TokenComma CallableParameter)
Expand Down Expand Up @@ -192,6 +198,9 @@ TokenIs
TokenNot
= %s"not" 1*ByteHorizontalWs

TokenOf
= %s"of" 1*ByteHorizontalWs

TokenContravariant
= %s"contravariant" 1*ByteHorizontalWs

Expand All @@ -211,7 +220,7 @@ TokenIdentifier

ByteHorizontalWs
= %x09 ; horizontal tab
/ %x20 ; space
/ " "

ByteNumberSign
= "+"
Expand All @@ -238,11 +247,8 @@ ByteIdentifierFirst
/ %x80-FF

ByteIdentifierSecond
= %x30-39 ; 0-9
/ %x41-5A ; A-Z
/ "_"
/ %x61-7A ; a-z
/ %x80-FF
= ByteIdentifierFirst
/ %x30-39 ; 0-9

ByteSingleQuote
= %x27 ; '
Expand Down
13 changes: 11 additions & 2 deletions src/Ast/Type/CallableTypeNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use function implode;

class CallableTypeNode implements TypeNode
Expand All @@ -13,6 +14,9 @@ class CallableTypeNode implements TypeNode
/** @var IdentifierTypeNode */
public $identifier;

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

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

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

/**
* @param CallableTypeParameterNode[] $parameters
* @param TemplateTagValueNode[] $templateTypes
*/
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType)
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType, array $templateTypes = [])
{
$this->identifier = $identifier;
$this->parameters = $parameters;
$this->returnType = $returnType;
$this->templateTypes = $templateTypes;
}


Expand All @@ -36,8 +42,11 @@ public function __toString(): string
if ($returnType instanceof self) {
$returnType = "({$returnType})";
}
$template = $this->templateTypes !== []
? '<' . implode(', ', $this->templateTypes) . '>'
: '';
$parameters = implode(', ', $this->parameters);
return "{$this->identifier}({$parameters}): {$returnType}";
return "{$this->identifier}{$template}({$parameters}): {$returnType}";
}

}
41 changes: 12 additions & 29 deletions src/Parser/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,12 @@ 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, true);
$tagValue = $this->typeParser->parseTemplateTagValue(
$tokens,
function ($tokens) {
return $this->parseOptionalDescription($tokens);
}
);
break;

case '@extends':
Expand Down Expand Up @@ -947,7 +952,12 @@ private function parseMethodTagValue(TokenIterator $tokens): Ast\PhpDoc\MethodTa
do {
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();
$templateTypes[] = $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens, false), $startLine, $startIndex);
$templateTypes[] = $this->enrichWithAttributes(
$tokens,
$this->typeParser->parseTemplateTagValue($tokens),
$startLine,
$startIndex
);
} while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
}
Expand Down Expand Up @@ -1003,33 +1013,6 @@ private function parseMethodTagValueParameter(TokenIterator $tokens): Ast\PhpDoc
);
}

private function parseTemplateTagValue(TokenIterator $tokens, bool $parseDescription): Ast\PhpDoc\TemplateTagValueNode
{
$name = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
$bound = $this->typeParser->parse($tokens);

} else {
$bound = null;
}

if ($tokens->tryConsumeTokenValue('=')) {
$default = $this->typeParser->parse($tokens);
} else {
$default = null;
}

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

return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
}

private function parseExtendsTagValue(string $tagName, TokenIterator $tokens): Ast\PhpDoc\PhpDocTagValueNode
{
$startLine = $tokens->currentTokenLine();
Expand Down
104 changes: 96 additions & 8 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function in_array;
use function str_replace;
Expand Down Expand Up @@ -164,13 +165,17 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
return $type;
}

$type = $this->parseGeneric($tokens, $type);
$origType = $type;
$type = $this->tryParseCallable($tokens, $type, true);
if ($type === $origType) {
$type = $this->parseGeneric($tokens, $type);

if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
}
}
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$type = $this->tryParseCallable($tokens, $type);
$type = $this->tryParseCallable($tokens, $type, false);

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

/**
* @throws ParserException
* @param ?callable(TokenIterator): string $parseDescription
*/
public function parseTemplateTagValue(
TokenIterator $tokens,
?callable $parseDescription = null
): TemplateTagValueNode
{
$name = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
$bound = $this->parse($tokens);

} else {
$bound = null;
}

if ($tokens->tryConsumeTokenValue('=')) {
$default = $this->parse($tokens);
} else {
$default = null;
}

if ($parseDescription !== null) {
$description = $parseDescription($tokens);
} else {
$description = '';
}

return new Ast\PhpDoc\TemplateTagValueNode($name, $bound, $description, $default);
}


/** @phpstan-impure */
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
{
$templates = $hasTemplate
? $this->parseCallableTemplates($tokens)
: [];

$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);

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

return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates);
}


/**
* @return Ast\PhpDoc\TemplateTagValueNode[]
*
* @phpstan-impure
*/
private function parseCallableTemplates(TokenIterator $tokens): array
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);

$templates = [];

$isFirst = true;
while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);

// trailing comma case
if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
break;
}
$isFirst = false;

$templates[] = $this->parseCallableTemplateArgument($tokens);
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
}

$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);

return $templates;
}


private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();

return $this->enrichWithAttributes(
$tokens,
$this->parseTemplateTagValue($tokens),
$startLine,
$startIndex
);
}


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


/** @phpstan-impure */
private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
{
try {
$tokens->pushSavePoint();
$type = $this->parseCallable($tokens, $identifier);
$type = $this->parseCallable($tokens, $identifier, $hasTemplate);
$tokens->dropSavePoint();

} catch (ParserException $e) {
Expand Down
8 changes: 7 additions & 1 deletion src/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ final class Printer
ArrayShapeNode::class . '->items' => ', ',
ObjectShapeNode::class . '->items' => ', ',
CallableTypeNode::class . '->parameters' => ', ',
CallableTypeNode::class . '->templateTypes' => ', ',
GenericTypeNode::class . '->genericTypes' => ', ',
ConstExprArrayNode::class . '->items' => ', ',
MethodTagValueNode::class . '->parameters' => ', ',
Expand Down Expand Up @@ -380,10 +381,15 @@ private function printType(TypeNode $node): string
} else {
$returnType = $this->printType($node->returnType);
}
$template = $node->templateTypes !== []
? '<' . implode(', ', array_map(function (TemplateTagValueNode $templateNode): string {
return $this->print($templateNode);
}, $node->templateTypes)) . '>'
: '';
$parameters = implode(', ', array_map(function (CallableTypeParameterNode $parameterNode): string {
return $this->print($parameterNode);
}, $node->parameters));
return "{$node->identifier}({$parameters}): {$returnType}";
return "{$node->identifier}{$template}({$parameters}): {$returnType}";
}
if ($node instanceof ConditionalTypeForParameterNode) {
return sprintf(
Expand Down

0 comments on commit c23674d

Please sign in to comment.