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

Issue:
Add `general_attribute_remove` similar to `general_phpdoc_annotation_remove`
## Feature request

Like the `general_phpdoc_annotation_remove` a `general_attribute_remove` rule could be implemented to remove some attributes based on their name.


Pull request description:
feat: Introduce `general_attribute_remove` fixer
fixes #7794

Patch:
diff --git a/dev-tools/phpstan/baseline.php b/dev-tools/phpstan/baseline.php
index 7626828cd..c83318b0b 100644
--- a/dev-tools/phpstan/baseline.php
+++ b/dev-tools/phpstan/baseline.php
@@ -775,12 +775,6 @@
 	'count' => 1,
 	'path' => __DIR__ . '/../../src/Fixer/ArrayNotation/YieldFromArrayToYieldsFixer.php',
 ];
-$ignoreErrors[] = [
-	'message' => '#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#',
-	'identifier' => 'argument.type',
-	'count' => 1,
-	'path' => __DIR__ . '/../../src/Fixer/AttributeNotation/OrderedAttributesFixer.php',
-];
 $ignoreErrors[] = [
 	'message' => '#^Offset 0 might not exist on list\\<string\\>\\.$#',
 	'identifier' => 'offsetAccess.notFound',
diff --git a/doc/rules/attribute_notation/general_attribute_remove.rst b/doc/rules/attribute_notation/general_attribute_remove.rst
new file mode 100644
index 000000000..2cc2d2ca2
--- /dev/null
+++ b/doc/rules/attribute_notation/general_attribute_remove.rst
@@ -0,0 +1,56 @@
+=================================
+Rule ``general_attribute_remove``
+=================================
+
+Removes configured attributes by their respective FQN.
+
+Configuration
+-------------
+
+``attributes``
+~~~~~~~~~~~~~~
+
+List of FQNs of attributes for removal.
+
+Allowed types: ``list<string>``
+
+Default value: ``[]``
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+With configuration: ``['attributes' => ['\\A\\B\\Foo']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+   -#[\A\B\Foo]
+    function foo() {}
+
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['attributes' => ['\\A\\B\\Foo', 'A\\B\\Bar']]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    use A\B\Bar as BarAlias;
+
+   -#[\A\B\Foo]
+   -#[BarAlias]
+    function foo() {}
+References
+----------
+
+- Fixer class: `PhpCsFixer\\Fixer\\AttributeNotation\\GeneralAttributeRemoveFixer <./../../../src/Fixer/AttributeNotation/GeneralAttributeRemoveFixer.php>`_
+- Test class: `PhpCsFixer\\Tests\\Fixer\\AttributeNotation\\GeneralAttributeRemoveFixerTest <./../../../tests/Fixer/AttributeNotation/GeneralAttributeRemoveFixerTest.php>`_
+
+The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
diff --git a/doc/rules/index.rst b/doc/rules/index.rst
index 47b1b2587..285f1529b 100644
--- a/doc/rules/index.rst
+++ b/doc/rules/index.rst
@@ -76,6 +76,9 @@ Attribute Notation
 - `attribute_empty_parentheses <./attribute_notation/attribute_empty_parentheses.rst>`_
 
   PHP attributes declared without arguments must (not) be followed by empty parentheses.
+- `general_attribute_remove <./attribute_notation/general_attribute_remove.rst>`_
+
+  Removes configured attributes by their respective FQN.
 - `ordered_attributes <./attribute_notation/ordered_attributes.rst>`_
 
   Sorts attributes using the configured sort algorithm.
@@ -715,7 +718,7 @@ PHPDoc
   Each line of multi-line DocComments must have an asterisk [PSR-5] and must be aligned with the first one.
 - `general_phpdoc_annotation_remove <./phpdoc/general_phpdoc_annotation_remove.rst>`_
 
-  Configured annotations should be omitted from PHPDoc.
+  Removes configured annotations from PHPDoc.
 - `general_phpdoc_tag_rename <./phpdoc/general_phpdoc_tag_rename.rst>`_
 
   Renames PHPDoc tags.
@@ -754,16 +757,16 @@ PHPDoc
   PHPDoc ``list`` type must be used instead of ``array`` without a key.
 - `phpdoc_no_access <./phpdoc/phpdoc_no_access.rst>`_
 
-  ``@access`` annotations should be omitted from PHPDoc.
+  ``@access`` annotations must be removed from PHPDoc.
 - `phpdoc_no_alias_tag <./phpdoc/phpdoc_no_alias_tag.rst>`_
 
   No alias PHPDoc tags should be used.
 - `phpdoc_no_empty_return <./phpdoc/phpdoc_no_empty_return.rst>`_
 
-  ``@return void`` and ``@return null`` annotations should be omitted from PHPDoc.
+  ``@return void`` and ``@return null`` annotations must be removed from PHPDoc.
 - `phpdoc_no_package <./phpdoc/phpdoc_no_package.rst>`_
 
-  ``@package`` and ``@subpackage`` annotations should be omitted from PHPDoc.
+  ``@package`` and ``@subpackage`` annotations must be removed from PHPDoc.
 - `phpdoc_no_useless_inheritdoc <./phpdoc/phpdoc_no_useless_inheritdoc.rst>`_
 
   Classy that does not inherit must not have ``@inheritdoc`` tags.
diff --git a/doc/rules/phpdoc/general_phpdoc_annotation_remove.rst b/doc/rules/phpdoc/general_phpdoc_annotation_remove.rst
index e927b5e1e..ce5374bba 100644
--- a/doc/rules/phpdoc/general_phpdoc_annotation_remove.rst
+++ b/doc/rules/phpdoc/general_phpdoc_annotation_remove.rst
@@ -2,7 +2,7 @@
 Rule ``general_phpdoc_annotation_remove``
 =========================================
 
-Configured annotations should be omitted from PHPDoc.
+Removes configured annotations from PHPDoc.
 
 Configuration
 -------------
diff --git a/doc/rules/phpdoc/phpdoc_no_access.rst b/doc/rules/phpdoc/phpdoc_no_access.rst
index 4720b58e3..4a44593da 100644
--- a/doc/rules/phpdoc/phpdoc_no_access.rst
+++ b/doc/rules/phpdoc/phpdoc_no_access.rst
@@ -2,7 +2,7 @@
 Rule ``phpdoc_no_access``
 =========================
 
-``@access`` annotations should be omitted from PHPDoc.
+``@access`` annotations must be removed from PHPDoc.
 
 Examples
 --------
diff --git a/doc/rules/phpdoc/phpdoc_no_empty_return.rst b/doc/rules/phpdoc/phpdoc_no_empty_return.rst
index e9d471eac..4294a3062 100644
--- a/doc/rules/phpdoc/phpdoc_no_empty_return.rst
+++ b/doc/rules/phpdoc/phpdoc_no_empty_return.rst
@@ -2,7 +2,7 @@
 Rule ``phpdoc_no_empty_return``
 ===============================
 
-``@return void`` and ``@return null`` annotations should be omitted from PHPDoc.
+``@return void`` and ``@return null`` annotations must be removed from PHPDoc.
 
 Examples
 --------
diff --git a/doc/rules/phpdoc/phpdoc_no_package.rst b/doc/rules/phpdoc/phpdoc_no_package.rst
index 32e77520e..d80a853b2 100644
--- a/doc/rules/phpdoc/phpdoc_no_package.rst
+++ b/doc/rules/phpdoc/phpdoc_no_package.rst
@@ -2,7 +2,7 @@
 Rule ``phpdoc_no_package``
 ==========================
 
-``@package`` and ``@subpackage`` annotations should be omitted from PHPDoc.
+``@package`` and ``@subpackage`` annotations must be removed from PHPDoc.
 
 Examples
 --------
diff --git a/src/Fixer/AttributeNotation/GeneralAttributeRemoveFixer.php b/src/Fixer/AttributeNotation/GeneralAttributeRemoveFixer.php
new file mode 100644
index 000000000..a276ea924
--- /dev/null
+++ b/src/Fixer/AttributeNotation/GeneralAttributeRemoveFixer.php
@@ -0,0 +1,142 @@
+<?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\Fixer\AttributeNotation;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\Fixer\ConfigurableFixerTrait;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
+use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Raffaele Carelle <raffaele.carelle@gmail.com>
+ *
+ * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
+ *
+ * @phpstan-import-type _AttributeItems from AttributeAnalysis
+ * @phpstan-import-type _AttributeItem from AttributeAnalysis
+ *
+ * @phpstan-type _AutogeneratedInputConfiguration array{
+ *  attributes?: list<string>
+ * }
+ * @phpstan-type _AutogeneratedComputedConfiguration array{
+ *  attributes: list<string>
+ * }
+ */
+final class GeneralAttributeRemoveFixer extends AbstractFixer implements ConfigurableFixerInterface
+{
+    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
+    use ConfigurableFixerTrait;
+
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Removes configured attributes by their respective FQN.',
+            [
+                new CodeSample(
+                    '<?php
+#[\A\B\Foo]
+function foo() {}
+',
+                    ['attributes' => ['\A\B\Foo']]
+                ),
+                new CodeSample(
+                    '<?php
+use A\B\Bar as BarAlias;
+
+#[\A\B\Foo]
+#[BarAlias]
+function foo() {}
+',
+                    ['attributes' => ['\A\B\Foo', 'A\B\Bar']]
+                ),
+            ]
+        );
+    }
+
+    public function getPriority(): int
+    {
+        return 0;
+    }
+
+    public function isCandidate(Tokens $tokens): bool
+    {
+        return \defined('T_ATTRIBUTE') && $tokens->isTokenKindFound(T_ATTRIBUTE);
+    }
+
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        if (0 === \count($this->configuration['attributes'])) {
+            return;
+        }
+
+        $index = 0;
+
+        while (null !== $index = $tokens->getNextTokenOfKind($index, [[T_ATTRIBUTE]])) {
+            $attributeAnalysis = AttributeAnalyzer::collectOne($tokens, $index);
+
+            $endIndex = $attributeAnalysis->getEndIndex();
+
+            $removedCount = 0;
+            foreach ($attributeAnalysis->getAttributes() as $element) {
+                $fullname = AttributeAnalyzer::determineAttributeFullyQualifiedName($tokens, $element['name'], $element['start']);
+
+                if (!\in_array($fullname, $this->configuration['attributes'], true)) {
+                    continue;
+                }
+
+                $tokens->clearRange($element['start'], $element['end']);
+                ++$removedCount;
+
+                $siblingIndex = $tokens->getNonEmptySibling($element['end'], 1);
+
+                // Clear element comma
+                if (',' === $tokens[$siblingIndex]->getContent()) {
+                    $tokens->clearAt($siblingIndex);
+                }
+            }
+
+            // Clear whole attribute if all are removed (multiline attribute case)
+            if (\count($attributeAnalysis->getAttributes()) === $removedCount) {
+                $tokens->clearRange($attributeAnalysis->getStartIndex(), $attributeAnalysis->getEndIndex());
+            }
+
+            // Clear trailing comma
+            $tokenIndex = $tokens->getMeaningfulTokenSibling($attributeAnalysis->getClosingBracketIndex(), -1);
+            if (',' === $tokens[$tokenIndex]->getContent()) {
+                $tokens->clearAt($tokenIndex);
+            }
+
+            $index = $endIndex;
+        }
+    }
+
+    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('attributes', 'List of FQNs of attributes for removal.'))
+                ->setAllowedTypes(['string[]'])
+                ->setDefault([])
+                ->getOption(),
+        ]);
+    }
+}
diff --git a/src/Fixer/AttributeNotation/OrderedAttributesFixer.php b/src/Fixer/AttributeNotation/OrderedAttributesFixer.php
index 3b54fbec0..957344e42 100644
--- a/src/Fixer/AttributeNotation/OrderedAttributesFixer.php
+++ b/src/Fixer/AttributeNotation/OrderedAttributesFixer.php
@@ -26,11 +26,7 @@
 use PhpCsFixer\FixerDefinition\VersionSpecification;
 use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
 use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
