Skip to content

Commit

Permalink
bug: Fix array/object shape phpdoc type parse (#6962)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed May 18, 2023
1 parent fd80d9e commit 234e98f
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 45 deletions.
61 changes: 31 additions & 30 deletions src/DocBlock/TypeExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ final class TypeExpression
(?<nullable>\??)
(?:
(?<object_like_array>
(?<object_like_array_start>array\h*\{)
(?<object_like_array_keys>
(?<object_like_array_key>
\h*[^?:\h]+\h*\??\h*:\h*(?&types)
(?<object_like_array_start>(array|object)\h*\{\h*)
(?<object_like_array_inners>
(?<object_like_array_inner>
(?<object_like_array_inner_key>(?:(?&constant)|(?&name))\h*\??\h*:\h*)?
(?<object_like_array_inner_value>(?&types))
)
(?:\h*,(?&object_like_array_key))*
)
(?:\h*,\h*(?&object_like_array_inner))*
)?
\h*\}
)
|
Expand Down Expand Up @@ -343,9 +344,9 @@ private function parse(): void
}

if ('' !== ($matches['object_like_array'] ?? '')) {
$this->parseObjectLikeArrayKeys(
$this->parseObjectLikeArrayInnerTypes(
$index + \strlen($matches['object_like_array_start']),
$matches['object_like_array_keys']
$matches['object_like_array_inners'] ?? ''
);

return;
Expand Down Expand Up @@ -397,7 +398,7 @@ private function parseCommaSeparatedInnerTypes(int $startIndex, string $value):
{
while ('' !== $value) {
Preg::match(
'{^'.self::REGEX_TYPES.'\h*(?:,|$)}x',
'{^'.self::REGEX_TYPES.'(?:\h*,\h*|$)}x',
$value,
$matches
);
Expand All @@ -407,39 +408,39 @@ private function parseCommaSeparatedInnerTypes(int $startIndex, string $value):
'expression' => $this->inner($matches['types']),
];

$newValue = Preg::replace(
'/^'.preg_quote($matches['types'], '/').'(\h*\,\h*)?/',
'',
$value
);

$startIndex += \strlen($value) - \strlen($newValue);
$value = $newValue;
$consumedValueLength = \strlen($matches[0]);
$startIndex += $consumedValueLength;
$value = substr($value, $consumedValueLength);
}
}

private function parseObjectLikeArrayKeys(int $startIndex, string $value): void
private function parseObjectLikeArrayInnerTypes(int $startIndex, string $value): void
{
while ('' !== $value) {
Preg::match(
'{(?<_start>^.+?:\h*)'.self::REGEX_TYPES.'\h*(?:,|$)}x',
'{^(?:(?=1)0'.self::REGEX_TYPES.'|(?<object_like_array_inner2>(?&object_like_array_inner))(?:\h*,\h*)?)}x',
$value,
$matches
$prematches
);
$consumedValue = $prematches['object_like_array_inner2'];
$consumedValueLength = \strlen($consumedValue);
$consumedCommaLength = \strlen($prematches[0]) - $consumedValueLength;

$addedPrefix = 'array{';
Preg::match(
'{^'.self::REGEX_TYPES.'$}x',
$addedPrefix.$consumedValue.'}',
$matches,
PREG_OFFSET_CAPTURE
);

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

$newValue = Preg::replace(
'/^.+?:\h*'.preg_quote($matches['types'], '/').'(\h*\,\h*)?/',
'',
$value
);

$startIndex += \strlen($value) - \strlen($newValue);
$value = $newValue;
$startIndex += $consumedValueLength + $consumedCommaLength;
$value = substr($value, $consumedValueLength + $consumedCommaLength);
}
}

Expand Down
24 changes: 12 additions & 12 deletions src/Tokenizer/Tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public static function fromCode(string $code): self
}

/**
* @return array<self::BLOCK_TYPE_*, array<'start'|'end', string|array{int, string}>>
* @return array<self::BLOCK_TYPE_*, array<'end'|'start', array{int, string}|string>>
*/
public static function getBlockEdgeDefinitions(): array
{
Expand Down Expand Up @@ -535,9 +535,9 @@ public function getNextNonWhitespace(int $index, ?string $whitespaces = null): ?
*
* This method is shorthand for getTokenOfKindSibling method.
*
* @param int $index token index
* @param int $index token index
* @param list<array{int}|string|Token> $tokens possible tokens
* @param bool $caseSensitive perform a case sensitive comparison
* @param bool $caseSensitive perform a case sensitive comparison
*/
public function getNextTokenOfKind(int $index, array $tokens = [], bool $caseSensitive = true): ?int
{
Expand Down Expand Up @@ -583,9 +583,9 @@ public function getPrevNonWhitespace(int $index, ?string $whitespaces = null): ?
* Get index for closest previous token of given kind.
* This method is shorthand for getTokenOfKindSibling method.
*
* @param int $index token index
* @param int $index token index
* @param list<array{int}|string|Token> $tokens possible tokens
* @param bool $caseSensitive perform a case sensitive comparison
* @param bool $caseSensitive perform a case sensitive comparison
*/
public function getPrevTokenOfKind(int $index, array $tokens = [], bool $caseSensitive = true): ?int
{
Expand All @@ -595,10 +595,10 @@ public function getPrevTokenOfKind(int $index, array $tokens = [], bool $caseSen
/**
* Get index for closest sibling token of given kind.
*
* @param int $index token index
* @param -1|1 $direction
* @param list<array{int}|string|Token> $tokens possible tokens
* @param bool $caseSensitive perform a case sensitive comparison
* @param int $index token index
* @param -1|1 $direction
* @param list<array{int}|string|Token> $tokens possible tokens
* @param bool $caseSensitive perform a case sensitive comparison
*/
public function getTokenOfKindSibling(int $index, int $direction, array $tokens = [], bool $caseSensitive = true): ?int
{
Expand Down Expand Up @@ -626,9 +626,9 @@ public function getTokenOfKindSibling(int $index, int $direction, array $tokens
/**
* Get index for closest sibling token not of given kind.
*
* @param int $index token index
* @param -1|1 $direction
* @param list<array{int}|string|Token> $tokens possible tokens
* @param int $index token index
* @param -1|1 $direction
* @param list<array{int}|string|Token> $tokens possible tokens
*/
public function getTokenNotOfKindSibling(int $index, int $direction, array $tokens = []): ?int
{
Expand Down
28 changes: 28 additions & 0 deletions tests/DocBlock/TypeExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ public static function provideGetTypesCases(): iterable

yield ['A & B', ['A', 'B']];

yield ['array{}', ['array{}']];

yield ['object{ }', ['object{ }']];

yield ['array{1: bool, 2: bool}', ['array{1: bool, 2: bool}']];

yield ['array{a: int|string, b?: bool}', ['array{a: int|string, b?: bool}']];
Expand All @@ -111,6 +115,10 @@ public static function provideGetTypesCases(): iterable

yield ['array { a : int | string , b ? : A<B, C> }', ['array { a : int | string , b ? : A<B, C> }']];

yield ['array{bool, int}', ['array{bool, int}']];

yield ['object{ bool, foo2: int }', ['object{ bool, foo2: int }']];

yield ['callable(string)', ['callable(string)']];

yield ['callable(string): bool', ['callable(string): bool']];
Expand Down Expand Up @@ -381,6 +389,21 @@ public static function provideSortTypesCases(): iterable
'array{0: bool|int, "foo": bool|int}',
];

yield 'simple in array shape with implicit key' => [
'array{int|bool}',
'array{bool|int}',
];

yield 'array shape with multiple colons - array shape' => [
'array{array{x:int|bool}, a:array{x:int|bool}}',
'array{array{x:bool|int}, a:array{x:bool|int}}',
];

yield 'array shape with multiple colons - callable' => [
'array{array{x:int|bool}, int|bool, callable(): void}',
'array{array{x:bool|int}, bool|int, callable(): void}',
];

yield 'simple in callable argument' => [
'callable(int|bool)',
'callable(bool|int)',
Expand Down Expand Up @@ -416,6 +439,11 @@ public static function provideSortTypesCases(): iterable
'array{0: Bar<callable(array<bool|int>|float|string): Bar|Foo>|Foo<bool|int>}',
];

yield 'complex type with Closure with $this' => [
'array<string, string|array{ string|\Closure(mixed, string, $this): (int|float) }>|false',
'array<string, array{ \Closure(mixed, string, $this): (float|int)|string }|string>|false',
];

yield 'nullable generic' => [
'?array<Foo|Bar>',
'?array<Bar|Foo>',
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixer/Basic/EncodingFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static function provideFixCases(): iterable
}

/**
* @return array{string, string|null, \SplFileInfo}
* @return array{string, null|string, \SplFileInfo}
*/
private static function prepareTestCase(string $expectedFilename, ?string $inputFilename = null): array
{
Expand Down
4 changes: 2 additions & 2 deletions tests/Tokenizer/TokensTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -623,8 +623,8 @@ public static function provideGetClearTokenAndMergeSurroundingWhitespaceCases():
}

/**
* @param ?int $expectedIndex
* @param -1|1 $direction
* @param ?int $expectedIndex
* @param -1|1 $direction
* @param list<array{int}|string|Token> $findTokens
*
* @dataProvider provideTokenOfKindSiblingCases
Expand Down

0 comments on commit 234e98f

Please sign in to comment.