Browse Source

Add PHPUnit Migration rulesets and fixers

Dariusz Ruminski 7 years ago
parent
commit
cc1749c0b5

+ 1 - 2
.php_cs.dist

@@ -14,6 +14,7 @@ $config = PhpCsFixer\Config::create()
     ->setRiskyAllowed(true)
     ->setRules([
         '@PHP56Migration' => true,
+        '@PHPUnit60Migration:risky' => true,
         '@Symfony' => true,
         '@Symfony:risky' => true,
         'align_multiline_comment' => true,
@@ -22,8 +23,6 @@ $config = PhpCsFixer\Config::create()
         'combine_consecutive_issets' => true,
         'combine_consecutive_unsets' => true,
         'compact_nullable_typehint' => true,
-        // one should use PHPUnit methods to set up expected exception instead of annotations
-        'general_phpdoc_annotation_remove' => ['annotations' => ['expectedException', 'expectedExceptionMessage', 'expectedExceptionMessageRegExp']],
         'header_comment' => ['header' => $header],
         'heredoc_to_nowdoc' => true,
         'list_syntax' => ['syntax' => 'long'],

+ 50 - 6
README.rst

@@ -969,7 +969,7 @@ Choose from the list of available rules:
   - ``assertions`` (``array``): list of assertion methods to fix; defaults to
     ``['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']``
 
-* **php_unit_dedicate_assert** [@Symfony:risky]
+* **php_unit_dedicate_assert** [@Symfony:risky, @PHPUnit30Migration:risky, @PHPUnit32Migration:risky, @PHPUnit35Migration:risky, @PHPUnit43Migration:risky, @PHPUnit48Migration:risky, @PHPUnit50Migration:risky, @PHPUnit52Migration:risky, @PHPUnit54Migration:risky, @PHPUnit56Migration:risky, @PHPUnit57Migration:risky, @PHPUnit60Migration:risky]
 
   PHPUnit assertions like "assertInternalType", "assertFileExists", should
   be used over "assertTrue".
@@ -978,16 +978,60 @@ Choose from the list of available rules:
 
   Configuration options:
 
-  - ``functions`` (``array``): list of assertions to fix; defaults to
-    ``['array_key_exists', 'empty', 'file_exists', 'is_array', 'is_bool',
-    'is_boolean', 'is_callable', 'is_double', 'is_float', 'is_infinite',
-    'is_int', 'is_integer', 'is_long', 'is_nan', 'is_null', 'is_numeric',
-    'is_object', 'is_real', 'is_resource', 'is_scalar', 'is_string']``
+  - ``functions`` (``null``): (deprecated, use ``target`` instead) List of assertions
+    to fix (overrides ``target``); defaults to ``null``
+  - ``target`` (``'3.0'``, ``'3.5'``, ``'5.0'``, ``'5.6'``, ``'newest'``): target version of
+    PHPUnit; defaults to ``'5.0'``
+
+* **php_unit_expectation** [@PHPUnit52Migration:risky, @PHPUnit54Migration:risky, @PHPUnit56Migration:risky, @PHPUnit57Migration:risky, @PHPUnit60Migration:risky]
+
+  Usages of ``->setExpectedException*`` methods MUST be replaced by
+  ``->expectException*`` methods.
+
+  *Risky rule: risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.*
+
+  Configuration options:
+
+  - ``target`` (``'5.2'``, ``'5.6'``, ``'newest'``): target version of PHPUnit; defaults to
+    ``'newest'``
 
 * **php_unit_fqcn_annotation** [@Symfony]
 
   PHPUnit annotations should be a FQCNs including a root namespace.
 
+* **php_unit_mock** [@PHPUnit54Migration:risky, @PHPUnit56Migration:risky, @PHPUnit57Migration:risky, @PHPUnit60Migration:risky]
+
+  Usages of ``->getMock`` and
+  ``->getMockWithoutInvokingTheOriginalConstructor`` methods MUST be
+  replaced by ``->createMock`` method.
+
+  *Risky rule: risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.*
+
+* **php_unit_namespaced** [@PHPUnit48Migration:risky, @PHPUnit50Migration:risky, @PHPUnit52Migration:risky, @PHPUnit54Migration:risky, @PHPUnit56Migration:risky, @PHPUnit57Migration:risky, @PHPUnit60Migration:risky]
+
+  PHPUnit classes MUST be used in namespaced version, eg
+  ``\PHPUnit\Framework\TestCase`` instead of ``\PHPUnit_Framework_TestCase``.
+
+  *Risky rule: risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.*
+
+  Configuration options:
+
+  - ``target`` (``'4.8'``, ``'5.7'``, ``'6.0'``, ``'newest'``): target version of PHPUnit;
+    defaults to ``'newest'``
+
+* **php_unit_no_expectation_annotation** [@PHPUnit32Migration:risky, @PHPUnit35Migration:risky, @PHPUnit43Migration:risky, @PHPUnit48Migration:risky, @PHPUnit50Migration:risky, @PHPUnit52Migration:risky, @PHPUnit54Migration:risky, @PHPUnit56Migration:risky, @PHPUnit57Migration:risky, @PHPUnit60Migration:risky]
+
+  Usages of ``@expectedException*`` annotations MUST be replaced by
+  ``->setExpectedException*`` methods.
+
+  *Risky rule: risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.*
+
+  Configuration options:
+
+  - ``target`` (``'3.2'``, ``'4.3'``, ``'newest'``): target version of PHPUnit; defaults to
+    ``'newest'``
+  - ``use_class_const`` (``bool``): use ::class notation; defaults to ``true``
+
 * **php_unit_strict**
 
   PHPUnit methods like ``assertSame`` should be used instead of

+ 90 - 7
src/Fixer/PhpUnit/PhpUnitDedicateAssertFixer.php

@@ -24,6 +24,7 @@ use PhpCsFixer\Tokenizer\Tokens;
 
 /**
  * @author SpacePossum
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  */
 final class PhpUnitDedicateAssertFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
 {
@@ -35,6 +36,7 @@ final class PhpUnitDedicateAssertFixer extends AbstractFixer implements Configur
         'is_bool' => true,
         'is_boolean' => true,
         'is_callable' => true,
+        'is_dir' => ['assertDirectoryNotExists', 'assertDirectoryExists'],
         'is_double' => true,
         'is_float' => true,
         'is_infinite' => ['assertFinite', 'assertInfinite'],
@@ -45,12 +47,80 @@ final class PhpUnitDedicateAssertFixer extends AbstractFixer implements Configur
         'is_null' => ['assertNotNull', 'assertNull'],
         'is_numeric' => true,
         'is_object' => true,
+        'is_readable' => ['assertNotIsReadable', 'assertIsReadable'],
         'is_real' => true,
         'is_resource' => true,
         'is_scalar' => true,
         'is_string' => true,
+        'is_writable' => ['assertNotIsWritable', 'assertIsWritable'],
     ];
 
+    /**
+     * @var string[]
+     */
+    private $functions = [];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function configure(array $configuration = null)
+    {
+        parent::configure($configuration);
+
+        if (isset($this->configuration['functions'])) {
+            @trigger_error('Option "functions" is deprecated and will be removed in 3.0, use option "target" instead.', E_USER_DEPRECATED);
+            $this->functions = $this->configuration['functions'];
+
+            return;
+        }
+
+        // assertions added in 3.0: assertArrayNotHasKey assertArrayHasKey assertFileNotExists assertFileExists assertNotNull, assertNull
+        $this->functions = [
+            'array_key_exists',
+            'file_exists',
+            'is_null',
+        ];
+
+        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_3_5)) {
+            // assertions added in 3.5: assertInternalType assertNotEmpty assertEmpty
+            $this->functions = array_merge($this->functions, [
+                'empty',
+                'is_array',
+                'is_bool',
+                'is_boolean',
+                'is_callable',
+                'is_double',
+                'is_float',
+                'is_int',
+                'is_integer',
+                'is_long',
+                'is_numeric',
+                'is_object',
+                'is_real',
+                'is_resource',
+                'is_scalar',
+                'is_string',
+            ]);
+        }
+
+        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_0)) {
+            // assertions added in 5.0: assertFinite assertInfinite assertNan
+            $this->functions = array_merge($this->functions, [
+                'is_infinite',
+                'is_nan',
+            ]);
+        }
+
+        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_6)) {
+            // assertions added in 5.6: assertDirectoryExists assertDirectoryNotExists assertIsReadable assertNotIsReadable assertIsWritable assertNotIsWritable
+            $this->functions = array_merge($this->functions, [
+                'is_dir',
+                'is_readable',
+                'is_writable',
+            ]);
+        }
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -83,10 +153,11 @@ $this->assertTrue(is_nan($a));
                 ),
                 new CodeSample(
                     '<?php
-$this->assertTrue(is_float( $a), "my message");
-$this->assertTrue(is_nan($a));
+$this->assertTrue(is_dir($a));
+$this->assertTrue(is_writable($a));
+$this->assertTrue(is_readable($a));
 ',
-                    ['functions' => ['is_nan']]
+                    ['target' => PhpUnitTargetVersion::VERSION_5_6]
                 ),
             ],
             null,
