Skip to content

Commit

Permalink
feature #3886 Add PhpUnitMethodCasingFixer (Slamdunk)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 2.13-dev branch (closes #3886).

Discussion
----------

Add PhpUnitMethodCasingFixer

Closes #3302
```diff
 class MyTest extends \PhpUnit\FrameWork\TestCase
 {
-    public function test_my_code() {}
+    public function testMyCode() {}
 }
```

- [x] Implement base fixer
- [x] Take care of `@depends`
- [x] Run after `PhpUnitTestAnnotationFixer`
- [x] Deprecate `PhpUnitTestAnnotationFixer` *case* config

Commits
-------

b87f254 Add PhpUnitMethodCasingFixer
  • Loading branch information
keradus committed Aug 22, 2018
2 parents 6dc464e + b87f254 commit 1311e2b
Show file tree
Hide file tree
Showing 7 changed files with 674 additions and 208 deletions.
13 changes: 12 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,16 @@ Choose from the list of available rules:
- ``types`` (a subset of ``['normal', 'final', 'abstract']``): what types of
classes to mark as internal; defaults to ``['normal', 'final']``

* **php_unit_method_casing**

Enforce camel (or snake) case for PHPUnit test methods, following
configuration.

Configuration options:

- ``case`` (``'camel_case'``, ``'snake_case'``): apply camel or snake case to test
methods; defaults to ``'camel_case'``

* **php_unit_mock** [@PHPUnit54Migration:risky, @PHPUnit55Migration:risky, @PHPUnit56Migration:risky, @PHPUnit57Migration:risky, @PHPUnit60Migration:risky]

Usages of ``->getMock`` and
Expand Down Expand Up @@ -1319,7 +1329,8 @@ Choose from the list of available rules:
Configuration options:

- ``case`` (``'camel'``, ``'snake'``): whether to camel or snake case when adding the
test prefix; defaults to ``'camel'``
test prefix; defaults to ``'camel'``. DEPRECATED: use
``php_unit_method_casing`` fixer instead
- ``style`` (``'annotation'``, ``'prefix'``): whether to use the @test annotation or
not; defaults to ``'prefix'``

Expand Down
274 changes: 274 additions & 0 deletions src/Fixer/PhpUnit/PhpUnitMethodCasingFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
<?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\DocBlock;
use PhpCsFixer\DocBlock\Line;
use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use PhpCsFixer\Utils;

/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class PhpUnitMethodCasingFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
{
/**
* @internal
*/
const CAMEL_CASE = 'camel_case';

/**
* @internal
*/
const SNAKE_CASE = 'snake_case';

/**
* {@inheritdoc}
*/
public function getDefinition()
{
return new FixerDefinition(
'Enforce camel (or snake) case for PHPUnit test methods, following configuration.',
[
new CodeSample(
'<?php
class MyTest extends \\PhpUnit\\FrameWork\\TestCase
{
public function test_my_code() {}
}
'
),
new CodeSample(
'<?php
class MyTest extends \\PhpUnit\\FrameWork\\TestCase
{
public function testMyCode() {}
}
',
['case' => self::SNAKE_CASE]
),
]
);
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens)
{
return $tokens->isAllTokenKindsFound([T_CLASS, T_FUNCTION]);
}

/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
$phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indexes) {
$this->applyCasing($tokens, $indexes[0], $indexes[1]);
}
}

/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition()
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('case', 'Apply camel or snake case to test methods'))
->setAllowedValues([self::CAMEL_CASE, self::SNAKE_CASE])
->setDefault(self::CAMEL_CASE)
->getOption(),
]);
}

/**
* @param Tokens $tokens
* @param int $startIndex
* @param int $endIndex
*/
private function applyCasing(Tokens $tokens, $startIndex, $endIndex)
{
for ($index = $endIndex - 1; $index > $startIndex; --$index) {
if (!$this->isTestMethod($tokens, $index)) {
continue;
}

$functionNameIndex = $tokens->getNextMeaningfulToken($index);
$functionName = $tokens[$functionNameIndex]->getContent();
$newFunctionName = $this->updateMethodCasing($functionName);

if ($newFunctionName !== $functionName) {
$tokens[$functionNameIndex] = new Token([T_STRING, $newFunctionName]);
}

$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
if ($this->hasDocBlock($tokens, $index)) {
$this->updateDocBlock($tokens, $docBlockIndex);
}
}
}

