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

Issue:
SelfStaticAccessorFixer - Does not fix enums
### Description
`SelfStaticAccessorFixer` does not fix usage of `static` in enums.

### Runtime version
PHP8.3 nightly, but effects all PHP versions supporting enums (8.1 and up)

### Used command

> $ sudo docker-compose run --rm --user "$(id --user):$(id --group)" php-x php php-cs-fixer fix test.php --dry-run -vvv --diff --rules=self_static_accessor

### Configuration file

None

### Code snippet that reproduces the problem

  ```php
<?php
enum Foo
{
    public const A = 123;

    public static function bar(): void
    {
        echo static::A; // expected to be fixed to `echo self::A; `
    }
}

Foo::bar();
 ```

The rationale is that since enums cannot by design be extended (nor inherit for that matter) there is never a use-case for LSB so no use-case for using `static::` inside a enum. 

ref.: https://php.watch/versions/8.1/enums#enum-inheritance


Discussion:

Patch:
diff --git a/doc/list.rst b/doc/list.rst
index 998d9a009ee..d9bd23a6cf3 100644
--- a/doc/list.rst
+++ b/doc/list.rst
@@ -2774,7 +2774,7 @@ List of Available Rules
    `Source PhpCsFixer\\Fixer\\ClassNotation\\SelfAccessorFixer <./../src/Fixer/ClassNotation/SelfAccessorFixer.php>`_
 -  `self_static_accessor <./rules/class_notation/self_static_accessor.rst>`_
 
