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

Issue:
Fixer proposal: remove `@final` annotation from blocks already marked as final
Example that doesn't need the final annotation anymore:

<img width="285" alt="image" src="https://user-images.githubusercontent.com/2829600/132575614-8151686b-2b76-427a-8119-050c0e525be7.png">

We could automate the removal of this. Simiarly for final methods, constants (php 8.1), etc.

Pull request description:
NoSuperfluousPhpdocTagsFixer - Remove superfluous annotation `@abstract` and `@final`
Closes #5985 

Patch:
diff --git a/src/DocBlock/Annotation.php b/src/DocBlock/Annotation.php
index 5d4376427..c27081baf 100644
--- a/src/DocBlock/Annotation.php
+++ b/src/DocBlock/Annotation.php
@@ -23,8 +23,6 @@
  *
  * @author Graham Campbell <hello@gjcampbell.co.uk>
  * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
- *
- * @final
  */
 final class Annotation
 {
diff --git a/src/DocBlock/DocBlock.php b/src/DocBlock/DocBlock.php
index 827db5afa..6e873014a 100644
--- a/src/DocBlock/DocBlock.php
+++ b/src/DocBlock/DocBlock.php
@@ -24,8 +24,6 @@
  * It internally splits it up into "lines" that we can manipulate.
  *
  * @author Graham Campbell <hello@gjcampbell.co.uk>
- *
- * @final
  */
 final class DocBlock
 {
diff --git a/src/DocBlock/Line.php b/src/DocBlock/Line.php
index d54c53da6..5e757f5d7 100644
--- a/src/DocBlock/Line.php
+++ b/src/DocBlock/Line.php
@@ -20,8 +20,6 @@
  * This represents a line of a docblock.
  *
  * @author Graham Campbell <hello@gjcampbell.co.uk>
- *
- * @final
  */
 final class Line
 {
diff --git a/src/DocBlock/Tag.php b/src/DocBlock/Tag.php
index 225e30362..f06522b9d 100644
--- a/src/DocBlock/Tag.php
+++ b/src/DocBlock/Tag.php
@@ -20,8 +20,6 @@
  * This represents a tag, as defined by the proposed PSR PHPDoc standard.
  *
  * @author Graham Campbell <hello@gjcampbell.co.uk>
- *
- * @final
  */
 final class Tag
 {
diff --git a/src/DocBlock/TagComparator.php b/src/DocBlock/TagComparator.php
index c49761a61..d0e7c8744 100644
--- a/src/DocBlock/TagComparator.php
+++ b/src/DocBlock/TagComparator.php
@@ -19,8 +19,6 @@
  * together, or kept apart.
  *
  * @author Graham Campbell <hello@gjcampbell.co.uk>
- *
- * @final
  */
 final class TagComparator
 {
diff --git a/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php b/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php
index 74360c598..c34f45e3b 100644
--- a/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php
+++ b/src/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixer.php
@@ -120,22 +120,24 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
             }
 
             $content = $initialContent = $token->getContent();
-            $documentedElementIndex = $this->findDocumentedElement($tokens, $index);
+            $documentedElement = $this->findDocumentedElement($tokens, $index);
 
-            if (null === $documentedElementIndex) {
+            if (null === $documentedElement) {
                 continue;
             }
 
-            $token = $tokens[$documentedElementIndex];
-
             if (true === $this->configuration['remove_inheritdoc']) {
                 $content = $this->removeSuperfluousInheritDoc($content);
             }
 
-            if ($token->isGivenKind(T_FUNCTION)) {
-                $content = $this->fixFunctionDocComment($content, $tokens, $index, $shortNames);
-            } elseif ($token->isGivenKind(T_VARIABLE)) {
-                $content = $this->fixPropertyDocComment($content, $tokens, $index, $shortNames);
+            if ('function' === $documentedElement['type']) {
+                $content = $this->fixFunctionDocComment($content, $tokens, $documentedElement, $shortNames);
+            } elseif ('property' === $documentedElement['type']) {
+                $content = $this->fixPropertyDocComment($content, $tokens, $documentedElement, $shortNames);
+            } elseif ('classy' === $documentedElement['type']) {
+                $content = $this->fixClassDocComment($content, $documentedElement);
+            } else {
+                throw new \RuntimeException('Unknown type.');
             }
 
             if ('' === $content) {
@@ -169,46 +171,81 @@ protected function createConfigurationDefinition(): FixerConfigurationResolverIn
         ]);
     }
 
-    private function findDocumentedElement(Tokens $tokens, int $docCommentIndex): ?int
+    private function findDocumentedElement(Tokens $tokens, int $docCommentIndex): ?array
     {
-        $index = $docCommentIndex;
-
-        do {
-            $index = $tokens->getNextMeaningfulToken($index);
-
-            if (null === $index || $tokens[$index]->isGivenKind([T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT])) {
-                return $index;
-            }
-        } while ($tokens[$index]->isGivenKind([T_ABSTRACT, T_FINAL, T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC]));
+        $modifierKinds = [
+            T_PRIVATE,
+            T_PROTECTED,
+            T_PUBLIC,
+            T_ABSTRACT,
+            T_FINAL,
+            T_STATIC,
+        ];
 
-        $kindsBeforeProperty = [T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION, T_STRING, T_NS_SEPARATOR];
+        $typeKinds = [
+            CT::T_NULLABLE_TYPE,
+            CT::T_ARRAY_TYPEHINT,
+            CT::T_TYPE_ALTERNATION,
+            T_STRING,
+            T_NS_SEPARATOR,
+        ];
 
         if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
-            $kindsBeforeProperty[] = T_READONLY;
+            $modifierKinds[] = T_READONLY;
         }
 
+        $element = [
+            'modifiers' => [],
+            'types' => [],
+        ];
+
         $index = $tokens->getNextMeaningfulToken($docCommentIndex);
 
-        if (!$tokens[$index]->isGivenKind($kindsBeforeProperty)) {
-            return null;
-        }
+        while (true) {
+            if (null === $index) {
+                break;
+            }
 
-        do {
-            $index = $tokens->getNextMeaningfulToken($index);
+            if ($tokens[$index]->isClassy()) {
+                $element['index'] = $index;
+                $element['type'] = 'classy';
+
+                return $element;
+            }
+
+            if ($tokens[$index]->isGivenKind(T_FUNCTION)) {
+                $element['index'] = $index;
+                $element['type'] = 'function';
+
+                return $element;
+            }
 
             if ($tokens[$index]->isGivenKind(T_VARIABLE)) {
-                return $index;
+                $element['index'] = $index;
+                $element['type'] = 'property';
+
+                return $element;
+            }
+
+            if ($tokens[$index]->isGivenKind($modifierKinds)) {
+                $element['modifiers'][$index] = $tokens[$index];
+            } elseif ($tokens[$index]->isGivenKind($typeKinds)) {
+                $element['types'][$index] = $tokens[$index];
+            } else {
+                break;
             }
-        } while ($tokens[$index]->isGivenKind($kindsBeforeProperty));
+
+            $index = $tokens->getNextMeaningfulToken($index);
+        }
 
         return null;
     }
 
-    private function fixFunctionDocComment(string $content, Tokens $tokens, int $functionIndex, array $shortNames): string
+    private function fixFunctionDocComment(string $content, Tokens $tokens, array $element, array $shortNames): string
     {
         $docBlock = new DocBlock($content);
 
-        $openingParenthesisIndex = $tokens->getNextTokenOfKind($functionIndex, ['(']);
+        $openingParenthesisIndex = $tokens->getNextTokenOfKind($element['index'], ['(']);
         $closingParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openingParenthesisIndex);
 
         $argumentsInfo = $this->getArgumentsInfo(
@@ -241,28 +278,24 @@ private function fixFunctionDocComment(string $content, Tokens $tokens, int $fun
             }
         }
 
+        $this->removeSuperfluousModifierAnnotation($docBlock, $element);
+
         return $docBlock->getContent();
     }
 
-    /**
-     * @param int $index Index of the DocComment token
-     */
-    private function fixPropertyDocComment(string $content, Tokens $tokens, int $index, array $shortNames): string
+    private function fixPropertyDocComment(string $content, Tokens $tokens, array $element, array $shortNames): string
     {
-        $propertyModifierKinds = [T_STATIC, T_PRIVATE, T_PROTECTED, T_PUBLIC];
-
-        if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
-            $propertyModifierKinds[] = T_READONLY;
+        if (\count($element['types']) > 0) {
+            $propertyTypeInfo = $this->parseTypeHint($tokens, array_key_first($element['types']));
+        } else {
+            $propertyTypeInfo = [
+                'types' => [],
+                'allows_null' => true,
+            ];
         }
 
         $docBlock = new DocBlock($content);
 
-        do {
-            $index = $tokens->getNextMeaningfulToken($index);
-        } while ($tokens[$index]->isGivenKind($propertyModifierKinds));
-
-        $propertyTypeInfo = $this->getPropertyTypeInfo($tokens, $index);
-
         foreach ($docBlock->getAnnotationsOfType('var') as $annotation) {
             if ($this->annotationIsSuperfluous($annotation, $propertyTypeInfo, $shortNames)) {
                 $annotation->remove();
@@ -272,6 +305,15 @@ private function fixPropertyDocComment(string $content, Tokens $tokens, int $ind
         return $docBlock->getContent();
     }
 
+    private function fixClassDocComment(string $content, array $element): string
+    {
+        $docBlock = new DocBlock($content);
+
+        $this->removeSuperfluousModifierAnnotation($docBlock, $element);
+
+        return $docBlock->getContent();
+    }
+
     /**
      * @return array<string, array>
      */
@@ -302,7 +344,7 @@ private function getArgumentsInfo(Tokens $tokens, int $start, int $end): array
                 $nextIndex = $tokens->getNextMeaningfulToken($index);
                 if (
                     $tokens[$nextIndex]->equals('=')
-                    && $tokens[$tokens->getNextMeaningfulToken($nextIndex)]->equals([T_STRING, 'null'])
+                    && $tokens[$tokens->getNextMeaningfulToken($nextIndex)]->equals([T_STRING, 'null'], false)
                 ) {
                     $info['allows_null'] = true;
                 }
@@ -327,21 +369,6 @@ private function getReturnTypeInfo(Tokens $tokens, int $closingParenthesisIndex)
         ];
     }
 
-    /**
-     * @param int $index The index of the first token of the type hint
-     */
-    private function getPropertyTypeInfo(Tokens $tokens, int $index): array
-    {
-        if ($tokens[$index]->isGivenKind(T_VARIABLE)) {
-            return [
-                'types' => [],
-                'allows_null' => true,
-            ];
-        }
-
-        return $this->parseTypeHint($tokens, $index);
-    }
-
     /**
      * @param int $index The index of the first token of the type hint
      */
@@ -496,4 +523,19 @@ private function removeSuperfluousInheritDoc(string $docComment): string
             )
         ~ix', '$1$2', $docComment);
     }
+
+    private function removeSuperfluousModifierAnnotation(DocBlock $docBlock, array $element): void
+    {
+        foreach (['abstract' => T_ABSTRACT, 'final' => T_FINAL] as $annotationType => $modifierToken) {
+            $annotations = $docBlock->getAnnotationsOfType($annotationType);
+
+            foreach ($element['modifiers'] as $token) {
+                if ($token->isGivenKind($modifierToken)) {
+                    foreach ($annotations as $annotation) {
+                        $annotation->remove();
+                    }
+                }
+            }
+        }
+    }
 }


