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

Issue:
Missing a FQCN to Import (use) conversion
## Rule request

Either I'm poorly looking for it, or a fairly (IMO) important rule is missing.

The FQCN is not replaced by the Import (use keyword), unless this import had been already added.

### Code snippet that shows how the rule would fix PHP code

#### How it is (already after `php-cs-fixer fix`):

  ```php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class TestController extends AbstractController
{
    public function index(): \Symfony\Component\HttpFoundation\Response
    {
        return new \Symfony\Component\HttpFoundation\Response();
    }
}
  ```

#### What would I expect:

```php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class TestController extends AbstractController
{
    public function index(): Response
    {
        return new Response();
    }
}
```

Only when I manually add `use Symfony\Component\HttpFoundation\Response;` and then run cs-fixer, it will do the necessary conversion in the class body.


Pull request description:
feat: Ability to import FQCNs found during analysis
Fixes #6582 🎉 .

Patch:
diff --git a/dev-tools/phpstan/baseline.php b/dev-tools/phpstan/baseline.php
index f7be28fc8..cbe253599 100644
--- a/dev-tools/phpstan/baseline.php
+++ b/dev-tools/phpstan/baseline.php
@@ -221,21 +221,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/Import/GlobalNamespaceImportFixer.php',
 ];
-$ignoreErrors[] = [
-	'message' => '#^Method PhpCsFixer\\\\Fixer\\\\Import\\\\GlobalNamespaceImportFixer\\:\\:filterUseDeclarations\\(\\) return type has no value type specified in iterable type array\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Import/GlobalNamespaceImportFixer.php',
-];
-$ignoreErrors[] = [
-	'message' => '#^Method PhpCsFixer\\\\Fixer\\\\Import\\\\GlobalNamespaceImportFixer\\:\\:insertImports\\(\\) has parameter \\$imports with no value type specified in iterable type array\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Import/GlobalNamespaceImportFixer.php',
-];
-$ignoreErrors[] = [
-	'message' => '#^Method PhpCsFixer\\\\Fixer\\\\Import\\\\GlobalNamespaceImportFixer\\:\\:prepareImports\\(\\) has parameter \\$global with no value type specified in iterable type array\\.$#',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/Import/GlobalNamespaceImportFixer.php',
-];
 $ignoreErrors[] = [
 	'message' => '#^Method PhpCsFixer\\\\Fixer\\\\Import\\\\OrderedImportsFixer\\:\\:getNewOrder\\(\\) return type has no value type specified in iterable type array\\.$#',
 	'count' => 1,
diff --git a/doc/rules/import/fully_qualified_strict_types.rst b/doc/rules/import/fully_qualified_strict_types.rst
index 09afc46e2..ed9f59a61 100644
--- a/doc/rules/import/fully_qualified_strict_types.rst
+++ b/doc/rules/import/fully_qualified_strict_types.rst
@@ -10,6 +10,15 @@ interfaces.
 Configuration
 -------------
 
+``import_symbols``
+~~~~~~~~~~~~~~~~~~
+
+Whether FQCNs found during analysis should be automatically imported.
+
+Allowed types: ``bool``
+
+Default value: ``false``
+
 ``leading_backslash_in_global_namespace``
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -122,6 +131,46 @@ With configuration: ``['leading_backslash_in_global_namespace' => true]``.
         }
     }
 
+Example #4
+~~~~~~~~~~
+
+With configuration: ``['import_symbols' => true]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+    namespace Foo\Test;
+   +use Other\BaseClass;
+   +use Other\CaughtThrowable;
+   +use Other\FunctionArgument;
+   +use Other\FunctionReturnType;
+   +use Other\Interface1;
+   +use Other\Interface2;
+   +use Other\PropertyPhpDoc;
+   +use Other\StaticFunctionCall;
+
+   -class Foo extends \Other\BaseClass implements \Other\Interface1, \Other\Interface2
+   +class Foo extends BaseClass implements Interface1, Interface2
+    {
+   -    /** @var \Other\PropertyPhpDoc */
+   +    /** @var PropertyPhpDoc */
+        private $array;
+   -    public function __construct(\Other\FunctionArgument $arg) {}
+   -    public function foo(): \Other\FunctionReturnType
+   +    public function __construct(FunctionArgument $arg) {}
+   +    public function foo(): FunctionReturnType
+        {
+            try {
+   -            \Other\StaticFunctionCall::bar();
+   -        } catch (\Other\CaughtThrowable $e) {}
+   +            StaticFunctionCall::bar();
+   +        } catch (CaughtThrowable $e) {}
+        }
+    }
+
 Rule sets
 ---------
 
diff --git a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
index 7ff833996..b306d1fe3 100644
--- a/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
+++ b/src/Fixer/Import/FullyQualifiedStrictTypesFixer.php
@@ -16,6 +16,7 @@
 
 use PhpCsFixer\AbstractFixer;
 use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
 use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
 use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
 use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
@@ -27,6 +28,7 @@
 use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
 use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
 use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Processor\ImportProcessor;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 
@@ -36,8 +38,17 @@
  * @author Greg Korba <greg@codito.dev>
  * @author SpacePossum <possumfromspace@gmail.com>
  */