@@ -160,12 +231,24 @@ $this->assertTrue(is_nan($a));
         sort($values);
 
         return new FixerConfigurationResolverRootless('functions', [
-            (new FixerOptionBuilder('functions', 'List of assertions to fix.'))
-                ->setAllowedTypes(['array'])
+            (new FixerOptionBuilder('functions', '(deprecated, use `target` instead) List of assertions to fix (overrides `target`).'))
+                ->setAllowedTypes(['null', 'array'])
                 ->setAllowedValues([
+                    null,
                     (new FixerOptionValidatorGenerator())->allowedValueIsSubsetOf($values),
                 ])
-                ->setDefault($values)
+                ->setDefault(null)
+                ->getOption(),
+            (new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
+                ->setAllowedTypes(['string'])
+                ->setAllowedValues([
+                    PhpUnitTargetVersion::VERSION_3_0,
+                    PhpUnitTargetVersion::VERSION_3_5,
+                    PhpUnitTargetVersion::VERSION_5_0,
+                    PhpUnitTargetVersion::VERSION_5_6,
+                    PhpUnitTargetVersion::VERSION_NEWEST,
+                ])
+                ->setDefault(PhpUnitTargetVersion::VERSION_5_0) // @TODO 3.x: change to `VERSION_NEWEST`
                 ->getOption(),
         ]);
     }