Test patch:
diff --git a/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php b/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
index c8868a30f..a28a382c3 100644
--- a/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
+++ b/tests/Fixer/Phpdoc/NoSuperfluousPhpdocTagsFixerTest.php
@@ -66,14 +66,14 @@ public function doFoo(Bar $bar) {}
 class Foo {
     /**
      */
-    public function doFoo(Bar $bar = null) {}
+    public function doFoo(Bar $bar = NULL) {}
 }',
                 '<?php
 class Foo {
     /**
      * @param Bar|null $bar
      */
-    public function doFoo(Bar $bar = null) {}
+    public function doFoo(Bar $bar = NULL) {}
 }',
             ],
             'same typehint with description' => [
@@ -1041,13 +1041,17 @@ public function doFoo($foo) {}
 }',
                 ['remove_inheritdoc' => true],
             ],
-            'dont_remove_inheritdoc_non_structural_element' => [
+            'remove_inheritdoc_non_structural_element_it_does_not_inherit' => [
+                '<?php
+/**
+ *
+ */
+$foo = 1;',
                 '<?php
 /**
  * @inheritDoc
  */
 $foo = 1;',
-                null,
                 ['remove_inheritdoc' => true],
             ],
             'property with unsupported type' => [
@@ -1406,6 +1410,92 @@ class Foo {
     public function doFoo(iterable $bar, ?int $baz): ?array {}
 }',
             ],
+            'remove abstract annotation in function' => [
+                '<?php
+abstract class Foo {
+    /**
+     */
+    public abstract function doFoo();
+}',
+                '<?php
+abstract class Foo {
+    /**
+     * @abstract
+     */
+    public abstract function doFoo();
+}', ],
+            'dont remove abstract annotation in function' => [
+                '<?php
+class Foo {
+    /**
+     * @abstract
+     */
+    public function doFoo() {}
+}', ],
+            'remove final annotation in function' => [
+                '<?php
+class Foo {
+    /**
+     */
+    public final function doFoo() {}
+}',
+                '<?php
+class Foo {
+    /**
+     * @final
+     */
+    public final function doFoo() {}
+}', ],
+            'dont remove final annotation in function' => [
+                '<?php
+class Foo {
+    /**
+     * @final
+     */
+    public function doFoo() {}
+}', ],
+            'remove abstract annotation in class' => [
+                '<?php
+/**
+ */
+abstract class Foo {
+}',
+                '<?php
+/**
+ * @abstract
+ */
+abstract class Foo {
+}', ],
+            'dont remove abstract annotation in class' => [
+                '<?php
+abstract class Bar{}
+
+/**
+ * @abstract
+ */
+class Foo {
+}', ],
+            'remove final annotation in class' => [
+                '<?php
+/**
+ */
+final class Foo {
+}',
+                '<?php
+/**
+ * @final
+ */
+final class Foo {
+}', ],
+            'dont remove final annotation in class' => [
+                '<?php
+final class Bar{}
+
+/**
+ * @final
+ */
+class Foo {
+}', ],
         ];
     }