-final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface
+final class FullyQualifiedStrictTypesFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
 {
+    /**
+     * @var array{
+     *     const?: array<string, class-string>,
+     *     class?: array<string, class-string>,
+     *     function?: array<string, class-string>
+     * }
+     */
+    private array $symbolsForImport = [];
+
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
@@ -113,6 +124,26 @@ class SomeClass implements \Foo\Bar\Baz
 ',
                     ['leading_backslash_in_global_namespace' => true]
                 ),
+                new CodeSample(
+                    '<?php
+
+namespace Foo\Test;
+
+class Foo extends \Other\BaseClass implements \Other\Interface1, \Other\Interface2
+{
+    /** @var \Other\PropertyPhpDoc */
+    private $array;
+    public function __construct(\Other\FunctionArgument $arg) {}
+    public function foo(): \Other\FunctionReturnType
+    {
+        try {
+            \Other\StaticFunctionCall::bar();
+        } catch (\Other\CaughtThrowable $e) {}
+    }
+}
+',
+                    ['import_symbols' => true]
+                ),
             ]
         );
     }
@@ -120,7 +151,7 @@ class SomeClass implements \Foo\Bar\Baz
     /**
      * {@inheritdoc}
      *
-     * Must run before NoSuperfluousPhpdocTagsFixer.
+     * Must run before NoSuperfluousPhpdocTagsFixer, OrderedImportsFixer, StatementIndentationFixer.
      * Must run after PhpdocToReturnTypeFixer.
      */
     public function getPriority(): int
@@ -150,6 +181,13 @@ protected function createConfigurationDefinition(): FixerConfigurationResolverIn
                 ->setAllowedTypes(['bool'])
                 ->setDefault(false)
                 ->getOption(),
+            (new FixerOptionBuilder(
+                'import_symbols',
+                'Whether FQCNs found during analysis should be automatically imported.'
+            ))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(false)
+                ->getOption(),
         ]);
     }
 
@@ -161,9 +199,11 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
         foreach ($tokens->getNamespaceDeclarations() as $namespace) {
             $namespaceName = strtolower($namespace->getFullName());
             $uses = [];
+            $lastUse = null;
 
             foreach ($namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace) as $use) {
-                $uses[strtolower(ltrim($use->getFullName(), '\\'))] = $use->getShortName();
+                $uses[$this->normaliseSymbolName($use->getFullName())] = $use->getShortName();
+                $lastUse = $use;
             }
 
             for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex(); ++$index) {
@@ -181,6 +221,15 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
                     $this->fixPhpDoc($tokens, $index, $uses, $namespaceName);
                 }
             }
+
+            if (true === $this->configuration['import_symbols'] && [] !== $this->symbolsForImport) {
+                $atIndex = (null !== $lastUse) ? $lastUse->getEndIndex() + 1 : $namespace->getEndIndex() + 1;
+
+                // Insert all registered FQCNs
+                $this->createImportProcessor()->insertImports($tokens, $this->symbolsForImport, $atIndex);
+
+                $this->symbolsForImport = [];
+            }
         }
     }
 
@@ -221,6 +270,10 @@ private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $name
                     continue;
                 }
 
