Skip to content

Commit 1311e2b

Browse files
committed
feature #3886 Add PhpUnitMethodCasingFixer (Slamdunk)
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
2 parents 6dc464e + b87f254 commit 1311e2b

File tree

7 files changed

+674
-208
lines changed

7 files changed

+674
-208
lines changed

README.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,16 @@ Choose from the list of available rules:
12471247
- ``types`` (a subset of ``['normal', 'final', 'abstract']``): what types of
12481248
classes to mark as internal; defaults to ``['normal', 'final']``
12491249

1250+
* **php_unit_method_casing**
1251+
1252+
Enforce camel (or snake) case for PHPUnit test methods, following
1253+
configuration.
1254+
1255+
Configuration options:
1256+
1257+
- ``case`` (``'camel_case'``, ``'snake_case'``): apply camel or snake case to test
1258+
methods; defaults to ``'camel_case'``
1259+
12501260
* **php_unit_mock** [@PHPUnit54Migration:risky, @PHPUnit55Migration:risky, @PHPUnit56Migration:risky, @PHPUnit57Migration:risky, @PHPUnit60Migration:risky]
12511261

12521262
Usages of ``->getMock`` and
@@ -1319,7 +1329,8 @@ Choose from the list of available rules:
13191329
Configuration options:
13201330

