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

feat: Add support for callable template in PHPDoc parser #7084

Merged
merged 10 commits into from Mar 13, 2024
85 changes: 81 additions & 4 deletions src/DocBlock/TypeExpression.php
Expand Up @@ -65,8 +65,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>
\h+(?i)(?<callable_template_inner_b_kw>of|as)(?-i)\h+
(?<callable_template_inner_b_types>(?&types_inner))
|)
(?<callable_template_inner_d>
\h*=\h*
(?<callable_template_inner_d_types>(?&types_inner))
|)
mvorisek marked this conversation as resolved.
Show resolved Hide resolved
)
(?:
\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 +402,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 +487,49 @@ private function parseCommaSeparatedInnerTypes(int $startIndex, string $value):
}
}

private function parseCallableTemplateInnerTypes(int $startIndex, string $value): void
mvorisek marked this conversation as resolved.
Show resolved Hide resolved
{
$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 +586,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
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
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