+                if (true === $this->configuration['import_symbols'] && isset($matches[2][0])) {
+                    $this->registerSymbolForImport('class', $matches[2][0], $uses, $namespaceName);
+                }
+
                 $shortTokens = $this->determineShortType($typeName, $uses, $namespaceName);
 
                 if (null !== $shortTokens) {
@@ -245,6 +298,8 @@ private function fixPhpDoc(Tokens $tokens, int $index, array $uses, string $name
      */
     private function fixExtendsImplements(Tokens $tokens, int $index, array $uses, string $namespaceName): void
     {
+        // We handle `extends` and `implements` with similar logic, but we need to exit the loop under different conditions.
+        $isExtends = $tokens[$index]->equals([T_EXTENDS]);
         $index = $tokens->getNextMeaningfulToken($index);
         $extend = ['content' => '', 'tokens' => []];
 
@@ -252,7 +307,7 @@ private function fixExtendsImplements(Tokens $tokens, int $index, array $uses, s
             if ($tokens[$index]->equalsAny([',', '{', [T_IMPLEMENTS]])) {
                 $this->shortenClassIfPossible($tokens, $extend, $uses, $namespaceName);
 
-                if ($tokens[$index]->equals('{')) {
+                if ($tokens[$index]->equalsAny($isExtends ? [[T_IMPLEMENTS], '{'] : ['{'])) {
                     break;
                 }
 
@@ -328,6 +383,10 @@ private function shortenClassIfPossible(Tokens $tokens, array $class, array $use
     {
         $longTypeContent = $class['content'];
 
+        if (true === $this->configuration['import_symbols']) {
+            $this->registerSymbolForImport('class', $longTypeContent, $uses, $namespaceName);
+        }
+
         if (str_starts_with($longTypeContent, '\\')) {
             $typeName = substr($longTypeContent, 1);
             $typeNameLower = strtolower($typeName);
@@ -382,6 +441,10 @@ private function replaceByShortType(Tokens $tokens, TypeAnalysis $type, array $u
                 return;
             }
 
+            if (true === $this->configuration['import_symbols']) {
+                $this->registerSymbolForImport('class', $typeName, $uses, $namespaceName);
+            }
+
             $shortType = $this->determineShortType($typeName, $uses, $namespaceName);
 
             if (null !== $shortType) {
@@ -552,4 +615,60 @@ private function namespacedStringToTokens(string $input, bool $withLeadingBacksl
 
         return $tokens;
     }
+
+    /**
+     * We need to create import processor dynamically (not in costructor), because actual whitespace configuration
+     * is set later, not when fixer's instance is created.
+     */
+    private function createImportProcessor(): ImportProcessor
+    {
+        return new ImportProcessor($this->whitespacesConfig);
+    }
+
+    /**
+     * @param "class"|"const"|"function" $kind
+     * @param class-string               $symbol
+     * @param array<string, string>      $uses
+     */
+    private function registerSymbolForImport(string $kind, string $symbol, array &$uses, string $namespaceName): void
+    {
+        $normalisedName = $this->normaliseSymbolName($symbol);
+
+        // Do NOT register symbol for importing if:
+        if (
+            // we already have the symbol in existing imports
+            isset($uses[$normalisedName])
+            // or if the symbol is not a FQCN
+            || !str_starts_with($symbol, '\\')
+            // or if it's a global symbol
+            || strpos($symbol, '\\') === strrpos($symbol, '\\')
+        ) {
+            return;
+        }
+
+        $shortSymbol = substr($symbol, strrpos($symbol, '\\') + 1);
+        $importedShortNames = array_map(
+            static fn (string $name): string => strtolower($name),
+            array_values($uses)
+        );
+
+        // If symbol
+        if (\in_array(strtolower($shortSymbol), $importedShortNames, true)) {
+            return;
+        }
+
+        $this->symbolsForImport[$kind][$normalisedName] = ltrim($symbol, '\\');
+        ksort($this->symbolsForImport[$kind], SORT_NATURAL);
+
+        // We must fake that the symbol is imported, so that it can be shortened.
+        $uses[$normalisedName] = $shortSymbol;
+    }
+
+    /**
+     * @param class-string $name
+     */
+    private function normaliseSymbolName(string $name): string
+    {
+        return strtolower(ltrim($name, '\\'));
+    }
 }
diff --git a/src/Fixer/Import/GlobalNamespaceImportFixer.php b/src/Fixer/Import/GlobalNamespaceImportFixer.php
index 16447192c..9d2e528fe 100644
--- a/src/Fixer/Import/GlobalNamespaceImportFixer.php
+++ b/src/Fixer/Import/GlobalNamespaceImportFixer.php
@@ -31,6 +31,7 @@
 use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
 use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
 use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Processor\ImportProcessor;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 use PhpCsFixer\Tokenizer\TokensAnalyzer;
@@ -40,6 +41,15 @@
  */
 final class GlobalNamespaceImportFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
 {
+    private ImportProcessor $importProcessor;
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->importProcessor = new ImportProcessor($this->whitespacesConfig);
+    }
+
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
@@ -90,7 +100,7 @@ public function getDefinition(): FixerDefinitionInterface
     /**
      * {@inheritdoc}
      *
-     * Must run before NoUnusedImportsFixer, OrderedImportsFixer.
+     * Must run before NoUnusedImportsFixer, OrderedImportsFixer, StatementIndentationFixer.
      * Must run after NativeConstantInvocationFixer, NativeFunctionInvocationFixer.
      */
     public function getPriority(): int
@@ -139,7 +149,15 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
         $newImports = array_filter($newImports);
 
         if (\count($newImports) > 0) {
-            $this->insertImports($tokens, $newImports, $useDeclarations);
+            if (\count($useDeclarations) > 0) {
+                $useDeclaration = end($useDeclarations);
+                $atIndex = $useDeclaration->getEndIndex() + 1;
+            } else {
+                $namespace = $tokens->getNamespaceDeclarations()[0];
+                $atIndex = $namespace->getEndIndex() + 1;
+            }
+
+            $this->importProcessor->insertImports($tokens, $newImports, $atIndex);
         }
     }
 
@@ -391,16 +409,17 @@ private function importClasses(Tokens $tokens, array $useDeclarations): array
             }
         }
 
-        return $imports + $this->prepareImports($tokens, $indices, $global, $other, false);
+        return array_merge($imports, $this->prepareImports($tokens, $indices, $global, $other, false));
     }
 
     /**
      * Removes the leading slash at the given indices (when the name is not already used).
      *
-     * @param int[]               $indices
-     * @param array<string, true> $other
+     * @param int[]                      $indices
+     * @param array<string, string|true> $global
+     * @param array<string, true>        $other
      *
-     * @return array<string, string> array keys contain the names that must be imported
+     * @return array<string, class-string> array keys contain the names that must be imported
      */
     private function prepareImports(Tokens $tokens, array $indices, array $global, array $other, bool $caseSensitive): array
     {
@@ -426,49 +445,6 @@ private function prepareImports(Tokens $tokens, array $indices, array $global, a
         return $imports;
     }
 
-    /**
-     * @param NamespaceUseAnalysis[] $useDeclarations
-     */
-    private function insertImports(Tokens $tokens, array $imports, array $useDeclarations): void
-    {
-        if (\count($useDeclarations) > 0) {
-            $useDeclaration = end($useDeclarations);
-            $index = $useDeclaration->getEndIndex() + 1;
-        } else {
-            $namespace = $tokens->getNamespaceDeclarations()[0];
-            $index = $namespace->getEndIndex() + 1;
-        }
-
-        $lineEnding = $this->whitespacesConfig->getLineEnding();
-
-        if (!$tokens[$index]->isWhitespace() || !str_contains($tokens[$index]->getContent(), "\n")) {
-            $tokens->insertAt($index, new Token([T_WHITESPACE, $lineEnding]));
-        }
-
-        foreach ($imports as $type => $typeImports) {
-            foreach ($typeImports as $name) {
-                $items = [
-                    new Token([T_WHITESPACE, $lineEnding]),
-                    new Token([T_USE, 'use']),
-                    new Token([T_WHITESPACE, ' ']),
-                ];
-
-                if ('const' === $type) {
-                    $items[] = new Token([CT::T_CONST_IMPORT, 'const']);
-                    $items[] = new Token([T_WHITESPACE, ' ']);
-                } elseif ('function' === $type) {
-                    $items[] = new Token([CT::T_FUNCTION_IMPORT, 'function']);
-                    $items[] = new Token([T_WHITESPACE, ' ']);
-                }
-
-                $items[] = new Token([T_STRING, $name]);
-                $items[] = new Token(';');
-
-                $tokens->insertAt($index, $items);
-            }
-        }
-    }
-
     /**
      * @param NamespaceUseAnalysis[] $useDeclarations
      */
@@ -480,7 +456,7 @@ private function fullyQualifyConstants(Tokens $tokens, array $useDeclarations):
 
         [$global] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isConstant() && !$declaration->isAliased(), true);
 
-        if (!$global) {
+        if ([] === $global) {
             return;
         }
 
@@ -520,7 +496,7 @@ private function fullyQualifyFunctions(Tokens $tokens, array $useDeclarations):
 
         [$global] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isFunction() && !$declaration->isAliased(), false);
 
-        if (!$global) {
+        if ([] === $global) {
             return;
         }
 
@@ -560,7 +536,7 @@ private function fullyQualifyClasses(Tokens $tokens, array $useDeclarations): vo
 
         [$global] = $this->filterUseDeclarations($useDeclarations, static fn (NamespaceUseAnalysis $declaration): bool => $declaration->isClass() && !$declaration->isAliased(), false);
 
-        if (!$global) {
+        if ([] === $global) {
             return;
         }
 
@@ -609,6 +585,8 @@ private function fullyQualifyClasses(Tokens $tokens, array $useDeclarations): vo
 
     /**
      * @param NamespaceUseAnalysis[] $declarations
+     *
+     * @return array{0: array<string, string|true>, 1: array<string, true>}
      */
     private function filterUseDeclarations(array $declarations, callable $callback, bool $caseSensitive): array
     {
diff --git a/src/Fixer/Import/OrderedImportsFixer.php b/src/Fixer/Import/OrderedImportsFixer.php
index 403677469..e23dd7118 100644
--- a/src/Fixer/Import/OrderedImportsFixer.php
+++ b/src/Fixer/Import/OrderedImportsFixer.php
@@ -175,7 +175,7 @@ public function getDefinition(): FixerDefinitionInterface
      * {@inheritdoc}
      *
      * Must run before BlankLineBetweenImportGroupsFixer.
-     * Must run after GlobalNamespaceImportFixer, NoLeadingImportSlashFixer.
+     * Must run after FullyQualifiedStrictTypesFixer, GlobalNamespaceImportFixer, NoLeadingImportSlashFixer.
      */
     public function getPriority(): int
     {
diff --git a/src/Fixer/Whitespace/StatementIndentationFixer.php b/src/Fixer/Whitespace/StatementIndentationFixer.php
index d22d50dd4..d15f56c8a 100644
--- a/src/Fixer/Whitespace/StatementIndentationFixer.php
+++ b/src/Fixer/Whitespace/StatementIndentationFixer.php
@@ -64,7 +64,7 @@ public function getDefinition(): FixerDefinitionInterface
      * {@inheritdoc}
      *
      * Must run before HeredocIndentationFixer.
-     * Must run after BracesPositionFixer, ClassAttributesSeparationFixer, CurlyBracesPositionFixer, MethodArgumentSpaceFixer, NoUselessElseFixer, YieldFromArrayToYieldsFixer.
+     * Must run after BracesPositionFixer, ClassAttributesSeparationFixer, CurlyBracesPositionFixer, FullyQualifiedStrictTypesFixer, GlobalNamespaceImportFixer, MethodArgumentSpaceFixer, NoUselessElseFixer, YieldFromArrayToYieldsFixer.
      */
     public function getPriority(): int
     {
diff --git a/src/Tokenizer/Processor/ImportProcessor.php b/src/Tokenizer/Processor/ImportProcessor.php
new file mode 100644
index 000000000..35fb0f9d4
--- /dev/null
+++ b/src/Tokenizer/Processor/ImportProcessor.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tokenizer\Processor;
+
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\WhitespacesFixerConfig;
+
+/**
+ * @author Greg Korba <greg@codito.dev>
+ */
+final class ImportProcessor
+{
+    private WhitespacesFixerConfig $whitespacesConfig;
+
+    public function __construct(WhitespacesFixerConfig $whitespacesConfig)
+    {
+        $this->whitespacesConfig = $whitespacesConfig;
+    }
+
+    /**
+     * @param array{
+     *     const?: array<int|string, class-string>,
+     *     class?: array<int|string, class-string>,
+     *     function?: array<int|string, class-string>
+     * } $imports
+     */
+    public function insertImports(Tokens $tokens, array $imports, int $atIndex): void
+    {
+        $lineEnding = $this->whitespacesConfig->getLineEnding();
+
+        if (!$tokens[$atIndex]->isWhitespace() || !str_contains($tokens[$atIndex]->getContent(), "\n")) {
+            $tokens->insertAt($atIndex, new Token([T_WHITESPACE, $lineEnding]));
+        }
+
+        foreach ($imports as $type => $typeImports) {
+            sort($typeImports);
+
+            $items = [];
+
+            foreach ($typeImports as $name) {
+                $items = array_merge($items, [
+                    new Token([T_WHITESPACE, $lineEnding]),
+                    new Token([T_USE, 'use']),
+                    new Token([T_WHITESPACE, ' ']),
+                ]);
+
+                if ('const' === $type) {
+                    $items[] = new Token([CT::T_CONST_IMPORT, 'const']);
+                    $items[] = new Token([T_WHITESPACE, ' ']);
+                } elseif ('function' === $type) {
+                    $items[] = new Token([CT::T_FUNCTION_IMPORT, 'function']);
+                    $items[] = new Token([T_WHITESPACE, ' ']);
+                }
+
+                $items = array_merge($items, self::tokenizeName($name));
+                $items[] = new Token(';');
+            }
+
+            $tokens->insertAt($atIndex, $items);
+        }
+    }
+
+    /**
+     * @param class-string $name
+     *
+     * @return list<Token>
+     */
+    public static function tokenizeName(string $name): array
+    {
+        $parts = explode('\\', $name);
+        $newTokens = [];
+
+        if ('' === $parts[0]) {
+            $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
+            array_shift($parts);
+        }
+
+        foreach ($parts as $part) {
+            $newTokens[] = new Token([T_STRING, $part]);
+            $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
+        }
+
+        array_pop($newTokens);
+
+        return $newTokens;
+    }
+}
diff --git a/src/Tokenizer/Transformer/NameQualifiedTransformer.php b/src/Tokenizer/Transformer/NameQualifiedTransformer.php
index b0521954e..ed8353c8d 100644
--- a/src/Tokenizer/Transformer/NameQualifiedTransformer.php
+++ b/src/Tokenizer/Transformer/NameQualifiedTransformer.php
@@ -15,6 +15,7 @@
 namespace PhpCsFixer\Tokenizer\Transformer;
 
 use PhpCsFixer\Tokenizer\AbstractTransformer;
+use PhpCsFixer\Tokenizer\Processor\ImportProcessor;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 
@@ -51,38 +52,15 @@ public function getCustomTokens(): array
 
     private function transformQualified(Tokens $tokens, Token $token, int $index): void
     {
-        $parts = explode('\\', $token->getContent());
-        $newTokens = [];
-
-        if ('' === $parts[0]) {
-            $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
-            array_shift($parts);
-        }
-
-        foreach ($parts as $part) {
-            $newTokens[] = new Token([T_STRING, $part]);
-            $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
-        }
-
-        array_pop($newTokens);
+        $newTokens = ImportProcessor::tokenizeName($token->getContent());
 
         $tokens->overrideRange($index, $index, $newTokens);
     }
 
     private function transformRelative(Tokens $tokens, Token $token, int $index): void
     {
-        $parts = explode('\\', $token->getContent());
-        $newTokens = [
-            new Token([T_NAMESPACE, array_shift($parts)]),
-            new Token([T_NS_SEPARATOR, '\\']),
-        ];
-
-        foreach ($parts as $part) {
-            $newTokens[] = new Token([T_STRING, $part]);
-            $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
-        }
-
-        array_pop($newTokens);
+        $newTokens = ImportProcessor::tokenizeName($token->getContent());
+        $newTokens[0] = new Token([T_NAMESPACE, 'namespace']);
 
         $tokens->overrideRange($index, $index, $newTokens);
     }


Test patch:
diff --git a/tests/AutoReview/FixerFactoryTest.php b/tests/AutoReview/FixerFactoryTest.php
index d56333fcd..adf952ffe 100644
--- a/tests/AutoReview/FixerFactoryTest.php
+++ b/tests/AutoReview/FixerFactoryTest.php
@@ -448,6 +448,8 @@ private static function getFixersPriorityGraph(): array
             ],
             'fully_qualified_strict_types' => [
                 'no_superfluous_phpdoc_tags',
+                'ordered_imports',
+                'statement_indentation',
             ],
             'function_declaration' => [
                 'method_argument_space',
@@ -476,6 +478,7 @@ private static function getFixersPriorityGraph(): array
             'global_namespace_import' => [
                 'no_unused_imports',
                 'ordered_imports',
+                'statement_indentation',
             ],
             'header_comment' => [
                 'blank_lines_before_namespace',
diff --git a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
index efaccd94d..ecdd54e2f 100644
--- a/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
+++ b/tests/Fixer/Import/FullyQualifiedStrictTypesFixerTest.php
@@ -15,6 +15,7 @@
 namespace PhpCsFixer\Tests\Fixer\Import;
 
 use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+use PhpCsFixer\WhitespacesFixerConfig;
 
 /**
  * @author VeeWee <toonverwerft@gmail.com>
@@ -30,9 +31,22 @@ final class FullyQualifiedStrictTypesFixerTest extends AbstractFixerTestCase
      *
      * @dataProvider provideFixCases
      */
-    public function testFix(string $expected, ?string $input = null, array $config = []): void
-    {
+    public function testFix(
+        string $expected,
+        ?string $input = null,
+        array $config = [],
+        ?WhitespacesFixerConfig $whitespaceConfig = null
+    ): void {
         $this->fixer->configure($config);
+
+        if (null !== $whitespaceConfig) {
+            $this->fixer->setWhitespacesConfig($whitespaceConfig);
+            $expected = str_replace("\n", $whitespaceConfig->getLineEnding(), $expected);
+            if (null !== $input) {
+                $input = str_replace("\n", $whitespaceConfig->getLineEnding(), $input);
+            }
+        }
+
         $this->doTest($expected, $input);
     }
 
@@ -355,6 +369,151 @@ function A(\Z\B\C\DE\Foo $fix) {}
             ',
             null,
         ];
+
+        yield 'import new symbols from all supported places' => [
+            '<?php
+
+namespace Foo\Test;
+use Other\BaseClass;
+use Other\CaughtThrowable;
+use Other\FunctionArgument;
+use Other\FunctionReturnType;
+use Other\Interface1;
+use Other\Interface2;
+use Other\PropertyPhpDoc;
+use Other\StaticFunctionCall;
+
+class Foo extends BaseClass implements Interface1, Interface2
+{
+    /** @var PropertyPhpDoc */
+    private $array;
+    public function __construct(FunctionArgument $arg) {}
+    public function foo(): FunctionReturnType
+    {
+        try {
+            StaticFunctionCall::bar();
+        } catch (CaughtThrowable $e) {}
+    }
+}
+            ',
+            '<?php
+
+namespace Foo\Test;
+
+class Foo extends \Other\BaseClass implements \Other\Interface1, \Other\Interface2
+{
+    /** @var \Other\PropertyPhpDoc */
+    private $array;
+    public function __construct(\Other\FunctionArgument $arg) {}
+    public function foo(): \Other\FunctionReturnType
+    {
+        try {
+            \Other\StaticFunctionCall::bar();
+        } catch (\Other\CaughtThrowable $e) {}
+    }
+}
+            ',
+            ['import_symbols' => true],
+        ];
+
+        yield 'import new symbols under already existing imports' => [
+            '<?php
+
+namespace Foo\Test;
+
+use Other\A;
+use Other\B;
+use Other\C;
+use Other\D;
+use Other\E;
+
+function foo(A $a, B $b) {}
+function bar(C $c, D $d): E {}
+',
+            '<?php
+
+namespace Foo\Test;
+
+use Other\A;
+use Other\B;
+
+function foo(A $a, B $b) {}
+function bar(\Other\C $c, \Other\D $d): \Other\E {}
+',
+            ['import_symbols' => true],
+        ];
+
+        yield 'import new symbols within multiple namespaces' => [
+            '<?php
+
+namespace Foo\Bar {
+    use Other\A;
+use Other\B;
+
+    function foo(A $a, B $b) {}
+}
+namespace Foo\Baz {
+    use Other\A;
+use Other\C;
+
+    function foo(A $a, C $c) {}
+}
+',
+            '<?php
+
+namespace Foo\Bar {
+    use Other\A;
+
+    function foo(A $a, \Other\B $b) {}
+}
+namespace Foo\Baz {
+    use Other\A;
+
+    function foo(A $a, \Other\C $c) {}
+}
+',
+            ['import_symbols' => true],
+        ];
+
+        yield 'import new symbols with custom whitespace config' => [
+            '<?php
+
+namespace Foo\Bar;
+
+use Other\A;
+use Other\B;
+
+function foo(A $a, B $b) {}
+',
+            '<?php
+
+namespace Foo\Bar;
+
+use Other\A;
+
+function foo(A $a, \Other\B $b) {}
+',
+            ['import_symbols' => true],
+            new WhitespacesFixerConfig("\t", "\r\n"),
+        ];
+
+        yield 'ignore importing if there is name conflict' => [
+            '<?php namespace Foo\Test; use Other\A; function foo(A $a, \YetAnother\A $b) {}',
+            null,
+            ['import_symbols' => true],
+        ];
+
+        yield 'ignore importing if symbol is not a FQN' => [
+            '<?php namespace Foo\Test; use Foo\Test\Sub\Symbol1; function foo(Symbol1 $a, Sub\Symbol2 $b) {}',
+            null,
+            ['import_symbols' => true],
+        ];
+
+        yield 'ignore global FQNs (there is GlobalNamespaceImportFixer for that)' => [
+            '<?php namespace Foo\Test; function foo(\Symbol $a, \OtherSymbol $b) {}',
+            null,
+            ['import_symbols' => true],
+        ];
     }
 
     /**
diff --git a/tests/Fixer/Import/GlobalNamespaceImportFixerTest.php b/tests/Fixer/Import/GlobalNamespaceImportFixerTest.php
index 8c8425946..eda8ed489 100644
--- a/tests/Fixer/Import/GlobalNamespaceImportFixerTest.php
+++ b/tests/Fixer/Import/GlobalNamespaceImportFixerTest.php
@@ -81,8 +81,8 @@ public static function provideFixImportConstantsCases(): iterable
             <<<'EXPECTED'
                 <?php
                 namespace Test;
-                use const FOO;
                 use const BAR;
+                use const FOO;
                 echo FOO, BAR;
                 EXPECTED
             ,
@@ -97,8 +97,8 @@ public static function provideFixImportConstantsCases(): iterable
             <<<'EXPECTED'
                 <?php
                 namespace Test {
-                use const FOO;
                 use const BAR;
+                use const FOO;
                     echo FOO, BAR;
                 }
                 EXPECTED
@@ -116,8 +116,8 @@ public static function provideFixImportConstantsCases(): iterable
                 <?php
                 namespace Test;
                 use BAR;
-                use const FOO;
                 use const BAR;
+                use const FOO;
                 echo FOO, BAR;
                 EXPECTED
             ,
@@ -300,8 +300,8 @@ function foo() {}
             <<<'EXPECTED'
                 <?php
                 namespace Test;
-                use function foo;
                 use function bar;
+                use function foo;
                 foo();
                 bar();
                 EXPECTED
@@ -318,8 +318,8 @@ function foo() {}
             <<<'EXPECTED'
                 <?php
                 namespace Test {
-                use function foo;
                 use function bar;
+                use function foo;
                     foo();
                     bar();
                 }
@@ -339,8 +339,8 @@ function foo() {}
                 <?php
                 namespace Test;
                 use bar;
-                use function foo;
                 use function bar;
+                use function foo;
                 foo();
                 bar();
                 EXPECTED
@@ -539,7 +539,7 @@ function x() {}
                 namespace Test;
                 use Bar;
                 use Baz;
-                use foo;
+                use Foo;
 
                 new Foo();
                 Bar::baz();
@@ -564,8 +564,8 @@ function x() {}
             <<<'EXPECTED'
                 <?php
                 namespace Test {
-                use Foo;
                 use Bar;
+                use Foo;
                     new Foo();
                     Bar::baz();
                 }
@@ -604,8 +604,8 @@ function x() {}
                 <?php
                 namespace Test;
                 use function Bar;
-                use Foo;
                 use Bar;
+                use Foo;
                 new Foo();
                 Bar::baz();
                 EXPECTED
@@ -715,8 +715,8 @@ function y() {}
                 <?php
                 namespace Test;
                 use Bar;
-                use Foo;
                 use Baz;
+                use Foo;
                 class Abc {
                     function bar(Foo $a, Bar $b, foo &$c, Baz ...$d) {}
                 }
@@ -735,8 +735,8 @@ function bar(\Foo $a, \Bar $b, \foo &$c, \Baz ...$d) {}
             <<<'EXPECTED'
                 <?php
                 namespace Test;
-                use Foo;
                 use Bar;
+                use Foo;
                 class Abc {
                     function bar(?Foo $a): ?Bar {}
                 }
diff --git a/tests/Fixtures/Integration/priority/fully_qualified_strict_types,ordered_imports.test b/tests/Fixtures/Integration/priority/fully_qualified_strict_types,ordered_imports.test
new file mode 100644
index 000000000..edef98a33
--- /dev/null
+++ b/tests/Fixtures/Integration/priority/fully_qualified_strict_types,ordered_imports.test
@@ -0,0 +1,26 @@
+--TEST--
+Integration of fixers: fully_qualified_strict_types,ordered_imports.
+--RULESET--
+{"fully_qualified_strict_types": {"import_symbols": true}, "ordered_imports": true}
+--EXPECT--
+<?php
+
+namespace Test;
+
+use Other\A;
+use Other\B;
+use Other\C;
+use Other\D;
+use Other\E;
+
+function foo(A $a, C $c): E {}
+
+--INPUT--
+<?php
+
+namespace Test;
+
+use Other\B;
+use Other\D;
+
+function foo(\Other\A $a, \Other\C $c): \Other\E {}
diff --git a/tests/Fixtures/Integration/priority/fully_qualified_strict_types,statement_indentation.test b/tests/Fixtures/Integration/priority/fully_qualified_strict_types,statement_indentation.test
new file mode 100644
index 000000000..905a7b730
--- /dev/null
+++ b/tests/Fixtures/Integration/priority/fully_qualified_strict_types,statement_indentation.test
@@ -0,0 +1,26 @@
+--TEST--
+Integration of fixers: fully_qualified_strict_types,statement_indentation.
+--RULESET--
+{"fully_qualified_strict_types": {"import_symbols": true}, "statement_indentation": true}
+--EXPECT--
+<?php
+
+namespace Test {
+    use Other\B;
+    use Other\D;
+    use Other\A;
+    use Other\C;
+    use Other\E;
+
+    function foo(A $a, C $c): E {}
+}
+
+--INPUT--
+<?php
+
+namespace Test {
+    use Other\B;
+    use Other\D;
+
+    function foo(\Other\A $a, \Other\C $c): \Other\E {}
+}
diff --git a/tests/Fixtures/Integration/priority/global_namespace_import,statement_indentation.test b/tests/Fixtures/Integration/priority/global_namespace_import,statement_indentation.test
new file mode 100644
index 000000000..511afb00b
--- /dev/null
+++ b/tests/Fixtures/Integration/priority/global_namespace_import,statement_indentation.test
@@ -0,0 +1,22 @@
+--TEST--
+Integration of fixers: global_namespace_import,statement_indentation.
+--RULESET--
+{"global_namespace_import": {"import_classes": true}, "statement_indentation": true}
+--EXPECT--
+<?php
+
+namespace Test {
+    use Z;
+    use Foo;
+
+    new Foo();
+}
+
+--INPUT--
+<?php
+
+namespace Test {
+    use Z;
+
+    new \Foo();
+}
diff --git a/tests/Tokenizer/Processor/ImportProcessorTest.php b/tests/Tokenizer/Processor/ImportProcessorTest.php
new file mode 100644
index 000000000..36a399028
--- /dev/null
+++ b/tests/Tokenizer/Processor/ImportProcessorTest.php
@@ -0,0 +1,128 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Tokenizer\Processor;
+
+use PhpCsFixer\Tests\TestCase;
+use PhpCsFixer\Tokenizer\Processor\ImportProcessor;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\WhitespacesFixerConfig;
+
+/**
+ * @internal
+ *
+ * @covers \PhpCsFixer\Tokenizer\Processor\ImportProcessor
+ */
+final class ImportProcessorTest extends TestCase
+{
+    /**
+     * @param class-string $symbol
+     *
+     * @dataProvider provideTokenizeNameCases
+     */
+    public function testTokenizeName(string $symbol): void
+    {
+        self::assertSame(
+            $symbol,
+            implode(
+                '',
+                array_map(
+                    static fn (Token $token): string => $token->getContent(),
+                    ImportProcessor::tokenizeName($symbol)
+                )
+            )
+        );
+    }
+
+    /**
+     * @return iterable<array{0: string}>
+     */
+    public static function provideTokenizeNameCases(): iterable
+    {
+        yield [__CLASS__];
+
+        yield ['Foo\\Bar'];
+
+        yield ['\\Foo\\Bar'];
+
+        yield ['FooBar'];
+
+        yield ['\\FooBar'];
+
+        yield ['\\Foo\\Bar\\Baz\\Buzz'];
+
+        yield ['\\Foo1\\Bar_\\baz\\buzz'];
+    }
+
+    /**
+     * @param array{
+     *      const?: array<int|string, class-string>,
+     *      class?: array<int|string, class-string>,
+     *      function?: array<int|string, class-string>
+     *  } $imports
+     *
+     * @dataProvider provideInsertImportsCases
+     */
+    public function testInsertImports(string $expected, string $input, array $imports, int $atIndex): void
+    {
+        $processor = new ImportProcessor(new WhitespacesFixerConfig());
+        $tokens = Tokens::fromCode($input);
+        $processor->insertImports($tokens, $imports, $atIndex);
+
+        self::assertSame($expected, $tokens->generateCode());
+    }
+
+    /**
+     * @return iterable<string, array{0: string, 1: string, 2: array{class?: list<string>, const?: list<string>, function?: list<string>}, 3: int}>
+     */
+    public static function provideInsertImportsCases(): iterable
+    {
+        yield 'class import in single namespace' => [
+            '<?php
+
+namespace Foo;
+use Other\A;
+use Other\B;
+',
+            '<?php
+
+namespace Foo;
+',
+            [
+                'class' => ['Other\\A', 'Other\\B'],
+            ],
+            6,
+        ];
+
+        yield 'class import in single {} namespace' => [
+            '<?php
+
+namespace Foo {
+use Other\A;
+use Other\B;
+}
+',
+            '<?php
+
+namespace Foo {
+}
+',
+            [
+                'class' => ['Other\\A', 'Other\\B'],
+            ],
+            7,
+        ];
+    }
+}