@@ -249,7 +332,7 @@ $this->assertTrue(is_nan($a));
         ) = $assertIndexes;
 
         $content = strtolower($tokens[$testIndex]->getContent());
-        if (!in_array($content, $this->configuration['functions'], true)) {
+        if (!in_array($content, $this->functions, true)) {
             return $assertCallCloseIndex;
         }
 

+ 247 - 0
src/Fixer/PhpUnit/PhpUnitExpectationFixer.php

@@ -0,0 +1,247 @@
+<?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\AbstractFunctionReferenceFixer;
+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\Analyzer\ArgumentsAnalyzer;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ */
+final class PhpUnitExpectationFixer extends AbstractFunctionReferenceFixer implements ConfigurationDefinitionFixerInterface, WhitespacesAwareFixerInterface
+{
+    /**
+     * @var array<string, string>
+     */
+    private $methodMap = [];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function configure(array $configuration = null)
+    {
+        parent::configure($configuration);
+
+        $this->methodMap = [
+            'setExpectedException' => 'expectExceptionMessage',
+        ];
+
+        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_6)) {
+            $this->methodMap['setExpectedExceptionRegExp'] = 'expectExceptionMessageRegExp';
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Usages of `->setExpectedException*` methods MUST be replaced by `->expectException*` methods.',
+            [
+                new CodeSample(
+                    '<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+    public function testFoo()
+    {
+        $this->setExpectedException("RuntimeException", "Msg", 123);
+        foo();
+    }
+
+    public function testBar()
+    {
+        $this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
+        bar();
+    }
+}
+'
+                ),
+                new CodeSample(
+                    '<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+    public function testFoo()
+    {
+        $this->setExpectedException("RuntimeException", null, 123);
+        foo();
+    }
+
+    public function testBar()
+    {
+        $this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
+        bar();
+    }
+}
+',
+                    ['target' => PhpUnitTargetVersion::VERSION_5_6]
+                ),
+                new CodeSample(
+                    '<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+    public function testFoo()
+    {
+        $this->setExpectedException("RuntimeException", "Msg", 123);
+        foo();
+    }
+
+    public function testBar()
+    {
+        $this->setExpectedExceptionRegExp("RuntimeException", "/Msg.*/", 123);
+        bar();
+    }
+}
+',
+                    ['target' => PhpUnitTargetVersion::VERSION_5_2]
+                ),
+            ],
+            null,
+            'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound(T_CLASS);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        $argumentsAnalyzer = new ArgumentsAnalyzer();
+        $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
+
+        $oldMethodSequence = [
+            new Token([T_VARIABLE, '$this']),
+            new Token([T_OBJECT_OPERATOR, '->']),
+            [T_STRING],
+        ];
+
+        $inPhpUnitClass = false;
+
+        for ($index = 0, $limit = $tokens->count() - 1; $index < $limit; ++$index) {
+            if (!$inPhpUnitClass && $tokens[$index]->isGivenKind(T_CLASS) && $phpUnitTestCaseIndicator->isPhpUnitClass($tokens, $index)) {
+                $inPhpUnitClass = true;
+            }
+
+            if (!$inPhpUnitClass) {
+                continue;
+            }
+
+            $match = $tokens->findSequence($oldMethodSequence, $index);
+
+            if (null === $match) {
+                return;
+            }
+
+            list($thisIndex, , $index) = array_keys($match);
+
+            if (!isset($this->methodMap[$tokens[$index]->getContent()])) {
+                continue;
+            }
+
+            $openIndex = $tokens->getNextTokenOfKind($index, ['(']);
+            $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
+
+            $arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
+            $argumentsCnt = count($arguments);
+
+            $argumentsReplacements = ['expectException', $this->methodMap[$tokens[$index]->getContent()], 'expectExceptionCode'];
+
+            $indent = $this->whitespacesConfig->getLineEnding().$this->detectIndent($tokens, $thisIndex);
+
+            $isMultilineWhitespace = false;
+
+            for ($cnt = $argumentsCnt - 1; $cnt >= 1; --$cnt) {
+                $argStart = array_keys($arguments)[$cnt];
+                $argBefore = $tokens->getPrevMeaningfulToken($argStart);
+                $isMultilineWhitespace = $isMultilineWhitespace || ($tokens[$argStart]->isWhitespace() && !$tokens[$argStart]->isWhitespace(" \t"));
+
+                $tokensOverrideArgStart = [
+                    new Token([T_WHITESPACE, $indent]),
+                    new Token([T_VARIABLE, '$this']),
+                    new Token([T_OBJECT_OPERATOR, '->']),
+                    new Token([T_STRING, $argumentsReplacements[$cnt]]),
+                    new Token('('),
+                ];
+                $tokensOverrideArgBefore = [
+                    new Token(')'),
+                    new Token(';'),
+                ];
+
+                if ($isMultilineWhitespace) {
+                    array_push($tokensOverrideArgStart, new Token([T_WHITESPACE, $indent.$this->whitespacesConfig->getIndent()]));
+                    array_unshift($tokensOverrideArgBefore, new Token([T_WHITESPACE, $indent]));
+                }
+
+                if ($tokens[$argStart]->isWhitespace()) {
+                    $tokens->overrideRange($argStart, $argStart, $tokensOverrideArgStart);
+                } else {
+                    $tokens->insertAt($argStart, $tokensOverrideArgStart);
+                }
+
+                $tokens->overrideRange($argBefore, $argBefore, $tokensOverrideArgBefore);
+
+                $limit = $tokens->count();
+            }
+
+            $tokens[$index] = new Token([T_STRING, 'expectException']);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function createConfigurationDefinition()
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
+                ->setAllowedTypes(['string'])
+                ->setAllowedValues([PhpUnitTargetVersion::VERSION_5_2, PhpUnitTargetVersion::VERSION_5_6, PhpUnitTargetVersion::VERSION_NEWEST])
+                ->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
+                ->getOption(),
+        ]);
+    }
+
+    /**
+     * @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("\n", $tokens[$index - 1]->getContent());
+
+        return end($explodedContent);
+    }
+}

+ 120 - 0
src/Fixer/PhpUnit/PhpUnitMockFixer.php

@@ -0,0 +1,120 @@
+<?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\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
+use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ */
+final class PhpUnitMockFixer extends AbstractFixer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Usages of `->getMock` and `->getMockWithoutInvokingTheOriginalConstructor` methods MUST be replaced by `->createMock` method.',
+            [
+                new CodeSample(
+                    '<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+    public function testFoo()
+    {
+        $mock = $this->getMockWithoutInvokingTheOriginalConstructor("Foo");
+    }
+}
+'
+                ),
+                new CodeSample(
+                    '<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+    public function testFoo()
+    {
+        $mock1 = $this->getMock("Foo");
+        $mock1 = $this->getMock("Bar", ["aaa"]); // version with multiple params is not supported
+    }
+}
+'
+                ),
+            ],
+            null,
+            'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound(T_CLASS);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isRisky()
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
+        $argumentsAnalyzer = new ArgumentsAnalyzer();
+
+        $inPhpUnitClass = false;
+
+        for ($index = 0, $limit = $tokens->count() - 1; $index < $limit; ++$index) {
+            if (!$inPhpUnitClass && $tokens[$index]->isGivenKind(T_CLASS) && $phpUnitTestCaseIndicator->isPhpUnitClass($tokens, $index)) {
+                $inPhpUnitClass = true;
+            }
+
+            if (!$inPhpUnitClass) {
+                continue;
+            }
+
+            if (!$tokens[$index]->isGivenKind(T_OBJECT_OPERATOR)) {
+                continue;
+            }
+
+            $index = $tokens->getNextMeaningfulToken($index);
+
+            if ($tokens[$index]->equals([T_STRING, 'getMockWithoutInvokingTheOriginalConstructor'], false)) {
+                $tokens[$index] = new Token([T_STRING, 'createMock']);
+            } elseif ($tokens[$index]->equals([T_STRING, 'getMock'], false)) {
+                $openingParenthesis = $tokens->getNextMeaningfulToken($index);
+                $closingParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openingParenthesis);
+
+                $argumentsCount = $argumentsAnalyzer->countArguments($tokens, $openingParenthesis, $closingParenthesis);
+
+                if (1 === $argumentsCount) {
+                    $tokens[$index] = new Token([T_STRING, 'createMock']);
+                }
+            }
+        }
+    }
+}

