TypeParser: add support for callable types

This commit is contained in:
Jan Tvrdik 2018-03-20 11:33:36 +01:00
parent 3ff33ac44c
commit 5251b5b4d4
6 changed files with 291 additions and 4 deletions

View File

@ -16,13 +16,39 @@ Nullable
= TokenNullable TokenIdentifier [Generic]
Atomic
= TokenIdentifier [Generic / Array]
= TokenIdentifier [Generic / Callable / Array]
/ TokenThisVariable
/ TokenParenthesesOpen Type TokenParenthesesClose [Array]
Generic
= TokenAngleBracketOpen Type *(TokenComma Type) TokenAngleBracketClose
Callable
= TokenParenthesesOpen [CallableParameters] TokenParenthesesClose TokenColon CallableReturnType
CallableParameters
= CallableParameter *(TokenComma CallableParameter)
CallableParameter
= Type [CallableParameterIsReference] [CallableParameterIsVariadic] [CallableParameterName] [CallableParameterIsOptional]
CallableParameterIsReference
= TokenIntersection
CallableParameterIsVariadic
= TokenVariadic
CallableParameterName
= TokenVariable
CallableParameterIsOptional
= TokenEqualSign
CallableReturnType
= TokenIdentifier [Generic]
/ Nullable
/ TokenParenthesesOpen Type TokenParenthesesClose
Array
= 1*(TokenSquareBracketOpen TokenSquareBracketClose)
@ -116,6 +142,18 @@ TokenSquareBracketClose
TokenComma
= "," *ByteHorizontalWs
TokenColon
= ":" *ByteHorizontalWs
TokenVariadic
= "..." *ByteHorizontalWs
TokenEqualSign
= "=" *ByteHorizontalWs
TokenVariable
= "$" ByteIdentifierFirst *ByteIdentifierSecond *ByteHorizontalWs
TokenDoubleArrow
= "=>" *ByteHorizontalWs

View File

@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
class CallableTypeNode implements TypeNode
{
/** @var IdentifierTypeNode */
public $identifier;
/** @var CallableTypeParameterNode[] */
public $parameters;
/** @var TypeNode */
public $returnType;
public function __construct(IdentifierTypeNode $identifier, array $parameters, TypeNode $returnType)
{
$this->identifier = $identifier;
$this->parameters = $parameters;
$this->returnType = $returnType;
}
public function __toString(): string
{
$parameters = implode(', ', $this->parameters);
return "{$this->identifier}({$parameters}): {$this->returnType}";
}
}

View File

@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Ast\Node;
class CallableTypeParameterNode implements Node
{
/** @var TypeNode */
public $type;
/** @var bool */
public $isReference;
/** @var bool */
public $isVariadic;
/** @var string (may be empty) */
public $parameterName;
/** @var bool */
public $isOptional;
public function __construct(TypeNode $type, bool $isReference, bool $isVariadic, string $parameterName, bool $isOptional)
{
$this->type = $type;
$this->isReference = $isReference;
$this->isVariadic = $isVariadic;
$this->parameterName = $parameterName;
$this->isOptional = $isOptional;
}
public function __toString(): string
{
$type = "{$this->type} ";
$isReference = $this->isReference ? '&' : '';
$isVariadic = $this->isVariadic ? '...' : '';
$default = $this->isOptional ? ' = default' : '';
return "{$type}{$isReference}{$isVariadic}{$this->parameterName}{$default}";
}
}

View File

