Skip to content

Commit 2b979f8

Browse files
committed
feature #3939 Add PhpUnitSizeClassFixer (Jefersson Nathan)
This PR was merged into the 2.15-dev branch. Discussion ---------- Add PhpUnitSizeClassFixer _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'`` Commits ------- 5f017a8 Add fix for phpunit class size annotation
2 parents 78acc4d + 5f017a8 commit 2b979f8

File tree

4 files changed

+639
-0
lines changed

4 files changed

+639
-0
lines changed

README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,16 @@ Choose from the list of available rules:
14001400

14011401
*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.*
14021402

1403+
* **php_unit_size_class**
1404+
1405+
All PHPUnit test cases should have ``@small``, ``@medium`` or ``@large``
1406+
annotation to enable run time limits.
1407+
1408+
Configuration options:
1409+
1410+
- ``group`` (``'large'``, ``'medium'``, ``'small'``): define a specific group to be used
1411+
in case no group is already in use; defaults to ``'small'``
1412+
14031413
* **php_unit_strict** [@PhpCsFixer:risky]
14041414

14051415
PHPUnit methods like ``assertSame`` should be used instead of
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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\Annotation;
17+
use PhpCsFixer\DocBlock\DocBlock;
18+
use PhpCsFixer\DocBlock\Line;
19+
use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
20+
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
21+
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
22+
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23+
use PhpCsFixer\FixerDefinition\CodeSample;
24+
use PhpCsFixer\FixerDefinition\FixerDefinition;
25+
use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
26+
use PhpCsFixer\Tokenizer\Token;
27+
use PhpCsFixer\Tokenizer\Tokens;
28+
use SplFileInfo;
29+
30+
/**
31+
* @author Jefersson Nathan <malukenho.dev@gmail.com>
32+
*/
33+
final class PhpUnitSizeClassFixer extends AbstractFixer implements WhitespacesAwareFixerInterface, ConfigurationDefinitionFixerInterface
34+
{
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
public function getDefinition()
39+
{
40+
return new FixerDefinition(
41+
'All PHPUnit test cases should have `@small`, `@medium` or `@large` annotation to enable run time limits.',
42+
[new CodeSample("<?php\nclass MyTest extends TestCase {}\n")],
43+
'The special groups [small, medium, large] provides a way to identify tests that are taking long to be executed.'
44+
);
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function isCandidate(Tokens $tokens)
51+
{
52+
return $tokens->isAllTokenKindsFound([T_CLASS, T_STRING]);
53+
}
54+
55+
protected function applyFix(SplFileInfo $file, Tokens $tokens)
56+
{
57+
$phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
58+
59+
foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens, true) as $indexes) {
60+
$this->markClassSize($tokens, $indexes[0]);
61+
}
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
protected function createConfigurationDefinition()
68+
{
69+
return new FixerConfigurationResolver([
70+
(new FixerOptionBuilder('group', 'Define a specific group to be used in case no group is already in use'))
71+
->setAllowedValues(['small', 'medium', 'large'])
72+
->setDefault('small')
73+
->getOption(),
74+
]);
75+
}
76+
77+
/**
78+
* @param Tokens $tokens
79+
* @param int $startIndex
80+
*/
81+
private function markClassSize(Tokens $tokens, $startIndex)
82+
{
83+
$classIndex = $tokens->getPrevTokenOfKind($startIndex, [[T_CLASS]]);
84+
85+
if ($this->isAbstractClass($tokens, $classIndex)) {
86+
return;
87+
}
88+
89+
$docBlockIndex = $this->getDocBlockIndex($tokens, $classIndex);
90+
91+
if ($this->hasDocBlock($tokens, $classIndex)) {
92+
$this->updateDocBlockIfNeeded($tokens, $docBlockIndex);
93+
94+
return;
95+
}
96+
97+
$this->createDocBlock($tokens, $docBlockIndex);
98+
}
99+
100+
/**
101+
* @param Tokens $tokens
102+
* @param int $i
103+
*
104+
* @return bool
105+
*/
106+
private function isAbstractClass(Tokens $tokens, $i)
107+
{
108+
$typeIndex = $tokens->getPrevMeaningfulToken($i);
109+
110+
return $tokens[$typeIndex]->isGivenKind([T_ABSTRACT]);
111+
}
112+
113+
private function createDocBlock(Tokens $tokens, $docBlockIndex)
114+
{
115+
$lineEnd = $this->whitespacesConfig->getLineEnding();
116+
$originalIndent = $this->detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
117+
$group = $this->configuration['group'];
118+
$toInsert = [
119+
new Token([T_DOC_COMMENT, '/**'.$lineEnd."${originalIndent} * @".$group.$lineEnd."${originalIndent} */"]),
120+
new Token([T_WHITESPACE, $lineEnd.$originalIndent]),
121+
];
122+
$index = $tokens->getNextMeaningfulToken($docBlockIndex);
123+
$tokens->insertAt($index, $toInsert);
124+
}
125+
126+
private function updateDocBlockIfNeeded(Tokens $tokens, $docBlockIndex)
127+
{
128+
$doc = new DocBlock($tokens[$docBlockIndex]->getContent());
129+
if (!empty($this->filterDocBlock($doc))) {
130+
return;
131+
}
132+
$doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex);
133+
$lines = $this->addSizeAnnotation($doc, $tokens, $docBlockIndex);
134+
$lines = implode('', $lines);
135+
136+
$tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
137+
}
138+
139+
/**
140+
* @param Tokens $tokens
141+
* @param int $index
142+
*
143+
* @return bool
144+
*/
145+
private function hasDocBlock(Tokens $tokens, $index)
146+
{
147+
$docBlockIndex = $this->getDocBlockIndex($tokens, $index);
148+
149+
return $tokens[$docBlockIndex]->isGivenKind(T_DOC_COMMENT);
150+
}
151+
152+
/**
153+
* @param Tokens $tokens
154+
* @param int $index
155+
*
156+
* @return int
157+
*/
158+
private function getDocBlockIndex(Tokens $tokens, $index)
159+
{
160+
do {
161+
$index = $tokens->getPrevNonWhitespace($index);
162+
} while ($tokens[$index]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT]));
163+
164+
return $index;
165+
}
166+
167+
/**
168+
* @param Tokens $tokens
169+
* @param int $index
170+
*
171+
* @return string
172+
*/
173+
private function detectIndent(Tokens $tokens, $index)
174+
{
175+
if (!$tokens[$index - 1]->isWhitespace()) {
176+
return ''; // cannot detect indent
177+
}
178+
179+
$explodedContent = explode($this->whitespacesConfig->getLineEnding(), $tokens[$index - 1]->getContent());
180+
181+
return end($explodedContent);
182+
}
183+
184+
/**
185+
* @param DocBlock $docBlock
186+
* @param Tokens $tokens
187+
* @param int $docBlockIndex
188+
*
189+
* @return Line[]
190+
*/
191+
private function addSizeAnnotation(DocBlock $docBlock, Tokens $tokens, $docBlockIndex)
192+
{
193+
$lines = $docBlock->getLines();
194+
$originalIndent = $this->detectIndent($tokens, $docBlockIndex);
195+
$lineEnd = $this->whitespacesConfig->getLineEnding();
196+
$group = $this->configuration['group'];
197+
array_splice($lines, -1, 0, $originalIndent.' *'.$lineEnd.$originalIndent.' * @'.$group.$lineEnd);
198+
199+
return $lines;
200+
}
201+
202+
/**
203+
* @param DocBlock $doc
204+
* @param Tokens $tokens
205+
* @param int $docBlockIndex
206+
*
207+
* @return DocBlock
208+
*/
209+
private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, $docBlockIndex)
210+
{
211+
$lines = $doc->getLines();
212+
if (1 === \count($lines) && empty($this->filterDocBlock($doc))) {
213+
$lines = $this->splitUpDocBlock($lines, $tokens, $docBlockIndex);
214+
215+
return new DocBlock(implode('', $lines));
216+
}
217+
218+
return $doc;
219+
}
220+
221+
/**
222+
* Take a one line doc block, and turn it into a multi line doc block.
223+
*
224+
* @param Line[] $lines
225+
* @param Tokens $tokens
226+
* @param int $docBlockIndex
227+
*
228+
* @return Line[]
229+
*/
230+
private function splitUpDocBlock($lines, Tokens $tokens, $docBlockIndex)
231+
{
232+
$lineContent = $this->getSingleLineDocBlockEntry($lines);
233+
$lineEnd = $this->whitespacesConfig->getLineEnding();
234+
$originalIndent = $this->detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
235+
236+
return [
237+
new Line('/**'.$lineEnd),
238+
new Line($originalIndent.' * '.$lineContent.$lineEnd),
239+
new Line($originalIndent.' */'),
240+
];
241+
}
242+
243+
/**
244+
* @param Line|Line[]|string $line
245+
*
246+
* @return string
247+
*/
248+
private function getSingleLineDocBlockEntry($line)
249+
{
250+
$line = $line[0];
251+
$line = str_replace('*/', '', $line);
252+
$line = trim($line);
253+
$line = str_split($line);
254+
$i = \count($line);
255+
do {
256+
--$i;
257+
} while ('*' !== $line[$i] && '*' !== $line[$i - 1] && '/' !== $line[$i - 2]);
258+
if (' ' === $line[$i]) {
259+
++$i;
260+
}
261+
$line = \array_slice($line, $i);
262+
263+
return implode('', $line);
264+
}
265+
266+
/**
267+
* @param DocBlock $doc
268+
*
269+
* @return Annotation[]
270+
*/
271+
private function filterDocBlock(DocBlock $doc)
272+
{
273+
return array_filter([
274+
$doc->getAnnotationsOfType('small'),
275+
$doc->getAnnotationsOfType('large'),
276+
$doc->getAnnotationsOfType('medium'),
277+
]);
278+
}
279+
}

tests/Fixer/PhpUnit/PhpUnitInternalClassFixerTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,38 @@ class Test Extends TestCase
162162
{
163163
}
164164
}
165+
',
166+
],
167+
'It works for tab ident' => [
168+
'<?php
169+
170+
if (class_exists("Foo\Bar")) {
171+
/**
172+
* @author me again
173+
*
174+
*
175+
* @covers \Other\Class
176+
*
177+
* @internal
178+
*/
179+
class Test Extends TestCase
180+
{
181+
}
182+
}
183+
',
184+
'<?php
185+
186+
if (class_exists("Foo\Bar")) {
187+
/**
188+
* @author me again
189+
*
190+
*
191+
* @covers \Other\Class
192+
*/
193+
class Test Extends TestCase
194+
{
195+
}
196+
}
165197
',
166198
],
167199
'It always adds @internal to the bottom of the doc block' => [

0 commit comments

Comments
 (0)