Просмотр исходного кода

Add fix for phpunit class size annotation

Signed-off-by: Jefersson Nathan <malukenho.dev@gmail.com>
Jefersson Nathan 6 лет назад
Родитель
Сommit
5f017a81a5

+ 10 - 0
README.rst

@@ -1357,6 +1357,16 @@ Choose from the list of available rules:
 
   *Risky rule: this fixer may change functions named ``setUp()`` or ``tearDown()`` outside of PHPUnit tests, when a class is wrongly seen as a PHPUnit test.*
 
+* **php_unit_size_class**
+
+  All PHPUnit test cases should have ``@small``, ``@medium`` or ``@large``
+  annotation to enable run time limits.
+
+  Configuration options:
+
+  - ``group`` (``'large'``, ``'medium'``, ``'small'``): define a specific group to be used
+    in case no group is already in use; defaults to ``'small'``
+
 * **php_unit_strict** [@PhpCsFixer:risky]
 
   PHPUnit methods like ``assertSame`` should be used instead of

+ 279 - 0
src/Fixer/PhpUnit/PhpUnitSizeClassFixer.php

@@ -0,0 +1,279 @@
+<?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\PhpUnit;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\DocBlock\Annotation;
+use PhpCsFixer\DocBlock\DocBlock;
+use PhpCsFixer\DocBlock\Line;
+use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
+use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+use SplFileInfo;
+
+/**
+ * @author Jefersson Nathan <malukenho.dev@gmail.com>
+ */
+final class PhpUnitSizeClassFixer extends AbstractFixer implements WhitespacesAwareFixerInterface, ConfigurationDefinitionFixerInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'All PHPUnit test cases should have `@small`, `@medium` or `@large` annotation to enable run time limits.',
+            [new CodeSample("<?php\nclass MyTest extends TestCase {}\n")],
+            'The special groups [small, medium, large] provides a way to identify tests that are taking long to be executed.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isAllTokenKindsFound([T_CLASS, T_STRING]);
+    }
+
+    protected function applyFix(SplFileInfo $file, Tokens $tokens)
+    {
+        $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
+
+        foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens, true) as $indexes) {
+            $this->markClassSize($tokens, $indexes[0]);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function createConfigurationDefinition()
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('group', 'Define a specific group to be used in case no group is already in use'))
+                ->setAllowedValues(['small', 'medium', 'large'])
+                ->setDefault('small')
+                ->getOption(),
+        ]);
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $startIndex
+     */
+    private function markClassSize(Tokens $tokens, $startIndex)
+    {
+        $classIndex = $tokens->getPrevTokenOfKind($startIndex, [[T_CLASS]]);
+
+        if ($this->isAbstractClass($tokens, $classIndex)) {
+            return;
+        }
+
+        $docBlockIndex = $this->getDocBlockIndex($tokens, $classIndex);
+
+        if ($this->hasDocBlock($tokens, $classIndex)) {
+            $this->updateDocBlockIfNeeded($tokens, $docBlockIndex);
+
+            return;
+        }
+
+        $this->createDocBlock($tokens, $docBlockIndex);
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $i
+     *
+     * @return bool
+     */
+    private function isAbstractClass(Tokens $tokens, $i)
+    {
+        $typeIndex = $tokens->getPrevMeaningfulToken($i);
+
+        return $tokens[$typeIndex]->isGivenKind([T_ABSTRACT]);
+    }
+
+    private function createDocBlock(Tokens $tokens, $docBlockIndex)
+    {
+        $lineEnd = $this->whitespacesConfig->getLineEnding();
+        $originalIndent = $this->detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
+        $group = $this->configuration['group'];
+        $toInsert = [
+            new Token([T_DOC_COMMENT, '/**'.$lineEnd."${originalIndent} * @".$group.$lineEnd."${originalIndent} */"]),
+            new Token([T_WHITESPACE, $lineEnd.$originalIndent]),
+        ];
+        $index = $tokens->getNextMeaningfulToken($docBlockIndex);
+        $tokens->insertAt($index, $toInsert);
+    }
+
+    private function updateDocBlockIfNeeded(Tokens $tokens, $docBlockIndex)
+    {
+        $doc = new DocBlock($tokens[$docBlockIndex]->getContent());
+        if (!empty($this->filterDocBlock($doc))) {
+            return;
+        }
+        $doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex);
+        $lines = $this->addSizeAnnotation($doc, $tokens, $docBlockIndex);
+        $lines = implode('', $lines);
+
+        $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $index
+     *
+     * @return bool
+     */
+    private function hasDocBlock(Tokens $tokens, $index)
+    {
+        $docBlockIndex = $this->getDocBlockIndex($tokens, $index);
+
+        return $tokens[$docBlockIndex]->isGivenKind(T_DOC_COMMENT);
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $index
+     *
+     * @return int
+     */
+    private function getDocBlockIndex(Tokens $tokens, $index)
+    {
+        do {
+            $index = $tokens->getPrevNonWhitespace($index);
+        } while ($tokens[$index]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT]));
+
+        return $index;
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $index
+     *
+     * @return string
+     */
+    private function detectIndent(Tokens $tokens, $index)
+    {
+        if (!$tokens[$index - 1]->isWhitespace()) {
+            return ''; // cannot detect indent
+        }
+
+        $explodedContent = explode($this->whitespacesConfig->getLineEnding(), $tokens[$index - 1]->getContent());
+
+        return end($explodedContent);
+    }
+
+    /**
+     * @param DocBlock $docBlock
+     * @param Tokens   $tokens
+     * @param int      $docBlockIndex
+     *
+     * @return Line[]
+     */
+    private function addSizeAnnotation(DocBlock $docBlock, Tokens $tokens, $docBlockIndex)
+    {
+        $lines = $docBlock->getLines();
+        $originalIndent = $this->detectIndent($tokens, $docBlockIndex);
+        $lineEnd = $this->whitespacesConfig->getLineEnding();
+        $group = $this->configuration['group'];
+        array_splice($lines, -1, 0, $originalIndent.' *'.$lineEnd.$originalIndent.' * @'.$group.$lineEnd);
+
+        return $lines;
+    }
+
+    /**
+     * @param DocBlock $doc
+     * @param Tokens   $tokens
+     * @param int      $docBlockIndex
+     *
+     * @return DocBlock
+     */
+    private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, $docBlockIndex)
+    {
+        $lines = $doc->getLines();
+        if (1 === \count($lines) && empty($this->filterDocBlock($doc))) {
+            $lines = $this->splitUpDocBlock($lines, $tokens, $docBlockIndex);
+
+            return new DocBlock(implode('', $lines));
+        }
+
+        return $doc;
+    }
+
+    /**
+     * Take a one line doc block, and turn it into a multi line doc block.
+     *
+     * @param Line[] $lines
+     * @param Tokens $tokens
+     * @param int    $docBlockIndex
+     *
+     * @return Line[]
+     */
+    private function splitUpDocBlock($lines, Tokens $tokens, $docBlockIndex)
+    {
+        $lineContent = $this->getSingleLineDocBlockEntry($lines);
+        $lineEnd = $this->whitespacesConfig->getLineEnding();
+        $originalIndent = $this->detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
+
+        return [
+            new Line('/**'.$lineEnd),
+            new Line($originalIndent.' * '.$lineContent.$lineEnd),
+            new Line($originalIndent.' */'),
+        ];
+    }
+
+    /**
+     * @param Line|Line[]|string $line
+     *
+     * @return string
+     */
+    private function getSingleLineDocBlockEntry($line)
+    {
+        $line = $line[0];
+        $line = str_replace('*/', '', $line);
+        $line = trim($line);
+        $line = str_split($line);
+        $i = \count($line);
+        do {
+            --$i;
+        } while ('*' !== $line[$i] && '*' !== $line[$i - 1] && '/' !== $line[$i - 2]);
+        if (' ' === $line[$i]) {
+            ++$i;
+        }
+        $line = \array_slice($line, $i);
+
+        return implode('', $line);
+    }
+
+    /**
+     * @param DocBlock $doc
+     *
+     * @return Annotation[]
+     */
+    private function filterDocBlock(DocBlock $doc)
+    {
+        return array_filter([
+            $doc->getAnnotationsOfType('small'),
+            $doc->getAnnotationsOfType('large'),
+            $doc->getAnnotationsOfType('medium'),
+        ]);
+    }
+}