-use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
-use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
 use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
-use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
-use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 use Symfony\Component\OptionsResolver\Options;
@@ -221,40 +217,12 @@ private function sortAttributes(Tokens $tokens, int $index, array $attributes):
     private function getAttributeName(Tokens $tokens, string $name, int $index): string
     {
         if (self::ORDER_CUSTOM === $this->configuration['sort_algorithm']) {
-            $name = $this->determineAttributeFullyQualifiedName($tokens, $name, $index);
+            $name = AttributeAnalyzer::determineAttributeFullyQualifiedName($tokens, $name, $index);
         }
 
         return ltrim($name, '\\');
     }
 
-    private function determineAttributeFullyQualifiedName(Tokens $tokens, string $name, int $index): string
-    {
-        if ('\\' === $name[0]) {
-            return $name;
-        }
-
-        if (!$tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
-            $index = $tokens->getNextTokenOfKind($index, [[T_STRING], [T_NS_SEPARATOR]]);
-        }
-
-        [$namespaceAnalysis, $namespaceUseAnalyses] = $this->collectNamespaceAnalysis($tokens, $index);
-        $namespace = $namespaceAnalysis->getFullName();
-        $firstTokenOfName = $tokens[$index]->getContent();
-        $namespaceUseAnalysis = $namespaceUseAnalyses[$firstTokenOfName] ?? false;
-
-        if ($namespaceUseAnalysis instanceof NamespaceUseAnalysis) {
-            $namespace = $namespaceUseAnalysis->getFullName();
-
-            if ($name === $firstTokenOfName) {
-                return $namespace;
-            }
-
-            $name = substr(strstr($name, '\\'), 1);
-        }
-
-        return $namespace.'\\'.$name;
-    }
-
     /**
      * @param _AttributeItems $elements
      *
@@ -300,24 +268,4 @@ private function sortTokens(Tokens $tokens, int $startIndex, int $endIndex, arra
 
         $tokens->overrideRange($startIndex, $endIndex, $replaceTokens);
     }
-
-    /**
-     * @return array{NamespaceAnalysis, array<string, NamespaceUseAnalysis>}
-     */
-    private function collectNamespaceAnalysis(Tokens $tokens, int $startIndex): array
-    {
-        $namespaceAnalysis = (new NamespacesAnalyzer())->getNamespaceAt($tokens, $startIndex);
-        $namespaceUseAnalyses = (new NamespaceUsesAnalyzer())->getDeclarationsInNamespace($tokens, $namespaceAnalysis);
-
-        $uses = [];
-        foreach ($namespaceUseAnalyses as $use) {
-            if (!$use->isClass()) {
-                continue;
-            }
-
-            $uses[$use->getShortName()] = $use;
-        }
-
-        return [$namespaceAnalysis, $uses];
-    }
 }