-   Inside a ``final`` class or anonymous class ``self`` should be preferred to ``static``.
+   Inside an enum or ``final``/anonymous class, ``self`` should be preferred over ``static``.
 
    Part of rule set `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_
 
diff --git a/doc/rules/class_notation/self_static_accessor.rst b/doc/rules/class_notation/self_static_accessor.rst
index 5a77091e309..1ce91ef7309 100644
--- a/doc/rules/class_notation/self_static_accessor.rst
+++ b/doc/rules/class_notation/self_static_accessor.rst
@@ -2,7 +2,7 @@
 Rule ``self_static_accessor``
 =============================
 
-Inside a ``final`` class or anonymous class ``self`` should be preferred to
+Inside an enum or ``final``/anonymous class, ``self`` should be preferred over
 ``static``.
 
 Examples
@@ -82,6 +82,25 @@ Example #4
         }
     };
 
+Example #5
+~~~~~~~~~~
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    enum Foo
+    {
+        public const A = 123;
+
+        public static function bar(): void
+        {
+   -        echo static::A;
+   +        echo self::A;
+        }
+    }
+
 Rule sets
 ---------
 
diff --git a/doc/rules/index.rst b/doc/rules/index.rst
index 1824efab998..53adc870d7e 100644
--- a/doc/rules/index.rst
+++ b/doc/rules/index.rst
@@ -198,7 +198,7 @@ Class Notation
   Inside class or interface element ``self`` should be preferred to the class name itself.
 - `self_static_accessor <./class_notation/self_static_accessor.rst>`_
 
-  Inside a ``final`` class or anonymous class ``self`` should be preferred to ``static``.
+  Inside an enum or ``final``/anonymous class, ``self`` should be preferred over ``static``.
 - `single_class_element_per_statement <./class_notation/single_class_element_per_statement.rst>`_
 
   There MUST NOT be more than one property or constant declared per statement.
diff --git a/src/Fixer/ClassNotation/SelfStaticAccessorFixer.php b/src/Fixer/ClassNotation/SelfStaticAccessorFixer.php
index 5b0d2d932c4..03b3b2df01e 100644
--- a/src/Fixer/ClassNotation/SelfStaticAccessorFixer.php
+++ b/src/Fixer/ClassNotation/SelfStaticAccessorFixer.php
@@ -18,6 +18,8 @@
 use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\FixerDefinition\VersionSpecification;
+use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 use PhpCsFixer\Tokenizer\TokensAnalyzer;
@@ -29,7 +31,7 @@ final class SelfStaticAccessorFixer extends AbstractFixer
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
-            'Inside a `final` class or anonymous class `self` should be preferred to `static`.',
+            'Inside an enum or `final`/anonymous class, `self` should be preferred over `static`.',
             [
                 new CodeSample(
                     '<?php
@@ -81,13 +83,29 @@ public function getBar()
 };
 '
                 ),
+                new VersionSpecificCodeSample(
+                    '<?php
+enum Foo
+{
+    public const A = 123;
+
+    public static function bar(): void
+    {
+        echo static::A;
+    }
+}
+',
+                    new VersionSpecification(8_01_00)
+                ),
             ]
         );
     }
 
     public function isCandidate(Tokens $tokens): bool
     {
-        return $tokens->isAllTokenKindsFound([T_CLASS, T_STATIC]) && $tokens->isAnyTokenKindsFound([T_DOUBLE_COLON, T_NEW, T_INSTANCEOF]);
+        return ($tokens->isAllTokenKindsFound([T_CLASS, T_STATIC])
+            || (\defined('T_ENUM') && $tokens->isAllTokenKindsFound([T_ENUM, T_STATIC]))) // @TODO drop condition when PHP 8.1+ is required
+            && $tokens->isAnyTokenKindsFound([T_DOUBLE_COLON, T_NEW, T_INSTANCEOF]);
     }
 
     /**
@@ -102,17 +120,27 @@ public function getPriority(): int
 
     protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
     {
+        $classyTokensOfInterest = [[T_CLASS]];
+
+        if (\defined('T_ENUM')) {
+            $classyTokensOfInterest[] = [T_ENUM]; // @TODO drop condition when PHP 8.1+ is required
+        }
+
         $this->tokensAnalyzer = new TokensAnalyzer($tokens);
-        $classIndex = $tokens->getNextTokenOfKind(0, [[T_CLASS]]);
+        $classyIndex = $tokens->getNextTokenOfKind(0, $classyTokensOfInterest);
 
-        while (null !== $classIndex) {
-            $modifiers = $this->tokensAnalyzer->getClassyModifiers($classIndex);
+        while (null !== $classyIndex) {
+            $modifiers = $this->tokensAnalyzer->getClassyModifiers($classyIndex);
 
-            if (isset($modifiers['final']) || $this->tokensAnalyzer->isAnonymousClass($classIndex)) {
-                $classIndex = $this->fixClass($tokens, $classIndex);
+            if (
+                isset($modifiers['final'])
+                || $this->tokensAnalyzer->isAnonymousClass($classyIndex)
+                || (\defined('T_ENUM') && $tokens[$classyIndex]->isGivenKind(T_ENUM)) // @TODO drop condition when PHP 8.1+ is required
+            ) {
+                $classyIndex = $this->fixClass($tokens, $classyIndex);
             }
 
-            $classIndex = $tokens->getNextTokenOfKind($classIndex, [[T_CLASS]]);
+            $classyIndex = $tokens->getNextTokenOfKind($classyIndex, $classyTokensOfInterest);
         }
     }
 


Test patch:
diff --git a/tests/Fixer/ClassNotation/SelfStaticAccessorFixerTest.php b/tests/Fixer/ClassNotation/SelfStaticAccessorFixerTest.php
index 2adc4302471..2cac9771378 100644
--- a/tests/Fixer/ClassNotation/SelfStaticAccessorFixerTest.php
+++ b/tests/Fixer/ClassNotation/SelfStaticAccessorFixerTest.php
@@ -360,6 +360,62 @@ public function Bar()
         ];
     }
 
+    /**
+     * @dataProvider provideFix81Cases
+     *
+     * @requires PHP 8.1
+     */
+    public function testFix81(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public static function provideFix81Cases(): iterable
+    {
+        yield 'enums' => [
+            '<?php
+enum Foo
+{
+    case Baz;
+
+    private const BAR = \'foo\';
+
+    public static function bar(): Foo
+    {
+        return self::Baz;
+    }
+
+    public static function baz(mixed $other): void
+    {
+        if ($other instanceof self) {
+            echo self::BAR;
+        }
+    }
+}
+',
+            '<?php
+enum Foo
+{
+    case Baz;
+
+    private const BAR = \'foo\';
+
+    public static function bar(): Foo
+    {
+        return static::Baz;
+    }
+
+    public static function baz(mixed $other): void
+    {
+        if ($other instanceof static) {
+            echo static::BAR;
+        }
+    }
+}
+',
+        ];
+    }
+
     /**
      * @dataProvider provideFix82Cases
      *