Browse Source

Add PhpdocNoUselessInheritdocFixer

SpacePossum 8 years ago
parent
commit
571e5727f5

+ 1 - 0
.php_cs.dist

@@ -29,6 +29,7 @@ return PhpCsFixer\Config::create()
         'ordered_imports' => true,
         'php_unit_strict' => true,
         'phpdoc_add_missing_param_annotation' => true,
+        'phpdoc_no_useless_inheritdoc' => true,
         'phpdoc_order' => true,
         'psr4' => true,
         'strict_comparison' => true,

+ 3 - 0
README.rst

@@ -480,6 +480,9 @@ Choose from the list of available rules:
 * **phpdoc_no_package** [@Symfony]
    | @package and @subpackage annotations should be omitted from phpdocs.
 
+* **phpdoc_no_useless_inheritdoc**
+   | Classy that does not inherit must not have inheritdoc tags.
+
 * **phpdoc_order**
    | Annotations in phpdocs should be ordered so that param annotations come
    | first, then throws annotations, then return annotations.

+ 0 - 3
src/AbstractAlignFixerHelper.php

@@ -35,9 +35,6 @@ abstract class AbstractAlignFixerHelper
      */
     protected $deepestLevel = 0;
 
-    /**
-     * {@inheritdoc}
-     */
     public function fix(\SplFileInfo $file, Tokens $tokens)
     {
         // This fixer works partially on Tokens and partially on string representation of code.

+ 192 - 0
src/Fixer/Phpdoc/PhpdocNoUselessInheritdocFixer.php

@@ -0,0 +1,192 @@
+<?php
+
+/*
+ * 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\Phpdoc;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * Remove inheritdoc tags from classy that does not inherit.
+ *
+ * @author SpacePossum
+ */
+final class PhpdocNoUselessInheritdocFixer extends AbstractFixer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function fix(\SplFileInfo $file, Tokens $tokens)
+    {
+        // min. offset 4 as minimal candidate is @: <?php\n/** @inheritdoc */class min{}
+        for ($index = 1, $count = count($tokens) - 4; $index < $count; ++$index) {
+            if ($tokens[$index]->isGivenKind(array(T_CLASS, T_INTERFACE))) {
+                $index = $this->fixClassy($tokens, $index);
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Classy that does not inherit must not have inheritdoc tags.',
+            array(
+                new CodeSample("<?php\n/** {@inheritdoc} */\nclass Sample\n{\n}"),
+                new CodeSample("<?php\nclass Sample\n{\n    /**\n     * @inheritdoc\n     */\n    public function Test()\n    {\n    }\n}"),
+            )
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority()
+    {
+        // should run before NoEmptyPhpdocFixer and PhpdocInlineTagFixer and after PhpdocToCommentFixer.
+        return 6;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound(T_DOC_COMMENT) && $tokens->isAnyTokenKindsFound(array(T_CLASS, T_INTERFACE));
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $index
+     *
+     * @return int
+     */
+    private function fixClassy(Tokens $tokens, $index)
+    {
+        // figure out where the classy starts
+        $classOpenIndex = $tokens->getNextTokenOfKind($index, array('{'));
+
+        // figure out where the classy ends
+        $classEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpenIndex);
+
+        // is classy extending or implementing some interface
+        $extendingOrImplementing = $this->isExtendingOrImplementing($tokens, $index, $classOpenIndex);
+
+        if (!$extendingOrImplementing) {
+            // PHPDoc of classy should not have inherit tag even when using traits as Traits cannot provide this information
+            $this->fixClassyOutside($tokens, $index);
+        }
+
+        // figure out if the classy uses a trait
+        if (!$extendingOrImplementing && $this->isUsingTrait($tokens, $index, $classOpenIndex, $classEndIndex)) {
+            $extendingOrImplementing = true;
+        }
+
+        $this->fixClassyInside($tokens, $classOpenIndex, $classEndIndex, !$extendingOrImplementing);
+
+        return $classEndIndex;
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $classOpenIndex
+     * @param int    $classEndIndex
+     * @param bool   $fixThisLevel
+     */
+    private function fixClassyInside(Tokens $tokens, $classOpenIndex, $classEndIndex, $fixThisLevel)
+    {
+        for ($i = $classOpenIndex; $i < $classEndIndex; ++$i) {
+            if ($tokens[$i]->isGivenKind(T_CLASS)) {
+                $i = $this->fixClassy($tokens, $i);
+            } elseif ($fixThisLevel && $tokens[$i]->isGivenKind(T_DOC_COMMENT)) {
+                $this->fixToken($tokens, $i);
+            }
+        }
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $classIndex
+     */
+    private function fixClassyOutside(Tokens $tokens, $classIndex)
+    {
+        $previousIndex = $tokens->getPrevNonWhitespace($classIndex);
+        if ($tokens[$previousIndex]->isGivenKind(T_DOC_COMMENT)) {
+            $this->fixToken($tokens, $previousIndex);
+        }
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $tokenIndex
+     */
+    private function fixToken(Tokens $tokens, $tokenIndex)
+    {
+        $count = 0;
+        $content = preg_replace_callback(
+            '#([\s]*(?:@{*|{*[ \t]*@)[ \t]*inheritdoc[\s]*)([^}]*)((?:}*)[\s]*)#i',
+            function ($matches) {
+                return ' '.$matches[2];
+            },
+            $tokens[$tokenIndex]->getContent(),
+            -1,
+            $count
+        );
+
+        if ($count) {
+            $tokens[$tokenIndex]->setContent($content);
+        }
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $classIndex
+     * @param int    $classOpenIndex
+     *
+     * @return bool
+     */
+    private function isExtendingOrImplementing(Tokens $tokens, $classIndex, $classOpenIndex)
+    {
+        for ($index = $classIndex; $index < $classOpenIndex; ++$index) {
+            if ($tokens[$index]->isGivenKind(array(T_EXTENDS, T_IMPLEMENTS))) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $classIndex
+     * @param int    $classOpenIndex
+     * @param int    $classCloseIndex
+     *
+     * @return bool
+     */
+    private function isUsingTrait(Tokens $tokens, $classIndex, $classOpenIndex, $classCloseIndex)
+    {
+        if ($tokens[$classIndex]->isGivenKind(T_INTERFACE)) {
+            // cannot use Trait inside an interface
+            return false;
+        }
+
+        $useIndex = $tokens->getNextTokenOfKind($classOpenIndex, array(array(CT::T_USE_TRAIT)));
+
+        return null !== $useIndex && $useIndex < $classCloseIndex;
+    }
+}

+ 359 - 0
tests/Fixer/Phpdoc/PhpdocNoUselessInheritdocFixerTest.php

@@ -0,0 +1,359 @@
+<?php
+
+/*
+ * 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\Phpdoc;
+
+use PhpCsFixer\Test\AbstractFixerTestCase;
+
+/**
+ * @internal
+ *
+ * @author SpacePossum
+ */
+final class PhpdocNoUselessInheritdocFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @param string      $expected
+     * @param string|null $input
+     *
+     * @dataProvider provideDoFixCases
+     */
+    public function testFix($expected, $input = null)
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public function provideDoFixCases()
+    {
+        return array(
+            array(
+                "<?php\n/** */class min1{}",
+                "<?php\n/** @inheritdoc */class min1{}",
+            ),
+            array(
+                "<?php\nclass min2{/** */}",
+                "<?php\nclass min2{/** @inheritdoc */}",
+            ),
+            array(
+                '<?php
+                class A
+                {
+                    /** */
+                    public function A(){}
+                }
+                ',
+                '<?php
+                class A
+                {
+                    /** @inheritdoc */
+                    public function A(){}
+                }
+                ',
+            ),
+            array(
+                '<?php
+                class B
+                {
+                    /** */
+                    public function B(){}
+                }
+                ',
+                '<?php
+                class B
+                {
+                    /** {@INHERITDOC} */
+                    public function B(){}
+                }
+                ',
+            ),
+            array(
+                '<?php
+                /** D C */
+                class C
+                {
+                }
+                ',
+                '<?php
+                /** D {    @INHERITDOC   } C */
+                class C
+                {
+                }
+                ',
+            ),
+            array(
+                '<?php
+                /** E */
+                class E
+                {
+                }
+                ',
+                '<?php
+                /**     {{@Inheritdoc}}   E */
+                class E
+                {
+                }
+                ',
+            ),
+            array(
+                '<?php
+                /** F */
+                class F
+                {
+                }
+                ',
+                '<?php
+                /** F    @inheritdoc      */
+                class F
+                {
+                }
+                ',
+            ),
+            array(
+                '<?php
+                    /** */
+                    class G1{}
+                    /** */
+                    class G2{}
+                ',
+                '<?php
+                    /** @inheritdoc */
+                    class G1{}
+                    /** @inheritdoc */
+                    class G2{}
+                ',
+            ),
+            array(
+                '<?php
+                class H
+                {
+                    /* @inheritdoc comment, not PHPDoc */
+                    public function H(){}
+                }
+                ',
+            ),
+            array(
+                '<?php
+                class J extends Z
+                {
+                    /** @inheritdoc */
+                    public function H(){}
+                }
+                ',
+            ),
+            array(
+                '<?php
+                interface K extends Z
+                {
+                    /** @inheritdoc */
+                    public function H();
+                }
+                ',
+            ),
+            array(
+                '<?php
+                /** */
+                interface K
+                {
+                    /** */
+                    public function H();
+                }
+                ',
+                '<?php
+                /** @{inheritdoc} */
+                interface K
+                {
+                    /** {@Inheritdoc} */
+                    public function H();
+                }
+                ',
+            ),
+        );
+    }
+
+    /**
+     * @param string      $expected
+     * @param null|string $input
+     *
+     * @dataProvider provide54Cases
+     * @requires PHP 5.4
+     */
+    public function testFix54($expected, $input = null)
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public function provide54Cases()
+    {
+        return array(
+            array(
+                '<?php
+                trait T
+                {
+                    /** @inheritdoc */
+                    public function T()
+                    {
+                    }
+                }',
+            ),
+            array(
+                '<?php
+                class B
+                {
+                    /** */
+                    public function falseImportFromTrait()
+                    {
+                    }
+                }
+
+                /** */
+                class A
+                {
+                    use T;
+                    
+                    /** @inheritdoc */
+                    public function importFromTrait()
+                    {
+                    }
+                }
+                ',
+                '<?php
+                class B
+                {
+                    /** @inheritdoc */
+                    public function falseImportFromTrait()
+                    {
+                    }
+                }
+
+                /** @inheritdoc */
+                class A
+                {
+                    use T;
+                    
+                    /** @inheritdoc */
+                    public function importFromTrait()
+                    {
+                    }
+                }
+                ',
+            ),
+        );
+    }
+
+    /**
+     * @param string      $expected
+     * @param null|string $input
+     *
+     * @dataProvider provide70Cases
+     * @requires PHP 7.0
+     */
+    public function testFix70($expected, $input = null)
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public function provide70Cases()
+    {
+        return array(
+            array(
+'<?php
+
+/** delete 1 */
+class A
+{
+    /** delete 2 */
+    public function B()
+    {
+        $a = new class implements I {
+
+            /** @inheritdoc keep */
+            public function A()
+            {
+                $b = new class extends D {
+
+                    /** @inheritdoc keep */
+                    public function C()
+                    {
+                        $d = new class() {
+
+                            /** delete 3 */
+                            public function D()
+                            {
+                            }
+                        };
+                    }
+                };
+            }
+        };
+    }
+
+    /** delete 4 */
+    public function B1()
+    {
+        $a1 = new class(){ };
+    }
+
+    /** delete 5 */
+    public function B2()
+    {
+        //$a1 = new class(){ use D; };
+    }
+}
+',
+'<?php
+
+/** @inheritdoc delete 1 */
+class A
+{
+    /** @inheritdoc delete 2 */
+    public function B()
+    {
+        $a = new class implements I {
+
+            /** @inheritdoc keep */
+            public function A()
+            {
+                $b = new class extends D {
+
+                    /** @inheritdoc keep */
+                    public function C()
+                    {
+                        $d = new class() {
+
+                            /** @inheritdoc delete 3 */
+                            public function D()
+                            {
+                            }
+                        };
+                    }
+                };
+            }
+        };
+    }
+
+    /** @inheritdoc delete 4 */
+    public function B1()
+    {
+        $a1 = new class(){ };
+    }
+
+    /** @inheritdoc delete 5 */
+    public function B2()
+    {
+        //$a1 = new class(){ use D; };
+    }
+}
+',
+            ),
+        );
+    }
+}

+ 3 - 0
tests/FixerFactoryTest.php

@@ -300,6 +300,9 @@ final class FixerFactoryTest extends \PHPUnit_Framework_TestCase
             array($fixers['protected_to_private'], $fixers['ordered_class_elements']), // tested also in: protected_to_private,ordered_class_elements.test
             array($fixers['phpdoc_add_missing_param_annotation'], $fixers['phpdoc_align']), // tested also in: phpdoc_add_missing_param_annotation,phpdoc_align.test
             array($fixers['phpdoc_no_alias_tag'], $fixers['phpdoc_add_missing_param_annotation']), // tested also in: phpdoc_no_alias_tag,phpdoc_add_missing_param_annotation.test
+            array($fixers['phpdoc_no_useless_inheritdoc'], $fixers['no_empty_phpdoc']), // tested also in: phpdoc_no_useless_inheritdoc,no_empty_phpdoc.test
+            array($fixers['phpdoc_no_useless_inheritdoc'], $fixers['phpdoc_inline_tag']), // tested also in: phpdoc_no_useless_inheritdoc,phpdoc_inline_tag.test
+            array($fixers['phpdoc_to_comment'], $fixers['phpdoc_no_useless_inheritdoc']), // tested also in: phpdoc_to_comment,phpdoc_no_useless_inheritdoc.test
         );
 
         // prepare bulk tests for phpdoc fixers to test that:

+ 25 - 0
tests/Fixtures/Integration/priority/phpdoc_no_useless_inheritdoc,no_empty_phpdoc.test

@@ -0,0 +1,25 @@
+--TEST--
+Integration of fixers: phpdoc_no_useless_inheritdoc,no_empty_phpdoc.
+--RULESET--
+{"phpdoc_no_useless_inheritdoc": true, "no_empty_phpdoc": true}
+--EXPECT--
+<?php
+
+class A
+{
+
+public function A()
+{
+}
+}
+
+--INPUT--
+<?php
+
+class A
+{
+/** {@inheritdoc} */
+public function A()
+{
+}
+}

+ 29 - 0
tests/Fixtures/Integration/priority/phpdoc_no_useless_inheritdoc,phpdoc_inline_tag.test

@@ -0,0 +1,29 @@
+--TEST--
+Integration of fixers: phpdoc_no_useless_inheritdoc,phpdoc_inline_tag.
+--RULESET--
+{"phpdoc_no_useless_inheritdoc": true, "phpdoc_inline_tag": true}
+--SETTINGS--
+{"checkPriority": false}
+--EXPECT--
+<?php
+class A
+{
+    /**
+     * a
+     */
+    public function A()
+    {
+    }
+}
+
+--INPUT--
+<?php
+class A
+{
+    /**
+     * @{inheritdoc} a
+     */
+    public function A()
+    {
+    }
+}

+ 21 - 0
tests/Fixtures/Integration/priority/phpdoc_to_comment,phpdoc_no_useless_inheritdoc.test

@@ -0,0 +1,21 @@
+--TEST--
+Integration of fixers: phpdoc_to_comment,phpdoc_no_useless_inheritdoc.
+--RULESET--
+{"phpdoc_to_comment": true, "phpdoc_no_useless_inheritdoc": true}
+--EXPECT--
+<?php
+
+class A {
+/*
+ * {@inheritdoc}
+ */
+}
+
+--INPUT--
+<?php
+
+class A {
+/**
+ * {@inheritdoc}
+ */
+}