Skip to content

Commit

Permalink
feat: Add support for callable template in PHPDoc parser (#7084)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Mar 13, 2024
1 parent ffce76b commit 54e8f39
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 4 deletions.
90 changes: 86 additions & 4 deletions src/DocBlock/TypeExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ final class TypeExpression
)*+
)';

/**
* Based on:
* - https://github.com/phpstan/phpdoc-parser/blob/1.26.0/doc/grammars/type.abnf fuzzing grammar
* - and https://github.com/phpstan/phpdoc-parser/blob/1.26.0/src/Parser/PhpDocParser.php parser impl.
*/
private const REGEX_TYPE = '(?<type>(?x) # single type
(?<nullable>\??\h*)
(?:
Expand All @@ -65,8 +70,33 @@ final class TypeExpression
\h*\}
)
|
(?<callable> # callable syntax, e.g. `callable(string, int...): bool`
(?<callable_start>(?&name)\h*\(\h*)
(?<callable> # callable syntax, e.g. `callable(string, int...): bool`, `\Closure<T>(T, int): T`
(?<callable_name>(?&name))
(?<callable_template>
(?<callable_template_start>\h*<\h*)
(?<callable_template_inners>
(?<callable_template_inner>
(?<callable_template_inner_name>
(?&identifier)
)
(?<callable_template_inner_b> # template bound
\h+(?i)(?<callable_template_inner_b_kw>of|as)(?-i)\h+
(?<callable_template_inner_b_types>(?&types_inner))
|)
(?<callable_template_inner_d> # template default
\h*=\h*
(?<callable_template_inner_d_types>(?&types_inner))
|)
)
(?:
\h*,\h*
(?&callable_template_inner)
)*+
)
\h*>
(?=\h*\()
|)
(?<callable_start>\h*\(\h*)
(?<callable_arguments>
(?<callable_argument>
(?<callable_argument_type>(?&types_inner))
Expand Down Expand Up @@ -377,8 +407,16 @@ private function parse(): void
$matches['generic_types'][0]
);
} elseif ('' !== ($matches['callable'][0] ?? '') && $matches['callable'][1] === $nullableLength) {
$this->parseCallableTemplateInnerTypes(
$index + \strlen($matches['callable_name'][0])
+ \strlen($matches['callable_template_start'][0]),
$matches['callable_template_inners'][0]
);

$this->parseCallableArgumentTypes(
$index + \strlen($matches['callable_start'][0]),
$index + \strlen($matches['callable_name'][0])
+ \strlen($matches['callable_template'][0])
+ \strlen($matches['callable_start'][0]),
$matches['callable_arguments'][0]
);

Expand Down Expand Up @@ -454,6 +492,49 @@ private function parseCommaSeparatedInnerTypes(int $startIndex, string $value):
}
}

private function parseCallableTemplateInnerTypes(int $startIndex, string $value): void
{
$index = 0;
while (\strlen($value) !== $index) {
Preg::match(
'{\G(?:(?=1)0'.self::REGEX_TYPES.'|(?<_callable_template_inner>(?&callable_template_inner))(?:\h*,\h*|$))}',
$value,
$prematches,
0,
$index
);
$consumedValue = $prematches['_callable_template_inner'];
$consumedValueLength = \strlen($consumedValue);
$consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;

$addedPrefix = 'Closure<';
Preg::match(
'{^'.self::REGEX_TYPES.'$}',
$addedPrefix.$consumedValue.'>(): void',
$matches,
PREG_OFFSET_CAPTURE
);

if ('' !== $matches['callable_template_inner_b'][0]) {
$this->innerTypeExpressions[] = [
'start_index' => $startIndex + $index + $matches['callable_template_inner_b_types'][1]
- \strlen($addedPrefix),
'expression' => $this->inner($matches['callable_template_inner_b_types'][0]),
];
}

if ('' !== $matches['callable_template_inner_d'][0]) {
$this->innerTypeExpressions[] = [
'start_index' => $startIndex + $index + $matches['callable_template_inner_d_types'][1]
- \strlen($addedPrefix),
'expression' => $this->inner($matches['callable_template_inner_d_types'][0]),
];
}

$index += $consumedValueLength + $consumedCommaLength;
}
}

private function parseCallableArgumentTypes(int $startIndex, string $value): void
{
$index = 0;
Expand Down Expand Up @@ -510,7 +591,8 @@ private function parseArrayShapeInnerTypes(int $startIndex, string $value): void
);

$this->innerTypeExpressions[] = [
'start_index' => $startIndex + $index + $matches['array_shape_inner_value'][1] - \strlen($addedPrefix),
'start_index' => $startIndex + $index + $matches['array_shape_inner_value'][1]
- \strlen($addedPrefix),
'expression' => $this->inner($matches['array_shape_inner_value'][0]),
];

Expand Down
37 changes: 37 additions & 0 deletions tests/DocBlock/TypeExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,24 @@ public static function provideGetTypesCases(): iterable

yield ['\\Closure(float|int): (bool|int)'];

yield ['Closure<T>(): T'];

yield ['Closure<Tx, Ty>(): array{x: Tx, y: Ty}'];

yield ['array < int , callable ( string ) : bool >'];

yield ['Closure<T of Foo>(T): T'];

yield ['Closure< T1 of Foo, T2 AS Foo >(T1): T2'];

yield ['Closure<T = Foo>(T): T'];

yield ['Closure<T1=int, T2 of Foo = Foo2>(T1): T2'];

yield ['Closure<T of string = \'\'>(T): T'];

yield ['Closure<Closure_can_be_regular_class>'];

yield ['Closure(int $a)'];

yield ['Closure(int $a): bool'];
Expand Down Expand Up @@ -376,6 +394,10 @@ public static function provideParseInvalidExceptionCases(): iterable
yield ['\' unclosed string \\\''];

yield 'generic with no arguments' => ['f<>'];

yield 'generic Closure with no arguments' => ['Closure<>(): void'];

yield 'generic Closure with non-identifier template argument' => ['Closure<A|B>(): void'];
}

public function testHugeType(): void
Expand Down Expand Up @@ -814,6 +836,21 @@ public static function provideSortTypesCases(): iterable
'array<string, array{ \Closure(mixed, string, $this): (float|int)|string }|string>|false',
];

yield 'generic Closure' => [
'Closure<B, A>(y|x, U<p|o>|B|A): (Y|B|X)',
'Closure<B, A>(x|y, A|B|U<o|p>): (B|X|Y)',
];

yield 'generic Closure with bound template' => [
'Closure<B of J|I, C, A of V|U, D of object>(B|A): array{B, A, B, C, D}',
'Closure<B of I|J, C, A of U|V, D of object>(A|B): array{B, A, B, C, D}',
];

yield 'generic Closure with template with default' => [
'Closure<T = B&A>(T): void',
'Closure<T = A&B>(T): void',
];

yield 'nullable generic' => [
'?array<Foo|Bar>',
'?array<Bar|Foo>',
Expand Down
15 changes: 15 additions & 0 deletions tests/Fixer/Phpdoc/PhpdocTypesOrderFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,21 @@ public static function provideFixWithAlphaAlgorithmCases(): iterable
'<?php /** @param A|((B&C)|D) */',
'<?php /** @param (D|(C&B))|A */',
];

yield [
'<?php /** @var Closure<T>(T): T|null|string */',
'<?php /** @var string|Closure<T>(T): T|null */',
];

yield [
'<?php /** @var \Closure<T of Model, T2, T3>(A|T, T3, T2): (T|T2)|null|string */',
'<?php /** @var string|\Closure<T of Model, T2, T3>(T|A, T3, T2): (T2|T)|null */',
];

yield [
'<?php /** @var Closure<Closure_can_be_regular_class>|null|string */',
'<?php /** @var string|Closure<Closure_can_be_regular_class>|null */',
];
}

/**
Expand Down

0 comments on commit 54e8f39

Please sign in to comment.