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

Issue:
PHP 8 attributes not detected properly for no_unused_imports
## Bug report

The `no_unused_imports` fixer seems to have a problem in v3.1.0, this was possible caused by this PR https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/5720. 

In v3.0.2 all classes in PHP 8 attributes would be detected, however with v3.1.0 & v3.0.3 when there are multiple attributes in one block CS Fixer will remove imports for some of the attribute classes. For example with the below code the `IsGranted` and `JsonSchema` use statements get removed using CS Fixer v3.1.0 & v3.0.3. This doesn't happen in CS Fixer v3.0.2.

   * PHP version: 8.0.8
   * PHP CS Fixer version: 3.1.0 & 3.0.3

### Config snippet
```php
$config = new PhpCsFixer\Config();
return $config
    ->setRules(array(
        '@PSR2' => true,
        '@Symfony' => true,
        'single_blank_line_before_namespace' => false,
        'phpdoc_var_without_name' => false,
        'phpdoc_summary' => false,
        'phpdoc_no_empty_return' => false,
        'blank_line_before_statement' => ['statements' => ['return']],
        'concat_space' => ['spacing' => 'one'],
        'function_typehint_space' => true,
        'no_blank_lines_after_class_opening' => true,
        'no_blank_lines_after_phpdoc' => true,
        'no_blank_lines_before_namespace' => true,
        'no_empty_comment' => true,
        'no_empty_phpdoc' => true,
        'no_empty_statement' => true,
        'no_extra_blank_lines' => true,
        'no_leading_import_slash' => true,
        'no_leading_namespace_whitespace' => true,
        'echo_tag_syntax' => ['format' => 'long'],
        'no_spaces_around_offset' => true, 
        'no_unused_imports' => true, 
        'no_whitespace_before_comma_in_array' => true,
        'no_whitespace_in_blank_line' => true,
        'object_operator_without_whitespace' => true,
        'phpdoc_scalar' => true,
        'phpdoc_separation' => true,
        'phpdoc_types' => true,
        'single_quote' => true,
        'standardize_not_equals' => true,
        'ternary_operator_spaces' => true,
        'trim_array_spaces' => true,
        'whitespace_after_comma_in_array' => true,
        'yoda_style' => false,
    ))
```

### Code snippet that reproduces the problem

  ```php
<?php
use Acme\JsonSchemaValidationBundle\Annotation\JsonSchema;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Routing\Annotation\Route;

...
#[
    Route('/basket/{uuid}/item', name: 'addBasketItem', requirements: ['uuid' => '%regex.uuid%'], methods: ['POST']),
    IsGranted('ROLE_USER'),
    JsonSchema('Public/Basket/addItem.json'),
]
  ```

Expected result: No code changes

Result with v3.1.0 & v3.0.3:
  ```php
<?php
use Symfony\Component\Routing\Annotation\Route;

...
#[
    Route('/basket/{uuid}/item', name: 'addBasketItem', requirements: ['uuid' => '%regex.uuid%'], methods: ['POST']),
    IsGranted('ROLE_USER'),
    JsonSchema('Public/Basket/addItem.json'),
]
  ```


Pull request:
NoUnusedImportsFixer - use in attribute
closes #5940

build on https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/5942

not 100% about this one, cc @kubawerlos about attribute usages 

Patch:
diff --git a/src/Fixer/Import/NoUnusedImportsFixer.php b/src/Fixer/Import/NoUnusedImportsFixer.php
index 0d4bbaaed..359dbb849 100644
--- a/src/Fixer/Import/NoUnusedImportsFixer.php
+++ b/src/Fixer/Import/NoUnusedImportsFixer.php
@@ -24,6 +24,7 @@
 use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
 use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
 use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
+use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 use PhpCsFixer\Tokenizer\TokensAnalyzer;
@@ -105,26 +106,45 @@ private function isImportUsed(Tokens $tokens, NamespaceAnalysis $namespace, Name
 
         $tokensNotBeforeFunctionCall = [T_NEW];
 
-        if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition when PHP 8.0+ is required
+        $attributeIsDefined = \defined('T_ATTRIBUTE');
+
+        if ($attributeIsDefined) { // @TODO: drop condition when PHP 8.0+ is required
             $tokensNotBeforeFunctionCall[] = T_ATTRIBUTE;
         }
 
         $namespaceEndIndex = $namespace->getScopeEndIndex();
+        $inAttribute = false;
 
         for ($index = $namespace->getScopeStartIndex(); $index <= $namespaceEndIndex; ++$index) {
+            $token = $tokens[$index];
+
+            if ($attributeIsDefined && $token->isGivenKind(T_ATTRIBUTE)) {
+                $inAttribute = true;
+
+                continue;
+            }
+
+            if ($attributeIsDefined && $token->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
+                $inAttribute = false;
+
+                continue;
+            }
+
             if (isset($ignoredIndexes[$index])) {
                 $index = $ignoredIndexes[$index];
 
                 continue;
             }
 
-            $token = $tokens[$index];
-
             if ($token->isGivenKind(T_STRING)) {
                 if (0 !== strcasecmp($import->getShortName(), $token->getContent())) {
                     continue;
                 }
 
+                if ($inAttribute) {
+                    return true;
+                }
+
                 $prevMeaningfulToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
 
                 if ($prevMeaningfulToken->isGivenKind(T_NAMESPACE)) {


Test patch:
diff --git a/tests/Fixer/Import/NoUnusedImportsFixerTest.php b/tests/Fixer/Import/NoUnusedImportsFixerTest.php
index a01c2efe8..a998d36e2 100644
--- a/tests/Fixer/Import/NoUnusedImportsFixerTest.php
+++ b/tests/Fixer/Import/NoUnusedImportsFixerTest.php
@@ -1395,7 +1395,7 @@ public function providePhp80Cases()
 try {} catch (Foo | Bar) {}',
         ];
 
-        yield [
+        yield 'union return' => [
             '<?php
 
 use Foo;
@@ -1407,5 +1407,20 @@ abstract public function test(): Foo|Bar;
 }
 ',
         ];
+
+        yield 'attribute' => [
+            "<?php
+use Acme\\JsonSchemaValidationBundle\\Annotation\\JsonSchema;
+use Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\IsGranted;
+use Symfony\\Component\\Routing\\Annotation\\Route;
+
+#[
+  Route('/basket/{uuid}/item', name: 'addBasketItem', requirements: ['uuid' => '%regex.uuid%'], methods: ['POST']),
+  IsGranted('ROLE_USER'),
+  JsonSchema('Public/Basket/addItem.json'),
+]
+class Foo {}
+",
+        ];
     }
 }