Repository: PHP-CS-Fixer/PHP-CS-Fixer (php)

Issue:
Support for complex PHPDoc types in `fully_qualified_strict_types`
## Feature request

With the latest release - v3.42.0 - the `fully_qualified_strict_types` rule does fix some phpdoc to imported types, but not all.

The following code:

```php
<?php

use DataTables;
use DataTables\Database;
use DataTables\Editor;
use DataTables\SearchPaneOptions;

/**
 * @param SearchPaneOptions|callable(Database, \DataTables\Editor): (array|bool) $spInput
 */
function searchPaneOptions($spInput = null) {}
```

is fixed to:
```php
<?php

use DataTables\Database;
use DataTables\SearchPaneOptions;

/**
 * @param SearchPaneOptions|callable(Database, \DataTables\Editor): (array|bool) $spInput
 */
function searchPaneOptions($spInput = null) {}
```
applied rules: braces_position, no_unused_imports, single_line_empty_body

but I would expect the following result instead:
```php
<?php

use DataTables\Database;
use DataTables\Editor;
use DataTables\SearchPaneOptions;

/**
 * @param SearchPaneOptions|callable(Database, Editor): (array|bool) $spInput
 */
function searchPaneOptions($spInput = null) {}
```

Pull request:
feat: Support for complex PHPDoc types in `fully_qualified_strict_types`
closes #7619 🔥

needs #8076, #8077, #8106 PRs

Patch:
diff --git a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
index 7c4361b6a..763cfdf34 100644
--- a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
+++ b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
@@ -653,27 +653,7 @@ private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $name
                 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 @@ private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $name
         }
     }
 
+    /**
+     * @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
      */


Test patch:
diff --git a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
index 265378a85..b7a2b76d1 100644
--- a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
+++ b/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 @@ final class SomeClass {}',
 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) {}',
diff --git a/tests/Fixtures/Integration/misc/fully_qualified_strict_types-leading_backslash_in_global_namespace.test b/tests/Fixtures/Integration/misc/fully_qualified_strict_types-leading_backslash_in_global_namespace.test
index 271754eea..3da655d5f 100644
--- a/tests/Fixtures/Integration/misc/fully_qualified_strict_types-leading_backslash_in_global_namespace.test
+++ b/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>
  */