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

Issue:
Import is considered as unused when used alongside with attribute and union type on method parameters
## Bug report

   * PHP CS Fixer version: `3.10.0`
   * PHP runtime: `8.0.22`
   
 Import is considered as unused but it is used as method parameter with an union type.
 
 Configuration file:
 
 ```php
 $finder = PhpCsFixer\Finder::create()
     ->in(__DIR__)
;
 
 return ($config = new PhpCsFixer\Config())
    ->setRules([
        'no_unused_imports' => true,
    ])
    ->setFinder($finder)
;
 ```

### Code snippet that reproduces the problem

```php
<?php

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

class Test
{
    public function __construct(
        #[Target('xxx')] LoggerInterface|null $logger = null,
    ) {
    }
}
```

### Output 

```
   1) Test.php (no_unused_imports)
      ---------- begin diff ----------
--- /var/www/Test.php
+++ /var/www/Test.php
@@ -1,9 +1,8 @@
 <?php
 
-use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\Attribute\Target;
 
class Test
 {
     public function __construct(
         #[Target('xxx')] LoggerInterface|null $logger = null,

      ----------- end diff -----------
```



Discussion:
Without the union type it works as expected:

```php
<?php

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

class Test
{
    public function __construct(
        #[Target('xxx')] ?LoggerInterface $logger = null,
    ) {
    }
}
```
Thanks for reporting!
I proposed a fix here https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/6579
Wow, so quick.
Thanks @SpacePossum 



Patch:
diff --git a/src/Tokenizer/AbstractTypeTransformer.php b/src/Tokenizer/AbstractTypeTransformer.php
index 9f3634f6f8e..400ed941164 100644
--- a/src/Tokenizer/AbstractTypeTransformer.php
+++ b/src/Tokenizer/AbstractTypeTransformer.php
@@ -30,7 +30,7 @@ protected function doProcess(Tokens $tokens, int $index, $originalToken): void
             return;
         }
 
-        $prevIndex = $tokens->getTokenNotOfKindsSibling($index, -1, [T_CALLABLE, T_NS_SEPARATOR, T_STRING, CT::T_ARRAY_TYPEHINT, T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]);
+        $prevIndex = $this->getPreviousTokenCandidate($tokens, $index);
 
         /** @var Token $prevToken */
         $prevToken = $tokens[$prevIndex];
@@ -83,4 +83,14 @@ protected function doProcess(Tokens $tokens, int $index, $originalToken): void
     }
 
     abstract protected function replaceToken(Tokens $tokens, int $index): void;
+
+    private function getPreviousTokenCandidate(Tokens $tokens, int $index): int
+    {
+        $candidateIndex = $tokens->getTokenNotOfKindsSibling($index, -1, [T_CALLABLE, T_NS_SEPARATOR, T_STRING, CT::T_ARRAY_TYPEHINT, T_WHITESPACE, T_COMMENT, T_DOC_COMMENT]);
+
+        return $tokens[$candidateIndex]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)
+            ? $this->getPreviousTokenCandidate($tokens, $tokens->getPrevTokenOfKind($index, [[T_ATTRIBUTE]]))
+            : $candidateIndex
+        ;
+    }
 }
diff --git a/src/Tokenizer/Transformer/TypeAlternationTransformer.php b/src/Tokenizer/Transformer/TypeAlternationTransformer.php
index 380ceb4c822..cbbb7a72298 100644
--- a/src/Tokenizer/Transformer/TypeAlternationTransformer.php
+++ b/src/Tokenizer/Transformer/TypeAlternationTransformer.php
@@ -34,7 +34,7 @@ final class TypeAlternationTransformer extends AbstractTypeTransformer
      */
     public function getPriority(): int
     {
-        // needs to run after ArrayTypehintTransformer and TypeColonTransformer
+        // needs to run after ArrayTypehintTransformer, TypeColonTransformer and AttributeTransformer
         return -15;
     }
 
