Browse Source

Merge branch '2.19' into 3.0

# Conflicts:
#	src/DocBlock/TypeExpression.php
Dariusz Ruminski 3 years ago
parent
commit
d78a368024

+ 1 - 0
phpunit.xml.dist

@@ -17,6 +17,7 @@
     convertWarningsToExceptions="true"
     defaultTimeLimit="10"
     enforceTimeLimit="true"
+    failOnRisky="true"
     failOnWarning="true"
     processIsolation="false"
     stopOnFailure="false"

+ 69 - 23
src/DocBlock/TypeExpression.php

@@ -29,31 +29,73 @@ final class TypeExpression
      * @internal
      */
     public const REGEX_TYPES = '
-    # <simple> is any non-array, non-generic, non-alternated type, eg `int` or `\Foo`
-    # <array> is array of <simple>, eg `int[]` or `\Foo[]`
-    # <generic> is generic collection type, like `array<string, int>`, `Collection<Item>` and more complex like `Collection<int, \null|SubCollection<string>>`
-    # <type> is <simple>, <array> or <generic> type, like `int`, `bool[]` or `Collection<ItemKey, ItemVal>`
-    # <types> is one or more types alternated via `|`, like `int|bool[]|Collection<ItemKey, ItemVal>`
-    (?<types>
-        (?<type>
-            (?<array>
-                (?&simple)(\[\])*
-            )
-            |
-            (?<simple>
-                [@$?]?[\\\\\w]+
-            )
-            |
-            (?<generic>
-                (?&simple)
-                <
-                    (?:(?&types),\s*)?(?:(?&types)|(?&generic))
-                >
+    (?<types> # alternation of several types separated by `|`
+        (?<type> # single type
+            \?? # optionally nullable
+            (?:
+                (?<object_like_array>
+                    array\h*\{
+                        (?<object_like_array_key>
+                            \h*[^?:\h]+\h*\??\h*:\h*(?&types)
+                        )
+                        (?:\h*,(?&object_like_array_key))*
+                    \h*\}
+                )
+                |
+                (?<callable> # callable syntax, e.g. `callable(string): bool`
+                    (?:callable|Closure)\h*\(\h*
+                        (?&types)
+                        (?:
+                            \h*,\h*
+                            (?&types)
+                        )*
+                    \h*\)
+                    (?:
+                        \h*\:\h*
+                        (?&types)
+                    )?
+                )
+                |
+                (?<generic> # generic syntax, e.g.: `array<int, \Foo\Bar>`
+                    (?&name)+
+                    \h*<\h*
+                        (?&types)
+                        (?:
+                            \h*,\h*
+                            (?&types)
+                        )*
+                    \h*>
+                )
+                |
+                (?<class_constant> # class constants with optional wildcard, e.g.: `Foo::*`, `Foo::CONST_A`, `FOO::CONST_*`
+                    (?&name)::(\*|\w+\*?)
+                )
+                |
+                (?<array> # array expression, e.g.: `string[]`, `string[][]`
+                    (?&name)(\[\])+
+                )
+                |
+                (?<constant> # single constant value (case insensitive), e.g.: 1, `\'a\'`
+                    (?i)
+                    null | true | false
+                    | [\d.]+
+                    | \'[^\']+?\' | "[^"]+?"
+                    | [@$]?(?:this | self | static)
+                    (?-i)
+                )
+                |
+                (?<name> # single type, e.g.: `null`, `int`, `\Foo\Bar`
+                    [\\\\\w-]++
+                )
             )
+            (?: # intersection
+                \h*&\h*
+                (?&type)
+            )*
         )
         (?:
-            \|
-            (?:(?&simple)|(?&array)|(?&generic))
+            \h*\|\h*
+            (?&type)
         )*
     )
     ';
@@ -88,7 +130,11 @@ final class TypeExpression
             );
 
             $this->types[] = $matches['type'];
-            $value = substr($value, \strlen($matches['type']) + 1);
+            $value = Preg::replace(
+                '/^'.preg_quote($matches['type'], '/').'(\h*\|\h*)?/',
+                '',
+                $value
+            );
         }
 
         $this->namespace = $namespace;

+ 3 - 1
src/Fixer/ClassNotation/FinalInternalClassFixer.php

@@ -197,7 +197,9 @@ final class FinalInternalClassFixer extends AbstractFixer implements Configurabl
         $tags = [];
 
         foreach ($doc->getAnnotations() as $annotation) {
-            Preg::match('/@\S+(?=\s|$)/', $annotation->getContent(), $matches);
+            if (1 !== Preg::match('/@\S+(?=\s|$)/', $annotation->getContent(), $matches)) {
+                continue;
+            }
             $tag = strtolower(substr(array_shift($matches), 1));
             foreach ($this->configuration['annotation_exclude'] as $tagStart => $true) {
                 if (0 === strpos($tag, $tagStart)) {

+ 5 - 0
src/Fixer/ClassUsage/DateTimeImmutableFixer.php

@@ -91,6 +91,11 @@ final class DateTimeImmutableFixer extends AbstractFixer
                 continue;
             }
 
+            $prevIndex = $tokens->getPrevMeaningfulToken($index);
+            if ($tokens[$prevIndex]->isGivenKind(T_FUNCTION)) {
+                continue;
+            }
+
             $lowercaseContent = strtolower($token->getContent());
 
             if ('datetime' === $lowercaseContent) {

+ 5 - 1
src/Fixer/Comment/CommentToPhpdocFixer.php

@@ -209,7 +209,11 @@ final class CommentToPhpdocFixer extends AbstractFixer implements ConfigurableFi
             if (false !== strpos($tokens[$index]->getContent(), '*/')) {
                 return;
             }
-            $newContent .= $indent.' *'.$this->getMessage($tokens[$index]->getContent()).$this->whitespacesConfig->getLineEnding();
+            $message = $this->getMessage($tokens[$index]->getContent());
+            if ('' !== trim(substr($message, 0, 1))) {
+                $message = ' '.$message;
+            }
+            $newContent .= $indent.' *'.$message.$this->whitespacesConfig->getLineEnding();
         }
 
         for ($index = $startIndex; $index <= $count; ++$index) {

+ 1 - 1
src/Fixer/FunctionNotation/RegularCallableCallFixer.php

@@ -118,7 +118,7 @@ call_user_func(static function ($a, $b) { var_dump($a, $b); }, 1, 2);
                 return; // first argument is an expression like `call_user_func("foo"."bar", ...)`, not supported!
             }
 
-            $newCallTokens = Tokens::fromCode('<?php '.substr($firstArgToken->getContent(), 1, -1).'();');
+            $newCallTokens = Tokens::fromCode('<?php '.substr(str_replace('\\\\', '\\', $firstArgToken->getContent()), 1, -1).'();');
             $newCallTokensSize = $newCallTokens->count();
             $newCallTokens->clearAt(0);
             $newCallTokens->clearRange($newCallTokensSize - 3, $newCallTokensSize - 1);

+ 7 - 1
src/Fixer/FunctionNotation/UseArrowFunctionsFixer.php

@@ -187,11 +187,17 @@ SAMPLE
 
     private function transform(Tokens $tokens, int $index, ?int $useStart, ?int $useEnd, int $braceOpen, int $return, int $semicolon, int $braceClose): void
     {
+        $tokensToInsert = [new Token([T_DOUBLE_ARROW, '=>'])];
+        if ($tokens->getNextMeaningfulToken($return) === $semicolon) {
+            $tokensToInsert[] = new Token([T_WHITESPACE, ' ']);
+            $tokensToInsert[] = new Token([T_STRING, 'null']);
+        }
+
         $tokens->clearRange($semicolon, $braceClose);
 
         $tokens->clearRange($braceOpen + 1, $return);
 
-        $tokens[$braceOpen] = new Token([T_DOUBLE_ARROW, '=>']);
+        $tokens->overrideRange($braceOpen, $braceOpen, $tokensToInsert);
 
         if ($useStart) {
             $tokens->clearRange($useStart, $useEnd);

+ 24 - 6
src/Fixer/Phpdoc/PhpdocTagTypeFixer.php

@@ -33,6 +33,16 @@ use Symfony\Component\OptionsResolver\Options;
  */
 final class PhpdocTagTypeFixer extends AbstractFixer implements ConfigurableFixerInterface
 {
+    private const TAG_REGEX = '/^(?:
+        (?<tag>
+            (?:@(?<tag_name>.+?)(?:\s.+)?)
+        )
+        |
+        {(?<inlined_tag>
+            (?:@(?<inlined_tag_name>.+?)(?:\s.+)?)
+        )}
+    )$/x';
+
     /**
      * {@inheritdoc}
      */
@@ -101,23 +111,31 @@ final class PhpdocTagTypeFixer extends AbstractFixer implements ConfigurableFixe
             );
 
             for ($i = 1, $max = \count($parts) - 1; $i < $max; $i += 2) {
-                if (!Preg::match('/^{?(@(.*?)(?:\s[^}]*)?)}?$/', $parts[$i], $matches)) {
+                if (!Preg::match(self::TAG_REGEX, $parts[$i], $matches)) {
                     continue;
                 }
 
-                $tag = strtolower($matches[2]);
-                if (!isset($this->configuration['tags'][$tag])) {
+                if ('' !== $matches['tag']) {
+                    $tag = $matches['tag'];
+                    $tagName = $matches['tag_name'];
+                } else {
+                    $tag = $matches['inlined_tag'];
+                    $tagName = $matches['inlined_tag_name'];
+                }
+
+                $tagName = strtolower($tagName);
+                if (!isset($this->configuration['tags'][$tagName])) {
                     continue;
                 }
 
-                if ('inline' === $this->configuration['tags'][$tag]) {
-                    $parts[$i] = '{'.$matches[1].'}';
+                if ('inline' === $this->configuration['tags'][$tagName]) {
+                    $parts[$i] = '{'.$tag.'}';
 
                     continue;
                 }
 
                 if (!$this->tagIsSurroundedByText($parts, $i)) {
-                    $parts[$i] = $matches[1];
+                    $parts[$i] = $tag;
                 }
             }
 

+ 100 - 0
tests/DocBlock/AnnotationTest.php

@@ -362,6 +362,106 @@ final class AnnotationTest extends TestCase
                 " * @return int\r\n",
                 ['int'],
             ],
+            [
+                '/** @var Collection<Foo<Bar>, Foo<Baz>>',
+                ['Collection<Foo<Bar>, Foo<Baz>>'],
+            ],
+            [
+                '/** @var int | string',
+                ['int', 'string'],
+            ],
+            [
+                '/** @var Foo::*',
+                ['Foo::*'],
+            ],
+            [
+                '/** @var Foo::A',
+                ['Foo::A'],
+            ],
+            [
+                '/** @var Foo::A|Foo::B',
+                ['Foo::A', 'Foo::B'],
+            ],
+            [
+                '/** @var Foo::A*',
+                ['Foo::A*'],
+            ],
+            [
+                '/** @var array<Foo::A*>|null',
+                ['array<Foo::A*>', 'null'],
+            ],
+            [
+                '/** @var null|true|false|1|1.5|\'a\'|"b"',
+                ['null', 'true', 'false', '1', '1.5', "'a'", '"b"'],
+            ],
+            [
+                '/** @param int | "a" | A<B<C, D>, E<F::*|G[]>> $foo */',
+                ['int', '"a"', 'A<B<C, D>, E<F::*|G[]>>'],
+            ],
+            [
+                '/** @var class-string<Foo> */',
+                ['class-string<Foo>'],
+            ],
+            [
+                '/** @var A&B */',
+                ['A&B'],
+            ],
+            [
+                '/** @var A & B */',
+                ['A & B'],
+            ],
+            [
+                '/** @var array{1: bool, 2: bool} */',
+                ['array{1: bool, 2: bool}'],
+            ],
+            [
+                '/** @var array{a: int|string, b?: bool} */',
+                ['array{a: int|string, b?: bool}'],
+            ],
+            [
+                '/** @var array{\'a\': "a", "b"?: \'b\'} */',
+                ['array{\'a\': "a", "b"?: \'b\'}'],
+            ],
+            [
+                '/** @var array { a : int | string , b ? : A<B, C> } */',
+                ['array { a : int | string , b ? : A<B, C> }'],
+            ],
+            [
+                '/** @param callable(string) $function',
+                ['callable(string)'],
+            ],
+            [
+                '/** @param callable(string): bool $function',
+                ['callable(string): bool'],
+            ],
+            [
+                '/** @param callable(array<int, string>, array<int, Foo>): bool $function',
+                ['callable(array<int, string>, array<int, Foo>): bool'],
+            ],
+            [
+                '/** @param array<int, callable(string): bool> $function',
+                ['array<int, callable(string): bool>'],
+            ],
+            [
+                '/** @param callable(string): callable(int) $function',
+                ['callable(string): callable(int)'],
+            ],
+            [
+                '/** @param callable(string) : callable(int) : bool $function',
+                ['callable(string) : callable(int) : bool'],
+            ],
+            [
+                '* @param TheCollection<callable(Foo, Bar,Baz): Foo[]>|string[]|null $x',
+                ['TheCollection<callable(Foo, Bar,Baz): Foo[]>', 'string[]', 'null'],
+            ],
+            [
+                '/** @param Closure(string) $function',
+                ['Closure(string)'],
+            ],
+            [
+                '/** @param   array  <  int   , callable  (  string  )  :   bool  > $function',
+                ['array  <  int   , callable  (  string  )  :   bool  >'],
+            ],
         ];
     }
 

+ 25 - 0
tests/DocBlock/TypeExpressionTest.php

@@ -60,6 +60,31 @@ final class TypeExpressionTest extends TestCase
         yield ['@this', ['@this']];
         yield ['$SELF|int', ['$SELF', 'int']];
         yield ['array<string|int, string>', ['array<string|int, string>']];
+        yield ['Collection<Foo<Bar>, Foo<Baz>>', ['Collection<Foo<Bar>, Foo<Baz>>']];
+        yield ['int | string', ['int', 'string']];
+        yield ['Foo::*', ['Foo::*']];
+        yield ['Foo::A', ['Foo::A']];
+        yield ['Foo::A|Foo::B', ['Foo::A', 'Foo::B']];
+        yield ['Foo::A*', ['Foo::A*']];
+        yield ['array<Foo::A*>|null', ['array<Foo::A*>', 'null']];
+        yield ['null|true|false|1|1.5|\'a\'|"b"', ['null', 'true', 'false', '1', '1.5', "'a'", '"b"']];
+        yield ['int | "a" | A<B<C, D>, E<F::*|G[]>>', ['int', '"a"', 'A<B<C, D>, E<F::*|G[]>>']];
+        yield ['class-string<Foo>', ['class-string<Foo>']];
+        yield ['A&B', ['A&B']];
+        yield ['A & B', ['A & B']];
+        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\': "a", "b"?: \'b\'}', ['array{\'a\': "a", "b"?: \'b\'}']];
+        yield ['array { a : int | string , b ? : A<B, C> }', ['array { a : int | string , b ? : A<B, C> }']];
+        yield ['callable(string)', ['callable(string)']];
+        yield ['callable(string): bool', ['callable(string): bool']];
+        yield ['callable(array<int, string>, array<int, Foo>): bool', ['callable(array<int, string>, array<int, Foo>): bool']];
+        yield ['array<int, callable(string): bool>', ['array<int, callable(string): bool>']];
+        yield ['callable(string): callable(int)', ['callable(string): callable(int)']];
+        yield ['callable(string) : callable(int) : bool', ['callable(string) : callable(int) : bool']];
+        yield ['TheCollection<callable(Foo, Bar,Baz): Foo[]>|string[]|null', ['TheCollection<callable(Foo, Bar,Baz): Foo[]>', 'string[]', 'null']];
+        yield ['Closure(string)', ['Closure(string)']];
+        yield ['array  <  int   , callable  (  string  )  :   bool  >', ['array  <  int   , callable  (  string  )  :   bool  >']];
     }
 
     /**

Some files were not shown because too many files changed in this diff