+ 167 - 0
src/Fixer/PhpUnit/PhpUnitNamespacedFixer.php

@@ -0,0 +1,167 @@
+<?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\Fixer\ConfigurationDefinitionFixerInterface;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ */
+final class PhpUnitNamespacedFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
+{
+    /**
+     * @var string
+     */
+    private $originalClassRegEx;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'PHPUnit classes MUST be used in namespaced version, eg `\PHPUnit\Framework\TestCase` instead of `\PHPUnit_Framework_TestCase`.',
+            [
+                new CodeSample(
+'<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+}
+'
+                ),
+            ],
+            "PHPUnit v6 has finally fully switched to namespaces.\n"
+            ."You could start preparing the upgrade by switching from non-namespaced TestCase to namespaced one.\n"
+            .'Forward compatibility layer (`\PHPUnit\Framework\TestCase` class) was backported to PHPUnit v4.8.35 and PHPUnit v5.4.0.'."\n"
+            .'Extended forward compatibility layer (`PHPUnit\Framework\Assert`, `PHPUnit\Framework\BaseTestListener`, `PHPUnit\Framework\TestListener` classes) was introduced in v5.7.0.'."\n",
+            'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound(T_CLASS);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isRisky()
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function configure(array $configuration = null)
+    {
+        parent::configure($configuration);
+
+        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_6_0)) {
+            $this->originalClassRegEx = '/^PHPUnit_\w+$/i';
+        } elseif (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_7)) {
+            $this->originalClassRegEx = '/^PHPUnit_Framework_TestCase|PHPUnit_Framework_Assert|PHPUnit_Framework_BaseTestListener|PHPUnit_Framework_TestListener$/i';
+        } else {
+            $this->originalClassRegEx = '/^PHPUnit_Framework_TestCase$/i';
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        $importedOriginalClassesMap = [];
+        $currIndex = 0;
+
+        while (null !== $currIndex) {
+            $match = $tokens->findSequence([[T_STRING]], $currIndex);
+
+            if (null === $match) {
+                break;
+            }
+
+            $matchIndexes = array_keys($match);
+            $currIndex = $matchIndexes[0];
+
+            $originalClass = $match[$currIndex]->getContent();
+
+            if (1 !== preg_match($this->originalClassRegEx, $originalClass)) {
+                ++$currIndex;
+
+                continue;
+            }
+
+            $substituteTokens = $this->generateReplacement($originalClass);
+
+            $tokens->clearAt($currIndex);
+            $tokens->insertAt(
+                $currIndex,
+                isset($importedOriginalClassesMap[$originalClass]) ? $substituteTokens[$substituteTokens->getSize() - 1] : $substituteTokens
+            );
+
+            $prevIndex = $tokens->getPrevMeaningfulToken($currIndex);
+            if ($tokens[$prevIndex]->isGivenKind(T_USE)) {
+                $importedOriginalClassesMap[$originalClass] = true;
+            } elseif ($tokens[$prevIndex]->isGivenKind(T_NS_SEPARATOR)) {
+                $prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
+                $importedOriginalClassesMap[$originalClass] = $tokens[$prevIndex]->isGivenKind(T_USE);
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function createConfigurationDefinition()
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
+                ->setAllowedTypes(['string'])
+                ->setAllowedValues([PhpUnitTargetVersion::VERSION_4_8, PhpUnitTargetVersion::VERSION_5_7, PhpUnitTargetVersion::VERSION_6_0, PhpUnitTargetVersion::VERSION_NEWEST])
+                ->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
+                ->getOption(),
+        ]);
+    }
+
+    /**
+     * @param string $originalClassName
+     *
+     * @return Tokens
+     */
+    private function generateReplacement($originalClassName)
+    {
+        $parts = explode('_', $originalClassName);
+
+        $tokensArray = [];
+        while (!empty($parts)) {
+            $tokensArray[] = new Token([T_STRING, array_shift($parts)]);
+            if (!empty($parts)) {
+                $tokensArray[] = new Token([T_NS_SEPARATOR, '\\']);
+            }
+        }
+
+        return Tokens::fromArray($tokensArray);
+    }
+}