diff --git a/src/Fixer/PhpUnit/PhpUnitAttributesFixer.php b/src/Fixer/PhpUnit/PhpUnitAttributesFixer.php
index 952f0bc41..49b045b79 100644
--- a/src/Fixer/PhpUnit/PhpUnitAttributesFixer.php
+++ b/src/Fixer/PhpUnit/PhpUnitAttributesFixer.php
@@ -17,7 +17,6 @@
 use PhpCsFixer\DocBlock\Annotation;
 use PhpCsFixer\DocBlock\DocBlock;
 use PhpCsFixer\Fixer\AbstractPhpUnitFixer;
-use PhpCsFixer\Fixer\AttributeNotation\OrderedAttributesFixer;
 use PhpCsFixer\Fixer\ConfigurableFixerInterface;
 use PhpCsFixer\Fixer\ConfigurableFixerTrait;
 use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
@@ -254,24 +253,9 @@ private static function isAttributeAlreadyPresent(Tokens $tokens, int $index, ar
             $insertedClassName .= $token->getContent();
         }
 
-        // @TODO: refactor OrderedAttributesFixer::determineAttributeFullyQualifiedName to shared analyzer
-        static $determineAttributeFullyQualifiedName = null;
-        static $orderedAttributesFixer = null;
-        if (null === $determineAttributeFullyQualifiedName) {
-            $orderedAttributesFixer = new OrderedAttributesFixer();
-            $reflection = new \ReflectionObject($orderedAttributesFixer);
-            $determineAttributeFullyQualifiedName = $reflection->getMethod('determineAttributeFullyQualifiedName');
-            $determineAttributeFullyQualifiedName->setAccessible(true);
-        }
-
         foreach (AttributeAnalyzer::collect($tokens, $attributeIndex) as $attributeAnalysis) {
             foreach ($attributeAnalysis->getAttributes() as $attribute) {
-                $className = ltrim($determineAttributeFullyQualifiedName->invokeArgs(
-                    $orderedAttributesFixer,
-                    [$tokens,
-                        $attribute['name'],
-                        $attribute['start']],
-                ), '\\');
+                $className = ltrim(AttributeAnalyzer::determineAttributeFullyQualifiedName($tokens, $attribute['name'], $attribute['start']), '\\');
 
                 if ($insertedClassName === $className) {
                     return true;
diff --git a/src/Fixer/Phpdoc/GeneralPhpdocAnnotationRemoveFixer.php b/src/Fixer/Phpdoc/GeneralPhpdocAnnotationRemoveFixer.php
index 18b66a352..2e5ab5200 100644
--- a/src/Fixer/Phpdoc/GeneralPhpdocAnnotationRemoveFixer.php
+++ b/src/Fixer/Phpdoc/GeneralPhpdocAnnotationRemoveFixer.php
@@ -51,7 +51,7 @@ final class GeneralPhpdocAnnotationRemoveFixer extends AbstractFixer implements
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
-            'Configured annotations should be omitted from PHPDoc.',
+            'Removes configured annotations from PHPDoc.',
             [
                 new CodeSample(
                     '<?php
diff --git a/src/Fixer/Phpdoc/PhpdocNoAccessFixer.php b/src/Fixer/Phpdoc/PhpdocNoAccessFixer.php
index e5f899ff1..d2f959a15 100644
--- a/src/Fixer/Phpdoc/PhpdocNoAccessFixer.php
+++ b/src/Fixer/Phpdoc/PhpdocNoAccessFixer.php
@@ -28,7 +28,7 @@ final class PhpdocNoAccessFixer extends AbstractProxyFixer
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
-            '`@access` annotations should be omitted from PHPDoc.',
+            '`@access` annotations must be removed from PHPDoc.',
             [
                 new CodeSample(
                     '<?php
diff --git a/src/Fixer/Phpdoc/PhpdocNoEmptyReturnFixer.php b/src/Fixer/Phpdoc/PhpdocNoEmptyReturnFixer.php
index 3b22bad97..c591c67ab 100644
--- a/src/Fixer/Phpdoc/PhpdocNoEmptyReturnFixer.php
+++ b/src/Fixer/Phpdoc/PhpdocNoEmptyReturnFixer.php
@@ -36,7 +36,7 @@ public function isCandidate(Tokens $tokens): bool
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
-            '`@return void` and `@return null` annotations should be omitted from PHPDoc.',
+            '`@return void` and `@return null` annotations must be removed from PHPDoc.',
             [
                 new CodeSample(
                     '<?php
diff --git a/src/Fixer/Phpdoc/PhpdocNoPackageFixer.php b/src/Fixer/Phpdoc/PhpdocNoPackageFixer.php
index 3e769762d..2f9278491 100644
--- a/src/Fixer/Phpdoc/PhpdocNoPackageFixer.php
+++ b/src/Fixer/Phpdoc/PhpdocNoPackageFixer.php
@@ -28,7 +28,7 @@ final class PhpdocNoPackageFixer extends AbstractProxyFixer
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
-            '`@package` and `@subpackage` annotations should be omitted from PHPDoc.',
+            '`@package` and `@subpackage` annotations must be removed from PHPDoc.',
             [
                 new CodeSample(
                     '<?php
diff --git a/src/Tokenizer/Analyzer/AttributeAnalyzer.php b/src/Tokenizer/Analyzer/AttributeAnalyzer.php
index 0b80a46f1..709f83213 100644
--- a/src/Tokenizer/Analyzer/AttributeAnalyzer.php
+++ b/src/Tokenizer/Analyzer/AttributeAnalyzer.php
@@ -16,6 +16,7 @@
 
 use PhpCsFixer\Preg;
 use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
 use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Tokens;
 
@@ -129,6 +130,34 @@ public static function collectOne(Tokens $tokens, int $index): AttributeAnalysis
         );
     }
 
+    public static function determineAttributeFullyQualifiedName(Tokens $tokens, string $name, int $index): string
+    {
+        if ('\\' === $name[0]) {
+            return $name;
+        }
+
+        if (!$tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
+            $index = $tokens->getNextTokenOfKind($index, [[T_STRING], [T_NS_SEPARATOR]]);
+        }
+
+        [$namespaceAnalysis, $namespaceUseAnalyses] = NamespacesAnalyzer::collectNamespaceAnalysis($tokens, $index);
+        $namespace = $namespaceAnalysis->getFullName();
+        $firstTokenOfName = $tokens[$index]->getContent();
+        $namespaceUseAnalysis = $namespaceUseAnalyses[$firstTokenOfName] ?? false;
+
+        if ($namespaceUseAnalysis instanceof NamespaceUseAnalysis) {
+            $namespace = $namespaceUseAnalysis->getFullName();
+
+            if ($name === $firstTokenOfName) {
+                return $namespace;
+            }
+
+            $name = substr((string) strstr($name, '\\'), 1);
+        }
+
+        return $namespace.'\\'.$name;
+    }
+
     /**
      * @return _AttributeItems
      */
diff --git a/src/Tokenizer/Analyzer/NamespacesAnalyzer.php b/src/Tokenizer/Analyzer/NamespacesAnalyzer.php
index 69d3b4192..ed1dacd4a 100644
--- a/src/Tokenizer/Analyzer/NamespacesAnalyzer.php
+++ b/src/Tokenizer/Analyzer/NamespacesAnalyzer.php
@@ -15,6 +15,7 @@
 namespace PhpCsFixer\Tokenizer\Analyzer;
 
 use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
 use PhpCsFixer\Tokenizer\Tokens;
 
 /**
@@ -92,4 +93,24 @@ public function getNamespaceAt(Tokens $tokens, int $index): NamespaceAnalysis
 
         throw new \LogicException(\sprintf('Unable to get the namespace at index %d.', $index));
     }
+
+    /**
+     * @return array{NamespaceAnalysis, array<string, NamespaceUseAnalysis>}
+     */
+    public static function collectNamespaceAnalysis(Tokens $tokens, int $startIndex): array
+    {
+        $namespaceAnalysis = (new self())->getNamespaceAt($tokens, $startIndex);
+        $namespaceUseAnalyses = (new NamespaceUsesAnalyzer())->getDeclarationsInNamespace($tokens, $namespaceAnalysis);
+
+        $uses = [];
+        foreach ($namespaceUseAnalyses as $use) {
+            if (!$use->isClass()) {
+                continue;
+            }
+
+            $uses[$use->getShortName()] = $use;
+        }
+
+        return [$namespaceAnalysis, $uses];
+    }
 }


Test patch:
diff --git a/tests/Fixer/AttributeNotation/GeneralAttributeRemoveFixerTest.php b/tests/Fixer/AttributeNotation/GeneralAttributeRemoveFixerTest.php
new file mode 100644
index 000000000..673f086aa
--- /dev/null
+++ b/tests/Fixer/AttributeNotation/GeneralAttributeRemoveFixerTest.php
@@ -0,0 +1,461 @@
+<?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\Fixer\AttributeNotation;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @author Raffaele Carelle <raffaele.carelle@gmail.com>
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Fixer\AttributeNotation\GeneralAttributeRemoveFixer
+ *
+ * @requires PHP 8.0
+ *
+ * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\AttributeNotation\GeneralAttributeRemoveFixer>
+ *
+ * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\AttributeNotation\GeneralAttributeRemoveFixer
+ */
+final class GeneralAttributeRemoveFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @param _AutogeneratedInputConfiguration $configuration
+     *
+     * @dataProvider provideFixCases
+     */
+    public function testFix(string $expected, ?string $input = null, array $configuration = []): void
+    {
+        $this->fixer->configure($configuration);
+
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array{0: string, 1?: string, 2?: array<string, mixed>}>
+     */
+    public static function provideFixCases(): iterable
+    {
+        yield 'Explicit in namespace' => [
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[Foo(4, \'baz qux\')]
+            #[AB\Baz(prop: \'baz\')]
+            #[\A\B\Qux()]
+            #[Corge]
+            function f() {}
+            ',
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[Foo(4, \'baz qux\')]
+            #[BarAlias(3)]
+            #[AB\Baz(prop: \'baz\')]
+            #[\A\B\Qux()]
+            #[A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')]
+            #[Corge]
+            function f() {}
+            ',
+            [
+                'attributes' => ['A\B\Bar', 'Test\A\B\Quux'],
+            ],
+        ];
+
+        yield 'Explicit in global namespace' => [
+            '<?php
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')]
+            #[AB\Baz(prop: \'baz\')]
+            #[Foo(4, \'baz qux\')]
+            function f() {}
+            ',
+            '<?php
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')]
+            #[BarAlias(3)]
+            #[AB\Baz(prop: \'baz\')]
+            #[Foo(4, \'baz qux\')]
+            #[\A\B\Qux()]
+            #[Corge]
+            function f() {}
+            ',
+            [
+                'attributes' => ['\A\B\Qux', '\Corge', 'A\B\Bar'],
+            ],
+        ];
+
+        yield 'Multiple namespaces' => [
+            '<?php
+            namespace Test
+            {
+                use A\B\Foo;
+                use A\B\Bar as BarAlias;
+
+                function f() {}
+            }
+
+            namespace Test2
+            {
+                use A\B\Bar as BarAlias;
+                use A\B as AB;
+
+                #[\A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')]
+                function f() {}
+            }
+
+            namespace Test
+            {
+                use A\B\Foo;
+                use A\B\Bar as BarAlias2;
+
+                function f2() {}
+            }
+
+            namespace
+            {
+                use A\B\Foo;
+                use A\B\Bar as BarAlias3;
+
+                function f() {}
+            }
+            ',
+            '<?php
+            namespace Test
+            {
+                use A\B\Foo;
+                use A\B\Bar as BarAlias;
+
+                #[AB\Baz(prop: \'baz\')]
+                #[Foo(4, \'baz qux\')]
+                #[BarAlias(3)]
+                function f() {}
+            }
+
+            namespace Test2
+            {
+                use A\B\Bar as BarAlias;
+                use A\B as AB;
+
+                #[BarAlias(3)]
+                #[AB\Baz(prop: \'baz\')]
+                #[\A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')]
+                function f() {}
+            }
+
+            namespace Test
+            {
+                use A\B\Foo;
+                use A\B\Bar as BarAlias2;
+
+                #[AB\Baz(prop: \'baz\')]
+                #[Foo(4, \'baz qux\')]
+                #[BarAlias2(3)]
+                function f2() {}
+            }
+
+            namespace
+            {
+                use A\B\Foo;
+                use A\B\Bar as BarAlias3;
+
+                #[AB\Baz(prop: \'baz\')]
+                #[Foo(4, \'baz qux\')]
+                #[BarAlias3(3)]
+                function f() {}
+            }
+            ',
+            [
+                'attributes' => ['A\B\Bar', 'Test\AB\Baz', 'A\B\Quux', 'A\B\Baz', 'A\B\Foo', '\AB\Baz'],
+            ],
+        ];
+
+        yield 'With whitespaces' => [
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[   \A\B\Qux()   ]
+            #[   BarAlias   (3)   ]
+            #[   Corge   ]
+            function f() {}
+            ',
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[   AB\Baz   (prop: \'baz\')   ]
+            #[   A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')   ]
+            #[   \A\B\Qux()   ]
+            #[   BarAlias   (3)   ]
+            #[   Corge   ]
+            #[   Foo   (4, \'baz qux\')   ]
+            function f() {}
+            ',
+            [
+                'attributes' => ['A\B\Foo', 'Test\A\B\Quux', 'A\B\Baz'],
+            ],
+        ];
+
+        yield 'With docblock' => [
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            /**
+             * Start docblock
+             */
+            /**
+             * AB\Baz docblock
+             */
+            #[AB\Baz(prop: \'baz\')]
+            #[A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')]
+            #[BarAlias(3)]
+            #[Foo(4, \'baz qux\')]
+            /**
+             * End docblock
+             */
+            class X
+            {}
+
+            function f2(/** Start docblock */#[Foo(4, \'baz qux\')] #[BarAlias(3)] /** End docblock */string $param) {}
+            ',
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            /**
+             * Start docblock
+             */
+            /**
+             * AB\Baz docblock
+             */
+            #[AB\Baz(prop: \'baz\')]
+            #[A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\')]
+            #[\A\B\Qux()]
+            #[BarAlias(3)]
+            /**
+             * Corge docblock
+             */
+            #[Corge]
+            #[Foo(4, \'baz qux\')]
+            /**
+             * End docblock
+             */
+            class X
+            {}
+
+            function f2(/** Start docblock */#[Foo(4, \'baz qux\')] #[BarAlias(3)] #[\A\B\Qux()] /** Corge docblock */#[Corge] /** End docblock */string $param) {}
+            ',
+            [
+                'attributes' => ['Test\Corge', '\A\B\Qux'],
+            ],
+        ];
+
+        yield 'With comments' => [
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            function f() {}
+            ',
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[/* comment */A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\') /* comment */]
+            #[ /* comment */ BarAlias/* comment */(3)/* comment */]
+            #[/* comment */ Corge/* comment */]
+            #[/* comment */AB\Baz /* comment */ (prop: \'baz\') /* comment */ ]
+            #[/* comment */Foo/* comment */(4, \'baz qux\') /* comment */ ]
+            #[   /* comment */   \A\B\Qux()/* comment */]
+            function f() {}
+            ',
+            [
+                'attributes' => ['Test\A\B\Quux', 'A\B\Bar', 'Test\Corge', 'A\B\Baz', 'A\B\Foo', '\A\B\Qux'],
+            ],
+        ];
+
+        yield 'With multiple attributes' => [
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[
+                AB\Baz(prop: \'baz\'),
+                A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'),
+                BarAlias(3),
+                Corge
+            ]
+            class X
+            {
+                #[ AB\Baz(prop: \'baz\'), A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'), BarAlias(3), Corge]
+                public function y() {}
+            }
+            ',
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[
+                AB\Baz(prop: \'baz\'),
+                A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'),
+                \A\B\Qux(),
+                BarAlias(3),
+                Corge,
+                Foo(4, \'baz qux\'),
+            ]
+            class X
+            {
+                #[ AB\Baz(prop: \'baz\'), A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'), \A\B\Qux(), BarAlias(3), Corge,Foo(4, \'baz qux\')]
+                public function y() {}
+            }
+            ',
+            [
+                'attributes' => ['A\B\Foo', '\A\B\Qux'],
+            ],
+        ];
+
+        yield 'Multiline with no trailing comma' => [
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            class X
+            {}
+            ',
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[
+                AB\Baz(prop: \'baz\'),
+                A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'),
+                \A\B\Qux(),
+                BarAlias(3),
+                Corge,
+                Foo(4, \'baz qux\')
+            ]
+            class X
+            {}
+            ',
+            [
+                'attributes' => ['A\B\Foo', '\A\B\Qux', 'A\B\Baz', 'Test\A\B\Quux', 'A\B\Bar', 'Test\Corge'],
+            ],
+        ];
+
+        yield 'Multiple with comments' => [
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[
+                /*
+                 * AB\Baz comment
+                 */
+                AB\Baz(prop: \'baz\'),
+                A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'),
+                \A\B\Qux(),
+                BarAlias(3)
+            ]
+            class X
+            {
+                #[ /* AB\Baz comment */AB\Baz(prop: \'baz\'), A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'), \A\B\Qux(), BarAlias(3)]
+                public function y() {}
+            }
+            ',
+            '<?php
+            namespace Test;
+
+            use A\B\Foo;
+            use A\B\Bar as BarAlias;
+            use A\B as AB;
+
+            #[
+                /*
+                 * AB\Baz comment
+                 */
+                AB\Baz(prop: \'baz\'),
+                A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'),
+                \A\B\Qux(),
+                BarAlias(3),
+                /*
+                 * Corge comment
+                 */
+                Corge,
+                /**
+                 * Foo docblock
+                 */
+                Foo(4, \'baz qux\'),
+            ]
+            class X
+            {
+                #[ /* AB\Baz comment */AB\Baz(prop: \'baz\'), A\B\Quux(prop1: [1, 2, 4], prop2: true, prop3: \'foo bar\'), \A\B\Qux(), BarAlias(3), /* Corge comment */Corge,/** Foo docblock */Foo(4, \'baz qux\')]
+                public function y() {}
+            }
+            ',
+            [
+                'attributes' => ['A\B\Foo', 'Test\Corge'],
+            ],
+        ];
+    }
+}
diff --git a/tests/Test/AbstractFixerTestCase.php b/tests/Test/AbstractFixerTestCase.php
index 51d84b075..61ed1e1c5 100644
--- a/tests/Test/AbstractFixerTestCase.php
+++ b/tests/Test/AbstractFixerTestCase.php
@@ -131,6 +131,7 @@ abstract class AbstractFixerTestCase extends TestCase
      */
     private array $allowedFixersWithoutDefaultCodeSample = [
         'general_phpdoc_annotation_remove' => true,
+        'general_attribute_remove' => true,
         'general_phpdoc_tag_rename' => true,
     ];