diff --git a/src/Tokenizer/Transformer/TypeIntersectionTransformer.php b/src/Tokenizer/Transformer/TypeIntersectionTransformer.php
index ea967f91174..be81fe65367 100644
--- a/src/Tokenizer/Transformer/TypeIntersectionTransformer.php
+++ b/src/Tokenizer/Transformer/TypeIntersectionTransformer.php
@@ -32,7 +32,7 @@ final class TypeIntersectionTransformer extends AbstractTypeTransformer
      */
     public function getPriority(): int
     {
-        // needs to run after ArrayTypehintTransformer and TypeColonTransformer
+        // needs to run after ArrayTypehintTransformer, TypeColonTransformer and AttributeTransformer
         return -15;
     }
 


Test patch:
diff --git a/tests/Fixer/Import/NoUnusedImportsFixerTest.php b/tests/Fixer/Import/NoUnusedImportsFixerTest.php
index b4e2c4e2cbc..f37afac37a5 100644
--- a/tests/Fixer/Import/NoUnusedImportsFixerTest.php
+++ b/tests/Fixer/Import/NoUnusedImportsFixerTest.php
@@ -1407,6 +1407,14 @@ abstract public function test(): Foo|Bar;
 class Foo {}
 ",
         ];
+
+        yield 'attribute 2' => [
+            '<?php
+
+use Psr\Log\LoggerInterface;
+function f( #[Target(\'xxx\')] LoggerInterface|null $logger) {}
+',
+        ];
     }
 
     /**
diff --git a/tests/Tokenizer/TokensAnalyzerTest.php b/tests/Tokenizer/TokensAnalyzerTest.php
index 02ced9719f0..a20bbcc1672 100644
--- a/tests/Tokenizer/TokensAnalyzerTest.php
+++ b/tests/Tokenizer/TokensAnalyzerTest.php
@@ -1216,6 +1216,15 @@ abstract public function test(): Foo|Bar;
             [5 => false, 14 => false, 16 => false, 19 => false, 22 => false],
             '<?php try { foo(); } catch(A\B|\InvalidArgumentException|\LogicException) {}',
         ];
+
+        yield 'attribute before' => [
+            [4 => false, 6 => false, 8 => false, 13 => false, 17 => false, 23 => false, 25 => false],
+            '<?php
+
+use Psr\Log\LoggerInterface;
+function f( #[Target(\'xxx\')] LoggerInterface|null $logger) {}
+',
+        ];
     }
 
     /**
diff --git a/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php b/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php
index 02535669846..b962e976417 100644
--- a/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php
+++ b/tests/Tokenizer/Transformer/TypeAlternationTransformerTest.php
@@ -312,6 +312,30 @@ public function __construct(
                 35 => CT::T_TYPE_ALTERNATION,
             ],
         ];
+
+        yield [
+            '<?php
+
+use Psr\Log\LoggerInterface;
+function f( #[Target(\'xxx\')] LoggerInterface|A $logger) {}
+
+',
+            [
+                24 => CT::T_TYPE_ALTERNATION,
+            ],
+        ];
+
+        yield [
+            '<?php
+
+use Psr\Log\LoggerInterface;
+function f( #[Target(\'a\')] #[Target(\'b\')] #[Target(\'c\')] #[Target(\'d\')] LoggerInterface|X $logger) {}
+
+',
+            [
+                45 => CT::T_TYPE_ALTERNATION,
+            ],
+        ];
     }
 
     /**
diff --git a/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php b/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php
index 7aa16005a56..23f35f73162 100644
--- a/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php
+++ b/tests/Tokenizer/Transformer/TypeIntersectionTransformerTest.php
@@ -295,5 +295,29 @@ function f4(callable&string $x) {};
                 52 => CT::T_TYPE_INTERSECTION,
             ],
         ];
+
+        yield [
+            '<?php
+
+use Psr\Log\LoggerInterface;
+function f( #[Target(\'xxx\')] LoggerInterface&A $logger) {}
+
+',
+            [
+                24 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
+
+        yield [
+            '<?php
+
+use Psr\Log\LoggerInterface;
+function f( #[Target(\'a\')] #[Target(\'b\')] #[Target(\'c\')] #[Target(\'d\')] LoggerInterface&X $logger) {}
+
+',
+            [
+                45 => CT::T_TYPE_INTERSECTION,
+            ],
+        ];
     }
 }