+ 311 - 0
src/Fixer/PhpUnit/PhpUnitNoExpectationAnnotationFixer.php

@@ -0,0 +1,311 @@
+<?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\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 PhpCsFixer\Tokenizer\TokensAnalyzer;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ */
+final class PhpUnitNoExpectationAnnotationFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface, WhitespacesAwareFixerInterface
+{
+    /**
+     * @var bool
+     */
+    private $fixMessageRegExp;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function configure(array $configuration = null)
+    {
+        parent::configure($configuration);
+
+        $this->fixMessageRegExp = PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_4_3);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Usages of `@expectedException*` annotations MUST be replaced by `->setExpectedException*` methods.',
+            [
+                new CodeSample(
+                    '<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @expectedException FooException
+     * @expectedExceptionMessageRegExp /foo.*$/
+     * @expectedExceptionCode 123
+     */
+    function testAaa()
+    {
+        aaa();
+    }
+}
+'
+                ),
+                new CodeSample(
+                    '<?php
+final class MyTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @expectedException FooException
+     * @expectedExceptionCode 123
+     */
+    function testBbb()
+    {
+        bbb();
+    }
+
+    /**
+     * @expectedException FooException
+     * @expectedExceptionMessageRegExp /foo.*$/
+     */
+    function testCcc()
+    {
+        ccc();
+    }
+}
+',
+                    ['target' => PhpUnitTargetVersion::VERSION_3_2]
+                ),
+            ],
+            null,
+            'Risky when PHPUnit classes are overridden or not accessible, or when project has PHPUnit incompatibilities.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority()
+    {
+        // should be run before the PhpUnitExpectationFixer, NoEmptyPhpdocFixer
+        return 10;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isAllTokenKindsFound([T_CLASS, T_DOC_COMMENT]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isRisky()
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        foreach (array_reverse($this->findPhpUnitClasses($tokens)) as $indexes) {
+            $this->fixPhpUnitClass($tokens, $indexes[0], $indexes[1]);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function createConfigurationDefinition()
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
+                ->setAllowedTypes(['string'])
+                ->setAllowedValues([PhpUnitTargetVersion::VERSION_3_2, PhpUnitTargetVersion::VERSION_4_3, PhpUnitTargetVersion::VERSION_NEWEST])
+                ->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
+                ->getOption(),
+            (new FixerOptionBuilder('use_class_const', 'Use ::class notation.'))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(true)
+                ->getOption(),
+        ]);
+    }
+
+    /**
+     * @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("\n", $tokens[$index - 1]->getContent());
+
+        return end($explodedContent);
+    }
+
+    /**
+     * @param Tokens $tokens
+     *
+     * @return int[][] array of [start, end] indexes from sooner to later classes
+     */
+    private function findPhpUnitClasses(Tokens $tokens)
+    {
+        $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
+        $phpunitClasses = [];
+
+        for ($index = 0, $limit = $tokens->count() - 1; $index < $limit; ++$index) {
+            if ($tokens[$index]->isGivenKind(T_CLASS) && $phpUnitTestCaseIndicator->isPhpUnitClass($tokens, $index)) {
+                $index = $tokens->getNextTokenOfKind($index, ['{']);
+                $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
+                $phpunitClasses[] = [$index, $endIndex];
+                $index = $endIndex;
+            }
+        }
+
+        return $phpunitClasses;
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $startIndex
+     * @param int    $endIndex
+     */
+    private function fixPhpUnitClass(Tokens $tokens, $startIndex, $endIndex)
+    {
+        $tokensAnalyzer = new TokensAnalyzer($tokens);
+
+        for ($i = $endIndex - 1; $i > $startIndex; --$i) {
+            if (!$tokens[$i]->isGivenKind(T_FUNCTION) || $tokensAnalyzer->isLambda($i)) {
+                continue;
+            }
+
+            $functionIndex = $i;
+            $docBlockIndex = $i;
+
+            // ignore abstract functions
+            $braceIndex = $tokens->getNextTokenOfKind($functionIndex, [';', '{']);
+            if (!$tokens[$braceIndex]->equals('{')) {
+                continue;
+            }
+
+            do {
+                $docBlockIndex = $tokens->getPrevNonWhitespace($docBlockIndex);
+            } while ($tokens[$docBlockIndex]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT]));
+
+            if (!$tokens[$docBlockIndex]->isGivenKind(T_DOC_COMMENT)) {
+                continue;
+            }
+
+            $doc = new DocBlock($tokens[$docBlockIndex]->getContent());
+            $annotations = [];
+
+            foreach ($doc->getAnnotationsOfType([
+                'expectedException',
+                'expectedExceptionCode',
+                'expectedExceptionMessage',
+                'expectedExceptionMessageRegExp',
+            ]) as $annotation) {
+                $tag = $annotation->getTag()->getName();
+                $content = $this->extractContentFromAnnotation($annotation);
+                $annotations[$tag] = $content;
+                $annotation->remove();
+            }
+
+            if (!isset($annotations['expectedException'])) {
+                continue;
+            }
+            if (!$this->fixMessageRegExp && isset($annotations['expectedExceptionMessageRegExp'])) {
+                continue;
+            }
+
+            $originalIndent = $this->detectIndent($tokens, $docBlockIndex);
+
+            $paramList = $this->annotationsToParamList($annotations);
+
+            $newMethodsCode = '<?php $this->'
+                .(isset($annotations['expectedExceptionMessageRegExp']) ? 'setExpectedExceptionRegExp' : 'setExpectedException')
+                .'('
+                .implode($paramList, ', ')
+                .');';
+            $newMethods = Tokens::fromCode($newMethodsCode);
+            $newMethods[0] = new Token([
+                T_WHITESPACE,
+                $this->whitespacesConfig->getLineEnding().$originalIndent.$this->whitespacesConfig->getIndent(),
+            ]);
+
+            // apply changes
+            $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $doc->getContent()]);
+            $tokens->insertAt($braceIndex + 1, $newMethods);
+
+            $i = $docBlockIndex;
+        }
+    }
+
+    /**
+     * @param Annotation $annotation
+     *
+     * @return string
+     */
+    private function extractContentFromAnnotation(Annotation $annotation)
+    {
+        $tag = $annotation->getTag()->getName();
+
+        preg_match('/^\s*\*\s*@'.$tag.'\s+(.+)$/', $annotation->getContent(), $matches);
+
+        return rtrim($matches[1]);
+    }
+
+    private function annotationsToParamList(array $annotations)
+    {
+        $params = [];
+        $exceptionClass = '\\'.ltrim($annotations['expectedException'], '\\');
+
+        if ($this->configuration['use_class_const']) {
+            $params[] = $exceptionClass.'::class';
+        } else {
+            $params[] = "'$exceptionClass'";
+        }
+
+        if (isset($annotations['expectedExceptionMessage'])) {
+            $params[] = "'{$annotations['expectedExceptionMessage']}'";
+        } elseif (isset($annotations['expectedExceptionMessageRegExp'])) {
+            $params[] = "'{$annotations['expectedExceptionMessageRegExp']}'";
+        } elseif (isset($annotations['expectedExceptionCode'])) {
+            $params[] = 'null';
+        }
+
+        if (isset($annotations['expectedExceptionCode'])) {
+            $params[] = $annotations['expectedExceptionCode'];
+        }
+
+        return $params;
+    }
+}