13211331
- ``case`` (``'camel'``, ``'snake'``): whether to camel or snake case when adding the
1322-
test prefix; defaults to ``'camel'``
1332+
test prefix; defaults to ``'camel'``. DEPRECATED: use
1333+
``php_unit_method_casing`` fixer instead
13231334
- ``style`` (``'annotation'``, ``'prefix'``): whether to use the @test annotation or
13241335
not; defaults to ``'prefix'``
13251336

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PHP CS Fixer.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
8+
*
9+
* This source file is subject to the MIT license that is bundled
10+
* with this source code in the file LICENSE.
11+
*/
12+
13+
namespace PhpCsFixer\Fixer\PhpUnit;
14+
15+
use PhpCsFixer\AbstractFixer;
16+
use PhpCsFixer\DocBlock\DocBlock;
17+
use PhpCsFixer\DocBlock\Line;
18+
use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
19+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
20+
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
21+
use PhpCsFixer\FixerDefinition\CodeSample;
22+
use PhpCsFixer\FixerDefinition\FixerDefinition;
23+
use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
24+
use PhpCsFixer\Preg;
25+
use PhpCsFixer\Tokenizer\Token;
26+
use PhpCsFixer\Tokenizer\Tokens;
27+
use PhpCsFixer\Tokenizer\TokensAnalyzer;
28+
use PhpCsFixer\Utils;
29+
30+
/**
31+
* @author Filippo Tessarotto <zoeslam@gmail.com>
32+
*/
33+
final class PhpUnitMethodCasingFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
34+
{
35+
/**
36+
* @internal
37+
*/
38+
const CAMEL_CASE = 'camel_case';
39+
40+
/**
41+
* @internal
42+
*/
43+
const SNAKE_CASE = 'snake_case';
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function getDefinition()
49+
{
50+
return new FixerDefinition(
51+
'Enforce camel (or snake) case for PHPUnit test methods, following configuration.',
52+
[
53+
new CodeSample(
54+
'<?php
55+
class MyTest extends \\PhpUnit\\FrameWork\\TestCase
56+
{
57+
public function test_my_code() {}
58+
}
59+
'
60+
),
61+
new CodeSample(
62+
'<?php
63+
class MyTest extends \\PhpUnit\\FrameWork\\TestCase
64+
{
65+
public function testMyCode() {}
66+
}
67+
',
68+
['case' => self::SNAKE_CASE]
69+
),
70+
]
71+
);
72+
}
73+
74+
/**
75+
* {@inheritdoc}
76+
*/
77+
public function isCandidate(Tokens $tokens)
78+
{
79+
return $tokens->isAllTokenKindsFound([T_CLASS, T_FUNCTION]);
80+
}
81+
82+
/**
83+
* {@inheritdoc}
84+
*/
85+
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
86+
{
87+
$phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
88+
foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indexes) {
89+
$this->applyCasing($tokens, $indexes[0], $indexes[1]);
90+
}
91+
}
92+
93+
/**
94+
* {@inheritdoc}
95+
*/
96+
protected function createConfigurationDefinition()
97+
{
98+
return new FixerConfigurationResolver([
99+
(new FixerOptionBuilder('case', 'Apply camel or snake case to test methods'))
100+
->setAllowedValues([self::CAMEL_CASE, self::SNAKE_CASE])
101+
->setDefault(self::CAMEL_CASE)
102+
->getOption(),
103+
]);
104+
}
105+
106+
/**
107+
* @param Tokens $tokens
108+
* @param int $startIndex
109+
* @param int $endIndex
110+
*/
111+
private function applyCasing(Tokens $tokens, $startIndex, $endIndex)
112+
{
113+
for ($index = $endIndex - 1; $index > $startIndex; --$index) {
114+
if (!$this->isTestMethod($tokens, $index)) {
115+
continue;
116+
}
117+
118+
$functionNameIndex = $tokens->getNextMeaningfulToken($index);
119+
$functionName = $tokens[$functionNameIndex]->getContent();
120+
$newFunctionName = $this->updateMethodCasing($functionName);
121+
122+
if ($newFunctionName !== $functionName) {
123+
$tokens[$functionNameIndex] = new Token([T_STRING, $newFunctionName]);
124+
}
125+
126+
$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
127+
if ($this->hasDocBlock($tokens, $index)) {
128+
$this->updateDocBlock($tokens, $docBlockIndex);
129+
}
130+
}
131+
}
132+
133+
/**
134+
* @param string $functionName
135+
*
136+
* @return string
137+
*/
138+
private function updateMethodCasing($functionName)
139+
{
140+
if (self::CAMEL_CASE === $this->configuration['case']) {
141+
$newFunctionName = $functionName;
142+
$newFunctionName = ucwords($newFunctionName, '_');
143+
$newFunctionName = str_replace('_', '', $newFunctionName);
144+
$newFunctionName = lcfirst($newFunctionName);
145+
} else {
146+
$newFunctionName = Utils::camelCaseToUnderscore($functionName);
147+
}
148+
149+
return $newFunctionName;
150+
}
151+
152+
/**
153+
* @param Tokens $tokens
154+
* @param int $index
155+
*
156+
* @return bool
157+
*/
158+
private function isTestMethod(Tokens $tokens, $index)
159+
{
160+
// Check if we are dealing with a (non abstract, non lambda) function
161+
if (!$this->isMethod($tokens, $index)) {
162+
return false;
163+
}
164+
165+
// if the function name starts with test it's a test
166+
$functionNameIndex = $tokens->getNextMeaningfulToken($index);
167+
$functionName = $tokens[$functionNameIndex]->getContent();
168+
169+
if ($this->startsWith('test', $functionName)) {
170+
return true;
171+
}
172+
// If the function doesn't have test in its name, and no doc block, it's not a test
173+
if (!$this->hasDocBlock($tokens, $index)) {
174+
return false;
175+
}
176+
177+
$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
178+
$doc = $tokens[$docBlockIndex]->getContent();
179+
if (false === strpos($doc, '@test')) {
180+
return false;
181+
}
182+
183+
return true;
184+
}
185+
186+
/**
187+
* @param Tokens $tokens
188+
* @param int $index
189+
*
190+
* @return bool
191+
*/
192+
private function isMethod(Tokens $tokens, $index)
193+
{
194+
$tokensAnalyzer = new TokensAnalyzer($tokens);
195+
196+
return $tokens[$index]->isGivenKind(T_FUNCTION) && !$tokensAnalyzer->isLambda($index);
197+
}
198+
199+
/**
200+
* @param string $needle
201+
* @param string $haystack
202+
*
203+
* @return bool
204+
*/
205+
private function startsWith($needle, $haystack)
206+
{
207+
return substr($haystack, 0, strlen($needle)) === $needle;
208+
}
209+
210+
/**
211+
* @param Tokens $tokens
212+
* @param int $index
213+
*
214+
* @return bool
215+
*/
216+
private function hasDocBlock(Tokens $tokens, $index)
217+
{
218+
$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
219+
220+
return $tokens[$docBlockIndex]->isGivenKind(T_DOC_COMMENT);
221+
}
222+
223+
/**
224+
* @param Tokens $tokens
225+
* @param int $index
226+
*
227+
* @return int
228+
*/
229+
private function getDocBlockIndex(Tokens $tokens, $index)
230+
{
231+
do {
232+
$index = $tokens->getPrevNonWhitespace($index);
233+
} while ($tokens[$index]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT]));
234+
235+
return $index;
236+
}
237+
238+
/**
239+
* @param Tokens $tokens
240+
* @param int $docBlockIndex
241+
*/
242+
private function updateDocBlock(Tokens $tokens, $docBlockIndex)
243+
{
244+
$doc = new DocBlock($tokens[$docBlockIndex]->getContent());
245+
$lines = $doc->getLines();
246+
247+
$docBlockNeesUpdate = false;
248+
for ($inc = 0; $inc < \count($lines); ++$inc) {
249+
$lineContent = $lines[$inc]->getContent();
250+
if (false === strpos($lineContent, '@depends')) {
251+
continue;
252+
}
253+
254+
$newLineContent = Preg::replaceCallback('/(@depends\s+)(.+)(\b)/', function (array $matches) {
255+
return sprintf(
256+
'%s%s%s',
257+
$matches[1],
258+
$this->updateMethodCasing($matches[2]),
259+
$matches[3]
260+
);
261+
}, $lineContent);
262+
263+
if ($newLineContent !== $lineContent) {
264+
$lines[$inc] = new Line($newLineContent);
265+
$docBlockNeesUpdate = true;
266+
}
267+
}
268+
269+
if ($docBlockNeesUpdate) {
270+
$lines = implode($lines);
271+
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
272+
}
273+
}
274+
}

src/Fixer/PhpUnit/PhpUnitTestAnnotationFixer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ protected function createConfigurationDefinition()
108108
(new FixerOptionBuilder('case', 'Whether to camel or snake case when adding the test prefix'))
109109
->setAllowedValues(['camel', 'snake'])
110110
->setDefault('camel')
111+
->setDeprecationMessage('Use `php_unit_method_casing` fixer instead.')
111112
->getOption(),
112113
]);
113114
}

tests/AutoReview/FixerFactoryTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ public function provideFixersPriorityCases()
208208
[$fixers['void_return'], $fixers['phpdoc_no_empty_return']],
209209
[$fixers['void_return'], $fixers['return_type_declaration']],
210210
[$fixers['php_unit_test_annotation'], $fixers['no_empty_phpdoc']],
211+
[$fixers['php_unit_test_annotation'], $fixers['php_unit_method_casing']],
211212
[$fixers['php_unit_test_annotation'], $fixers['phpdoc_trim']],
212213
[$fixers['no_alternative_syntax'], $fixers['braces']],
213214
[$fixers['no_alternative_syntax'], $fixers['elseif']],

0 commit comments

Comments
 (0)