Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add callable template support #199

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
12 changes: 10 additions & 2 deletions src/Ast/Type/CallableTypeNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class CallableTypeNode implements TypeNode
/** @var IdentifierTypeNode */
public $identifier;

/** @var CallableTypeTemplateNode[] */
public $templates;

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

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

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


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

}
35 changes: 35 additions & 0 deletions src/Ast/Type/CallableTypeTemplateNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDocParser\Ast\Type;

use PHPStan\PhpDocParser\Ast\Node;
use PHPStan\PhpDocParser\Ast\NodeAttributes;

class CallableTypeTemplateNode implements Node
{

use NodeAttributes;

/** @var IdentifierTypeNode */
public $identifier;

/** @var TypeNode|null */
public $bound;

public function __construct(IdentifierTypeNode $identifier, ?TypeNode $bound)
{
$this->identifier = $identifier;
$this->bound = $bound;
}

public function __toString(): string
{
$res = (string) $this->identifier;
if ($this->bound !== null) {
$res .= ' of ' . $this->bound;
}

return $res;
}

}
82 changes: 74 additions & 8 deletions src/Parser/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,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 @@ -458,8 +462,12 @@ public function parseGenericTypeArgument(TokenIterator $tokens): array


/** @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 @@ -484,7 +492,65 @@ 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\Type\CallableTypeTemplateNode[]
*
* @phpstan-impure
*/
private function parseCallableTemplates(TokenIterator $tokens): array
mvorisek marked this conversation as resolved.
Show resolved Hide resolved
{
$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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why is there so much repeated code. We're already parsing template types in similar position for @method, there has to be a way to reuse that code: #160

Everything you should need is already in parseTemplateTagValue.

Copy link
Contributor Author

@mvorisek mvorisek Nov 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please guide me how and what to reuse? The code in #160 (Parser/PhpDocParser in general) is for phpdoc instead of for type (Parser/TypeParser).

If the newly introduced Ast/Type/CallableTypeTemplateNode class is wanted (and it probably is - callable template should not consist of anything like description), then the code cannot be simplified much. As the grammar is not completely the same, basically the only parsing part to deduplicate would be <, ,, of.

If you want, feel free to push any changes to my branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think #160 implements the very same use case as this PR: it parses inline template tag definitions, disallowing descriptions, but accounting for everything else (bound types, defaults, ...). Perhaps it could suffice to extract the parseTemplateTagValue method into a TemplateTagParser that could be used in TypeParser as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that logic should be extracted so that it's usable by both TypeParser and PhpDocParser.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mvorisek are you going to look into this feedback or should i have a go at it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mad-briller Please have a go at it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mad-briller would you like to join the forces? Feel free to either reuse this PR as a separate one or submit a PR againts https://github.com/mvorisek/phpdoc-parser/tree/parse_generic_callable - this PR should be basically done, but the code should be deduplicated per #199 (comment) request.

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\Type\CallableTypeTemplateNode
{
$startLine = $tokens->currentTokenLine();
$startIndex = $tokens->currentTokenIndex();

$identifier = $this->enrichWithAttributes(
$tokens,
new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()),
$startLine,
$startIndex
);
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);

$bound = null;
if ($tokens->tryConsumeTokenValue('of')) {
mvorisek marked this conversation as resolved.
Show resolved Hide resolved
$bound = $this->parse($tokens);
}

return $this->enrichWithAttributes(
$tokens,
new Ast\Type\CallableTypeTemplateNode($identifier, $bound),
$startLine,
$startIndex
);
}


Expand Down Expand Up @@ -662,11 +728,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
14 changes: 13 additions & 1 deletion src/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeTemplateNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
Expand Down Expand Up @@ -97,6 +98,7 @@ final class Printer
ArrayShapeNode::class . '->items' => ', ',
ObjectShapeNode::class . '->items' => ', ',
CallableTypeNode::class . '->parameters' => ', ',
CallableTypeNode::class . '->templates' => ', ',
mvorisek marked this conversation as resolved.
Show resolved Hide resolved
GenericTypeNode::class . '->genericTypes' => ', ',
ConstExprArrayNode::class . '->items' => ', ',
MethodTagValueNode::class . '->parameters' => ', ',
Expand Down Expand Up @@ -224,6 +226,11 @@ function (PhpDocChildNode $child): string {
$isOptional = $node->isOptional ? '=' : '';
return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional;
}
if ($node instanceof CallableTypeTemplateNode) {
$identifier = $this->printType($node->identifier);
$bound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : '';
return "{$identifier}{$bound}";
}
if ($node instanceof DoctrineAnnotation) {
return (string) $node;
}
Expand Down Expand Up @@ -370,10 +377,15 @@ private function printType(TypeNode $node): string
} else {
$returnType = $this->printType($node->returnType);
}
$template = $node->templates !== []
? '<' . implode(', ', array_map(function (CallableTypeTemplateNode $templateNode): string {
return $this->print($templateNode);
}, $node->templates)) . '>'
: '';
$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
99 changes: 99 additions & 0 deletions tests/PHPStan/Parser/TypeParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeTemplateNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeForParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConditionalTypeNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
Expand Down Expand Up @@ -897,6 +898,104 @@ public function provideParseData(): array
new IdentifierTypeNode('Foo')
),
],
[
'callable<A>(B): C',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[
new CallableTypeParameterNode(
new IdentifierTypeNode('B'),
false,
false,
'',
false
),
],
new IdentifierTypeNode('C'),
[
new CallableTypeTemplateNode(new IdentifierTypeNode('A'), null),
]
),
],
[
'callable<>(): void',
new ParserException(
'>',
Lexer::TOKEN_END,
9,
Lexer::TOKEN_IDENTIFIER
),
],
[
'Closure<T of Model>(T, int): (T|false)',
new CallableTypeNode(
new IdentifierTypeNode('Closure'),
[
new CallableTypeParameterNode(
new IdentifierTypeNode('T'),
false,
false,
'',
false
),
new CallableTypeParameterNode(
new IdentifierTypeNode('int'),
false,
false,
'',
false
),
],
new UnionTypeNode([
new IdentifierTypeNode('T'),
new IdentifierTypeNode('false'),
]),
[
new CallableTypeTemplateNode(new IdentifierTypeNode('T'), new IdentifierTypeNode('Model')),
]
),
],
[
'\Closure<Tx of X|Z, Ty of Y>(Tx, Ty): array{ Ty, Tx }',
new CallableTypeNode(
new IdentifierTypeNode('\Closure'),
[
new CallableTypeParameterNode(
new IdentifierTypeNode('Tx'),
false,
false,
'',
false
),
new CallableTypeParameterNode(
new IdentifierTypeNode('Ty'),
false,
false,
'',
false
),
],
new ArrayShapeNode([
new ArrayShapeItemNode(
null,
false,
new IdentifierTypeNode('Ty')
),
new ArrayShapeItemNode(
null,
false,
new IdentifierTypeNode('Tx')
),
]),
[
new CallableTypeTemplateNode(new IdentifierTypeNode('Tx'), new UnionTypeNode([
new IdentifierTypeNode('X'),
new IdentifierTypeNode('Z'),
])),
new CallableTypeTemplateNode(new IdentifierTypeNode('Ty'), new IdentifierTypeNode('Y')),
]
),
],
[
'(Foo\\Bar<array<mixed, string>, (int | (string<foo> & bar)[])> | Lorem)',
new UnionTypeNode([
Expand Down
Loading