Browse Source

feat: Support for complex PHPDoc types in `fully_qualified_strict_types` (#8085)

Michael Voříšek 5 months ago
parent
commit
09bc892bd7

+ 35 - 21
src/Fixer/Import/FullyQualifiedStrictTypesFixer.php

@@ -653,27 +653,7 @@ class Foo extends \Other\BaseClass implements \Other\Interface1, \Other\Interfac
                 return $matches[0];
             }
 
-            /** @TODO parse the complex type using TypeExpression and fix all names inside (like `list<\Foo\Bar|'a|b|c'|string>` or `\Foo\Bar[]`) */
-            $unsupported = false;
-
-            return $matches[1].$matches[2].$matches[3].implode('|', array_map(function ($v) use ($uses, $namespaceName, &$unsupported) {
-                /** @var class-string $v */
-                if ($unsupported || !Preg::match('/^'.self::REGEX_CLASS.'$/', $v)) {
-                    $unsupported = true;
-
-                    return $v;
-                }
-
-                $shortTokens = $this->determineShortType($v, 'class', $uses, $namespaceName);
-                if (null === $shortTokens) {
-                    return $v;
-                }
-
-                return implode('', array_map(
-                    static fn (Token $token) => $token->getContent(),
-                    $shortTokens
-                ));
-            }, explode('|', $matches[4])));
+            return $matches[1].$matches[2].$matches[3].$this->fixPhpDocType($matches[4], $uses, $namespaceName);
         }, $phpDocContent);
 
         if ($phpDocContentNew !== $phpDocContent) {
@@ -681,6 +661,40 @@ class Foo extends \Other\BaseClass implements \Other\Interface1, \Other\Interfac
         }
     }
 
+    /**
+     * @param _Uses $uses
+     */
+    private function fixPhpDocType(string $type, array $uses, string $namespaceName): string
+    {
+        $typeExpression = new TypeExpression($type, null, []);
+
+        $typeExpression = $typeExpression->mapTypes(function (TypeExpression $type) use ($uses, $namespaceName) {
+            $currentTypeValue = $type->toString();
+
+            if ($type->isCompositeType() || !Preg::match('/^'.self::REGEX_CLASS.'$/', $currentTypeValue)) {
+                return $type;
+            }
+
+            /** @var class-string $currentTypeValue */
+            $shortTokens = $this->determineShortType($currentTypeValue, 'class', $uses, $namespaceName);
+
+            if (null === $shortTokens) {
+                return $type;
+            }
+
+            $newTypeValue = implode('', array_map(
+                static fn (Token $token) => $token->getContent(),
+                $shortTokens
+            ));
+
+            return $currentTypeValue === $newTypeValue
+                ? $type
+                : new TypeExpression($newTypeValue, null, []);
+        });
+
+        return $typeExpression->toString();
+    }
+
     /**
      * @param _Uses $uses
      */

+ 46 - 3
tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php

@@ -2163,14 +2163,40 @@ function foo($v) {}',
             ['import_symbols' => true],
         ];
 
+        yield 'Test complex PHPDoc with imports' => [
+            <<<'EOD'
+                <?php
+
+                namespace Ns;
+                use X\A;
+
+                /**
+                 * @param \Closure(A&\X\B, '\Y\A&(\Y\B|\Y\C)', array{A: B}): ($v is string ? Foo : Bar) $v
+                 */
+                function foo($v) {}
+                EOD,
+            <<<'EOD'
+                <?php
+
+                namespace Ns;
+
+                /**
+                 * @param \Closure(\X\A&\X\B, '\Y\A&(\Y\B|\Y\C)', array{A: B}): ($v is string ? \Ns\Foo : \Ns\Bar) $v
+                 */
+                function foo($v) {}
+                EOD,
+            ['import_symbols' => true],
+        ];
+
         yield 'Test PHPDoc string must be kept as is' => [
             '<?php
 
 namespace Ns;
 use Other\Foo;
+use Other\Foo2;
 
 /**
- * @param Foo|\'\Other\Bar|\Other\Bar2|\Other\Bar3\'|\Other\Foo2 $v
+ * @param Foo|\'\Other\Bar|\Other\Bar2|\Other\Bar3\'|Foo2 $v
  */
 function foo($v) {}',
             '<?php
@@ -2275,9 +2301,26 @@ namespace Foo\Bar;
 final class SomeClass {}',
         ];
 
-        yield 'PHPDoc with generics must not crash' => [
+        yield 'PHPDoc with generics - without namespace' => [
+            '<?php
+
+/**
+ * @param Iterator<mixed, SplFileInfo> $iter
+ */
+function foo($iter) {}',
             '<?php
 
+/**
+ * @param \Iterator<mixed, \SplFileInfo> $iter
+ */
+function foo($iter) {}',
+        ];
+
+        yield 'PHPDoc with generics - with namespace' => [
+            '<?php
+
+namespace Ns;
+
 /**
  * @param \Iterator<mixed, \SplFileInfo> $iter
  */
@@ -2487,7 +2530,7 @@ function foo($dateTime, $fx) {}',
  * @phpstan-param positive-int $v
  * @param \'GET\'|\'POST\' $method
  * @param \Closure $fx
- * @psalm-param Closure(): (callable(): Closure) $fx
+ * @psalm-param \Closure(): (callable(): \Closure) $fx
  * @return list<int>
  */
 function foo($v, $method, $fx) {}',

+ 1 - 1
tests/Fixtures/Integration/misc/fully_qualified_strict_types-leading_backslash_in_global_namespace.test

@@ -17,7 +17,7 @@ Integration of fixers: fully_qualified_strict_types /w leading_backslash_in_glob
  * @param \'GET\'|\'POST\' $method
  * @param \Closure $fx
  *
- * @psalm-param Closure(): (callable(): Closure) $fx
+ * @psalm-param \Closure(): (callable(): \Closure) $fx
  *
  * @return list<int>
  */