+ 59 - 0
src/Fixer/PhpUnit/PhpUnitTargetVersion.php

@@ -0,0 +1,59 @@
+<?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 Composer\Semver\Comparator;
+
+/**
+ * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * @internal
+ */
+final class PhpUnitTargetVersion
+{
+    const VERSION_3_0 = '3.0';
+    const VERSION_3_2 = '3.2';
+    const VERSION_3_5 = '3.5';
+    const VERSION_4_3 = '4.3';
+    const VERSION_4_8 = '4.8';
+    const VERSION_5_0 = '5.0';
+    const VERSION_5_2 = '5.2';
+    const VERSION_5_4 = '5.4';
+    const VERSION_5_6 = '5.6';
+    const VERSION_5_7 = '5.7';
+    const VERSION_6_0 = '6.0';
+    const VERSION_NEWEST = 'newest';
+
+    private function __construct()
+    {
+    }
+
+    /**
+     * @param string $candidate
+     * @param string $target
+     *
+     * @return bool
+     */
+    public static function fulfills($candidate, $target)
+    {
+        if (self::VERSION_NEWEST === $target) {
+            throw new \LogicException(sprintf('Parameter `target` shall not be provided as `%s`, determine proper target for tested PHPUnit feature instead.', self::VERSION_NEWEST));
+        }
+
+        if (self::VERSION_NEWEST === $candidate) {
+            return true;
+        }
+
+        return Comparator::greaterThanOrEqualTo($candidate, $target);
+    }
+}