+ 32 - 0
tests/Fixer/PhpUnit/PhpUnitInternalClassFixerTest.php

@@ -162,6 +162,38 @@ if (class_exists("Foo\Bar")) {
     {
     }
 }
+',
+            ],
+            'It works for tab ident' => [
+                '<?php
+
+if (class_exists("Foo\Bar")) {
+	/**
+	 * @author me again
+	 *
+	 *
+	 * @covers \Other\Class
+	 *
+	 * @internal
+	 */
+	class Test Extends TestCase
+	{
+	}
+}
+',
+                '<?php
+
+if (class_exists("Foo\Bar")) {
+	/**
+	 * @author me again
+	 *
+	 *
+	 * @covers \Other\Class
+	 */
+	class Test Extends TestCase
+	{
+	}
+}
 ',
             ],
             'It always adds @internal to the bottom of the doc block' => [

+ 318 - 0
tests/Fixer/PhpUnit/PhpUnitSizeClassFixerTest.php

@@ -0,0 +1,318 @@
+<?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\PhpUnit;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @internal
+ *
+ * @author Jefersson Nathan <malukenho.dev@gmail.com>
+ *
+ * @covers \PhpCsFixer\Fixer\PhpUnit\PhpUnitSizeClassFixer
+ */
+final class PhpUnitSizeClassFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @dataProvider provideFixCases
+     *
+     * @param string     $expected
+     * @param null|mixed $input
+     * @param array      $config
+     */
+    public function testFix($expected, $input = null, $config = [])
+    {
+        $this->fixer->configure($config);
+        $this->doTest($expected, $input);
+    }
+
+    public function provideFixCases()
+    {
+        return [
+            'It does not change normal classes' => [
+                '<?php
+
+class Hello
+{
+}
+',
+            ],
+            'It marks a test class as @small by default' => [
+                '<?php
+
+/**
+ * @small
+ */
+class Test extends TestCase
+{
+}
+',
+                '<?php
+
+class Test extends TestCase
+{
+}
+',
+            ],
+            'It marks a test class as specified in the configuration' => [
+                '<?php
+
+/**
+ * @large
+ */
+class Test extends TestCase
+{
+}
+',
+                '<?php
+
+class Test extends TestCase
+{
+}
+',
+                ['group' => 'large'],
+            ],
+            'It adds an @small tag to a class that already has a doc block' => [
+                '<?php
+
+/**
+ * @coversNothing
+ *
+ * @small
+ */
+class Test extends TestCase
+{
+}
+',
+                '<?php
+
+/**
+ * @coversNothing
+ */
+class Test extends TestCase
+{
+}
+',
+            ],
+            'It does not change a class that is already @small' => [
+                '<?php
+
+/**
+ * @small
+ */
+class Test extends TestCase
+{
+}
+',
+            ],
+            'It does not change a class that is already @small and has other annotations' => [
+                '<?php
+
+/**
+ * @author malukenho
+ * @coversNothing
+ * @large
+ * @group large
+ */
+class Test extends TestCase
+{
+}
+',
+            ],
+            'It works on other indentation levels' => [
+                '<?php
+
+if (class_exists("Foo\Bar")) {
+    /**
+     * @small
+     */
+    class Test Extends TestCase
+    {
+    }
+}
+',
+                '<?php
+
+if (class_exists("Foo\Bar")) {
+    class Test Extends TestCase
+    {
+    }
+}
+',
+            ],
+            'It works on other indentation levels when the class has other annotations' => [
+                '<?php
+
+if (class_exists("Foo\Bar")) {
+    /**
+     * @author malukenho again
+     *
+     *
+     * @covers \Other\Class
+     *
+     * @small
+     */
+    class Test Extends TestCase
+    {
+    }
+}
+',
+                '<?php
+
+if (class_exists("Foo\Bar")) {
+    /**
+     * @author malukenho again
+     *
+     *
+     * @covers \Other\Class
+     */
+    class Test Extends TestCase
+    {
+    }
+}
+',
+            ],
+            'It always adds @small to the bottom of the doc block' => [
+                '<?php
+
+/**
+ * @coversNothing
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @small
+ */
+class Test extends TestCase
+{
+}
+',
+                '<?php
+
+/**
+ * @coversNothing
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+class Test extends TestCase
+{
+}
+',
+            ],
+            'It does not change a class with a single line @{size} doc block' => [
+                '<?php
+
+/** @medium */
+class Test extends TestCase
+{
+}
+',
+            ],
+            'It adds an @small tag to a class that already has a one linedoc block' => [
+                '<?php
+
+/**
+ * @coversNothing
+ *
+ * @small
+ */
+class Test extends TestCase
+{
+}
+',
+                '<?php
+
+/** @coversNothing */
+class Test extends TestCase
+{
+}
+',
+            ],
+            'By default it will not mark an abstract class as @small' => [
+                '<?php
+
+abstract class Test
+{
+}
+',
+            ],
+            'It works correctly with multiple classes in one file, even when one of them is not allowed' => [
+                '<?php
+
+/**
+ * @small
+ */
+class Test
+{
+}
+
+abstract class Test
+{
+}
+
+class FooBar
+{
+}
+
+/**
+ * @small
+ */
+class Test extends TestCase
+{
+}
+',
+                '<?php
+
+class Test
+{
+}
+
+abstract class Test
+{
+}
+
+class FooBar
+{
+}
+
+class Test extends TestCase
+{
+}
+',
+            ],
+        ];
+    }
+}