Просмотр исходного кода

bug: Fix array/object shape phpdoc type parse (#6962)

Michael Voříšek 1 год назад
Родитель
Сommit
234e98f114

+ 31 - 30
src/DocBlock/TypeExpression.php

@@ -35,13 +35,14 @@ final class TypeExpression
             (?<nullable>\??)
             (?<nullable>\??)
             (?:
             (?:
                 (?<object_like_array>
                 (?<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*\}
                     \h*\}
                 )
                 )
                 |
                 |
@@ -343,9 +344,9 @@ final class TypeExpression
         }
         }
 
 
         if ('' !== ($matches['object_like_array'] ?? '')) {
         if ('' !== ($matches['object_like_array'] ?? '')) {
-            $this->parseObjectLikeArrayKeys(
+            $this->parseObjectLikeArrayInnerTypes(
                 $index + \strlen($matches['object_like_array_start']),
                 $index + \strlen($matches['object_like_array_start']),
-                $matches['object_like_array_keys']
+                $matches['object_like_array_inners'] ?? ''
             );
             );
 
 
             return;
             return;
@@ -397,7 +398,7 @@ final class TypeExpression
     {
     {
         while ('' !== $value) {
         while ('' !== $value) {
             Preg::match(
             Preg::match(
-                '{^'.self::REGEX_TYPES.'\h*(?:,|$)}x',
+                '{^'.self::REGEX_TYPES.'(?:\h*,\h*|$)}x',
                 $value,
                 $value,
                 $matches
                 $matches
             );
             );
@@ -407,39 +408,39 @@ final class TypeExpression
                 'expression' => $this->inner($matches['types']),
                 '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) {
         while ('' !== $value) {
             Preg::match(
             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,
                 $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[] = [
             $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);
         }
         }
     }
     }
 
 

+ 12 - 12
src/Tokenizer/Tokens.php

@@ -204,7 +204,7 @@ class Tokens extends \SplFixedArray
     }
     }
 
 
     /**
     /**
-     * @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
     public static function getBlockEdgeDefinitions(): array
     {
     {
@@ -535,9 +535,9 @@ class Tokens extends \SplFixedArray
      *
      *
      * This method is shorthand for getTokenOfKindSibling method.
      * 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 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
     public function getNextTokenOfKind(int $index, array $tokens = [], bool $caseSensitive = true): ?int
     {
     {
@@ -583,9 +583,9 @@ class Tokens extends \SplFixedArray
      * Get index for closest previous token of given kind.
      * Get index for closest previous token of given kind.
      * This method is shorthand for getTokenOfKindSibling method.
      * 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 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
     public function getPrevTokenOfKind(int $index, array $tokens = [], bool $caseSensitive = true): ?int
     {
     {
@@ -595,10 +595,10 @@ class Tokens extends \SplFixedArray
     /**
     /**
      * Get index for closest sibling token of given kind.
      * 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
     public function getTokenOfKindSibling(int $index, int $direction, array $tokens = [], bool $caseSensitive = true): ?int
     {
     {
@@ -626,9 +626,9 @@ class Tokens extends \SplFixedArray
     /**
     /**
      * Get index for closest sibling token not of given kind.
      * 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
     public function getTokenNotOfKindSibling(int $index, int $direction, array $tokens = []): ?int
     {
     {

+ 28 - 0
tests/DocBlock/TypeExpressionTest.php

@@ -103,6 +103,10 @@ final class TypeExpressionTest extends TestCase
 
 
         yield ['A & B', ['A', 'B']];
         yield ['A & B', ['A', 'B']];
 
 
+        yield ['array{}', ['array{}']];
+
+        yield ['object{ }', ['object{ }']];
+
         yield ['array{1: bool, 2: bool}', ['array{1: bool, 2: bool}']];
         yield ['array{1: bool, 2: bool}', ['array{1: bool, 2: bool}']];
 
 
         yield ['array{a: int|string, b?: bool}', ['array{a: int|string, b?: bool}']];
         yield ['array{a: int|string, b?: bool}', ['array{a: int|string, b?: bool}']];
@@ -111,6 +115,10 @@ final class TypeExpressionTest extends TestCase
 
 
         yield ['array { a : int | string , b ? : A<B, C> }', ['array { a : int | string , b ? : A<B, C> }']];
         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)', ['callable(string)']];
 
 
         yield ['callable(string): bool', ['callable(string): bool']];
         yield ['callable(string): bool', ['callable(string): bool']];
@@ -381,6 +389,21 @@ final class TypeExpressionTest extends TestCase
             'array{0: bool|int, "foo": bool|int}',
             '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' => [
         yield 'simple in callable argument' => [
             'callable(int|bool)',
             'callable(int|bool)',
             'callable(bool|int)',
             'callable(bool|int)',
@@ -416,6 +439,11 @@ final class TypeExpressionTest extends TestCase
             'array{0: Bar<callable(array<bool|int>|float|string): Bar|Foo>|Foo<bool|int>}',
             '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' => [
         yield 'nullable generic' => [
             '?array<Foo|Bar>',
             '?array<Foo|Bar>',
             '?array<Bar|Foo>',
             '?array<Bar|Foo>',

+ 1 - 1
tests/Fixer/Basic/EncodingFixerTest.php

@@ -43,7 +43,7 @@ final class EncodingFixerTest extends AbstractFixerTestCase
     }
     }
 
 
     /**
     /**
-     * @return array{string, string|null, \SplFileInfo}
+     * @return array{string, null|string, \SplFileInfo}
      */
      */
     private static function prepareTestCase(string $expectedFilename, ?string $inputFilename = null): array
     private static function prepareTestCase(string $expectedFilename, ?string $inputFilename = null): array
     {
     {

+ 2 - 2
tests/Tokenizer/TokensTest.php

@@ -623,8 +623,8 @@ PHP;
     }
     }
 
 
     /**
     /**
-     * @param ?int $expectedIndex
-     * @param -1|1 $direction
+     * @param ?int                          $expectedIndex
+     * @param -1|1                          $direction
      * @param list<array{int}|string|Token> $findTokens
      * @param list<array{int}|string|Token> $findTokens
      *
      *
      * @dataProvider provideTokenOfKindSiblingCases
      * @dataProvider provideTokenOfKindSiblingCases