/**
* @param string $functionName
*
* @return string
*/
private function updateMethodCasing($functionName)
{
if (self::CAMEL_CASE === $this->configuration['case']) {
$newFunctionName = $functionName;
$newFunctionName = ucwords($newFunctionName, '_');
$newFunctionName = str_replace('_', '', $newFunctionName);
$newFunctionName = lcfirst($newFunctionName);
} else {
$newFunctionName = Utils::camelCaseToUnderscore($functionName);
}

return $newFunctionName;
}

/**
* @param Tokens $tokens
* @param int $index
*
* @return bool
*/
private function isTestMethod(Tokens $tokens, $index)
{
// Check if we are dealing with a (non abstract, non lambda) function
if (!$this->isMethod($tokens, $index)) {
return false;
}

// if the function name starts with test it's a test
$functionNameIndex = $tokens->getNextMeaningfulToken($index);
$functionName = $tokens[$functionNameIndex]->getContent();

if ($this->startsWith('test', $functionName)) {
return true;
}
// If the function doesn't have test in its name, and no doc block, it's not a test
if (!$this->hasDocBlock($tokens, $index)) {
return false;
}

$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
$doc = $tokens[$docBlockIndex]->getContent();
if (false === strpos($doc, '@test')) {
return false;
}

return true;
}

/**
* @param Tokens $tokens
* @param int $index
*
* @return bool
*/
private function isMethod(Tokens $tokens, $index)
{
$tokensAnalyzer = new TokensAnalyzer($tokens);

return $tokens[$index]->isGivenKind(T_FUNCTION) && !$tokensAnalyzer->isLambda($index);
}

/**
* @param string $needle
* @param string $haystack
*
* @return bool
*/
private function startsWith($needle, $haystack)
{
return substr($haystack, 0, strlen($needle)) === $needle;
}

/**
* @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 $docBlockIndex
*/
private function updateDocBlock(Tokens $tokens, $docBlockIndex)
{
$doc = new DocBlock($tokens[$docBlockIndex]->getContent());
$lines = $doc->getLines();

$docBlockNeesUpdate = false;
for ($inc = 0; $inc < \count($lines); ++$inc) {
$lineContent = $lines[$inc]->getContent();
if (false === strpos($lineContent, '@depends')) {
continue;
}

$newLineContent = Preg::replaceCallback('/(@depends\s+)(.+)(\b)/', function (array $matches) {
return sprintf(
'%s%s%s',
$matches[1],
$this->updateMethodCasing($matches[2]),
$matches[3]
);
}, $lineContent);

if ($newLineContent !== $lineContent) {
$lines[$inc] = new Line($newLineContent);
$docBlockNeesUpdate = true;
}
}

if ($docBlockNeesUpdate) {
$lines = implode($lines);
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
}
}
}
1 change: 1 addition & 0 deletions src/Fixer/PhpUnit/PhpUnitTestAnnotationFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ protected function createConfigurationDefinition()
(new FixerOptionBuilder('case', 'Whether to camel or snake case when adding the test prefix'))
->setAllowedValues(['camel', 'snake'])
->setDefault('camel')
->setDeprecationMessage('Use `php_unit_method_casing` fixer instead.')
->getOption(),
]);
}
Expand Down
1 change: 1 addition & 0 deletions tests/AutoReview/FixerFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ public function provideFixersPriorityCases()
[$fixers['void_return'], $fixers['phpdoc_no_empty_return']],
[$fixers['void_return'], $fixers['return_type_declaration']],
[$fixers['php_unit_test_annotation'], $fixers['no_empty_phpdoc']],
[$fixers['php_unit_test_annotation'], $fixers['php_unit_method_casing']],
[$fixers['php_unit_test_annotation'], $fixers['phpdoc_trim']],
[$fixers['no_alternative_syntax'], $fixers['braces']],
[$fixers['no_alternative_syntax'], $fixers['elseif']],
Expand Down

0 comments on commit 1311e2b

Please sign in to comment.