mirror of
https://github.com/danog/phpdoc-parser.git
synced 2024-11-26 20:15:11 +01:00
TypeParser: add support for callable types
This commit is contained in:
parent
3ff33ac44c
commit
5251b5b4d4
@ -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
|
||||
|
||||
|
31
src/Ast/Type/CallableTypeNode.php
Normal file
31
src/Ast/Type/CallableTypeNode.php
Normal 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}";
|
||||
}
|
||||
|
||||
}
|
44
src/Ast/Type/CallableTypeParameterNode.php
Normal file
44
src/Ast/Type/CallableTypeParameterNode.php
Normal 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}";
|
||||
}
|
||||
|
||||
}
|
@ -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]++',
|
||||
|
@ -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 {
|
||||
|
@ -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([
|
||||
|
Loading…
Reference in New Issue
Block a user