+ 10 - 3
src/Fixer/PhpUnit/PhpUnitTestClassRequiresCoversFixer.php

@@ -18,7 +18,7 @@ use PhpCsFixer\DocBlock\Line;
 use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
 use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
-use PhpCsFixer\Indicator\PhpUnitIndicator;
+use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 
@@ -63,14 +63,21 @@ final class MyTest extends \PHPUnit_Framework_TestCase
      */
     protected function applyFix(\SplFileInfo $file, Tokens $tokens)
     {
-        $phpUnitIndicator = new PhpUnitIndicator();
+        $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
 
         for ($index = $tokens->count() - 1; $index >= 0; --$index) {
             if (!$tokens[$index]->isGivenKind(T_CLASS)) {
                 continue;
             }
 
-            if (!$phpUnitIndicator->isPhpUnitClass($tokens, $index)) {
+            $prevIndex = $tokens->getPrevMeaningfulToken($index);
+
+            // don't add `@covers` annotation for abstract base classes
+            if ($tokens[$prevIndex]->isGivenKind(T_ABSTRACT)) {
+                continue;
+            }
+
+            if (!$phpUnitTestCaseIndicator->isPhpUnitClass($tokens, $index)) {
                 continue;
             }
 

+ 22 - 11
src/FixerConfiguration/FixerConfigurationResolverRootless.php

@@ -65,19 +65,30 @@ final class FixerConfigurationResolverRootless implements FixerConfigurationReso
     public function resolve(array $options)
     {
         if (!empty($options) && !array_key_exists($this->root, $options)) {
-            if (getenv('PHP_CS_FIXER_FUTURE_MODE')) {
-                throw new \RuntimeException(sprintf(
-                    'Passing "%1$s" at the root of the configuration is deprecated and will not be supported in 3.0, use "%1$s" => array(...) option instead.  This check was performed as `PHP_CS_FIXER_FUTURE_MODE` env var is set.',
-                    $this->root
-                ));
-            }
+            $names = array_map(
+                function (FixerOptionInterface $option) {
+                    return $option->getName();
+                },
+                $this->resolver->getOptions()
+            );
+
+            $passedNames = array_keys($options);
 
-            @trigger_error(sprintf(
-                'Passing "%1$s" at the root of the configuration is deprecated and will not be supported in 3.0, use "%1$s" => array(...) option instead.',
-                $this->root
-            ), E_USER_DEPRECATED);
+            if (!empty(array_diff($passedNames, $names))) {
+                if (getenv('PHP_CS_FIXER_FUTURE_MODE')) {
+                    throw new \RuntimeException(sprintf(
+                        'Passing "%1$s" at the root of the configuration is deprecated and will not be supported in 3.0, use "%1$s" => array(...) option instead.  This check was performed as `PHP_CS_FIXER_FUTURE_MODE` env var is set.',
+                        $this->root
+                    ));
+                }
 
-            $options = [$this->root => $options];
+                @trigger_error(sprintf(
+                    'Passing "%1$s" at the root of the configuration is deprecated and will not be supported in 3.0, use "%1$s" => array(...) option instead.',
+                    $this->root
+                ), E_USER_DEPRECATED);
+
+                $options = [$this->root => $options];
+            }
         }
 
         return $this->resolver->resolve($options);

Some files were not shown because too many files changed in this diff