@ -19,6 +19,7 @@ class Lexer
const TOKEN_OPEN_SQUARE_BRACKET = 8;
const TOKEN_CLOSE_SQUARE_BRACKET = 9;
const TOKEN_COMMA = 10;
const TOKEN_COLON = 29;
const TOKEN_VARIADIC = 11;
const TOKEN_DOUBLE_COLON = 12;
const TOKEN_DOUBLE_ARROW = 13;
@ -50,6 +51,7 @@ class Lexer
self::TOKEN_OPEN_SQUARE_BRACKET => '\'[\'',
self::TOKEN_CLOSE_SQUARE_BRACKET => '\']\'',
self::TOKEN_COMMA => '\',\'',
self::TOKEN_COLON => '\':\'',
self::TOKEN_VARIADIC => '\'...\'',
self::TOKEN_DOUBLE_COLON => '\'::\'',
self::TOKEN_DOUBLE_ARROW => '\'=>\'',
@ -107,8 +109,8 @@ class Lexer
private function initialize()
{
$patterns = [
// '&' followed by TOKEN_VARIADIC or TOKEN_VARIABLE
self::TOKEN_REFERENCE => '&(?=\\s*+(?:(?:\\.\\.\\.)|(?:\\$(?!this\\b))))',
// '&' followed by TOKEN_VARIADIC, TOKEN_VARIABLE, TOKEN_EQUAL, TOKEN_EQUAL or TOKEN_CLOSE_PARENTHESES
self::TOKEN_REFERENCE => '&(?=\\s*+(?:[.,=)]|(?:\\$(?!this(?![0-9a-z_\\x80-\\xFF])))))',
self::TOKEN_UNION => '\\|',
self::TOKEN_INTERSECTION => '&',
self::TOKEN_NULLABLE => '\\?',
@ -125,6 +127,7 @@ class Lexer
self::TOKEN_DOUBLE_COLON => '::',
self::TOKEN_DOUBLE_ARROW => '=>',
self::TOKEN_EQUAL => '=',
self::TOKEN_COLON => ':',
self::TOKEN_OPEN_PHPDOC => '/\\*\\*(?=\\s)',
self::TOKEN_CLOSE_PHPDOC => '\\*/',
@ -137,7 +140,7 @@ class Lexer
self::TOKEN_DOUBLE_QUOTED_STRING => '"(?:\\\\[^\\r\\n]|[^"\\r\\n\\\\])*+"',
self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+)++',
self::TOKEN_THIS_VARIABLE => '\\$this\\b',
self::TOKEN_THIS_VARIABLE => '\\$this(?![0-9a-z_\\x80-\\xFF])',
self::TOKEN_VARIABLE => '\\$[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+',
self::TOKEN_HORIZONTAL_WS => '[\\x09\\x20]++',

View File

@ -48,6 +48,9 @@ class TypeParser
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric($tokens, $type);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$type = $this->parseCallable($tokens, $type);
} elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
$type = $this->tryParseArray($tokens, $type);
}
@ -110,6 +113,67 @@ class TypeParser
}
private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
{
$tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
$parameters = [];
if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
$parameters[] = $this->parseCallableParameter($tokens);
while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
$parameters[] = $this->parseCallableParameter($tokens);
}
}
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
$tokens->consumeTokenType(Lexer::TOKEN_COLON);
$returnType = $this->parseCallableReturnType($tokens);
return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
}
private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode
{
$type = $this->parse($tokens);
$isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
$isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
$parameterName = $tokens->currentTokenValue();
$tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
} else {
$parameterName = '';
}
$isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
return new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional);
}
private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode
{
if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
$type = $this->parseNullable($tokens);
} elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
$type = $this->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
} else {
$type = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
$tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$type = $this->parseGeneric($tokens, $type);
}
}
return $type;
}
private function tryParseArray(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
{
try {

View File

@ -3,6 +3,8 @@
namespace PHPStan\PhpDocParser\Parser;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
@ -262,6 +264,111 @@ class TypeParserTest extends \PHPUnit\Framework\TestCase
]
),
],
[
'callable(): Foo',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new IdentifierTypeNode('Foo')
),
],
[
'callable(): ?Foo',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new NullableTypeNode(
new IdentifierTypeNode('Foo')
)
),
],
[
'callable(): Foo<Bar>',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new GenericTypeNode(
new IdentifierTypeNode('Foo'),
[
new IdentifierTypeNode('Bar'),
]
)
),
],
[
'callable(): Foo|Bar',
new UnionTypeNode([
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new IdentifierTypeNode('Foo')
),
new IdentifierTypeNode('Bar'),
]),
],
[
'callable(): Foo&Bar',
new IntersectionTypeNode([
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new IdentifierTypeNode('Foo')
),
new IdentifierTypeNode('Bar'),
]),
],
[
'callable(): (Foo|Bar)',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new UnionTypeNode([
new IdentifierTypeNode('Foo'),
new IdentifierTypeNode('Bar'),
])
),
],
[
'callable(): (Foo&Bar)',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[],
new IntersectionTypeNode([
new IdentifierTypeNode('Foo'),
new IdentifierTypeNode('Bar'),
])
),
],
[
'callable(A&...$a=, B&...=, C): Foo',
new CallableTypeNode(
new IdentifierTypeNode('callable'),
[
new CallableTypeParameterNode(
new IdentifierTypeNode('A'),
true,
true,
'$a',
true
),
new CallableTypeParameterNode(
new IdentifierTypeNode('B'),
true,
true,
'',
true
),
new CallableTypeParameterNode(
new IdentifierTypeNode('C'),
false,
false,
'',
false
),
],
new IdentifierTypeNode('Foo')
),
],
[
'(Foo\\Bar<array<mixed, string>, (int | (string<foo> & bar)[])> | Lorem)',
new UnionTypeNode([