Skip to content

Commit 00a1b48

Browse files
authored
Add PhpUnitDedicatedAssertFixer (#638)
1 parent eec7b74 commit 00a1b48

File tree

6 files changed

+357
-2
lines changed

6 files changed

+357
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## v3.2.0
44
- Add PhpUnitAssertArgumentsOrderFixer
5+
- Add PhpUnitDedicatedAssertFixer
56
- PromotedConstructorPropertyFixer - add option "promote_only_existing_properties"
67
- NoUselessCommentFixer - support PHPDoc like `/** ClassName */`
78

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Latest stable version](https://img.shields.io/packagist/v/kubawerlos/php-cs-fixer-custom-fixers.svg?label=current%20version)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
44
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
55
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
6-
![Tests](https://img.shields.io/badge/tests-2932-brightgreen.svg)
6+
![Tests](https://img.shields.io/badge/tests-2978-brightgreen.svg)
77
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
88

99
[![CI Status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions)
@@ -339,6 +339,21 @@ PHPUnit assertions must have expected argument before actual one.
339339
}
340340
```
341341

342+
#### PhpUnitDedicatedAssertFixer
343+
PHPUnit assertions like `assertCount` and `assertInstanceOf` must be used over `assertEquals`/`assertSame`.
344+
*Risky: when original PHPUnit methods are overwritten.*
345+
```diff
346+
<?php
347+
class FooTest extends TestCase {
348+
public function testFoo() {
349+
- self::assertSame($size, count($elements));
350+
- self::assertSame($className, get_class($object));
351+
+ self::assertCount($size, $elements);
352+
+ self::assertInstanceOf($className, $object);
353+
}
354+
}
355+
```
356+
342357
#### PhpUnitNoUselessReturnFixer
343358
PHPUnit `fail`, `markTestIncomplete` and `markTestSkipped` functions should not be followed directly by return.
344359
*Risky: when original PHPUnit methods are overwritten.*

src/Fixer/PhpUnitAssertArgumentsOrderFixer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function testFoo() {
5454
}
5555

5656
/**
57-
* Must run before PhpUnitConstructFixer.
57+
* Must run before PhpUnitConstructFixer, PhpUnitDedicatedAssertFixer.
5858
*/
5959
public function getPriority(): int
6060
{
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace PhpCsFixerCustomFixers\Fixer;
15+
16+
use PhpCsFixer\FixerDefinition\CodeSample;
17+
use PhpCsFixer\FixerDefinition\FixerDefinition;
18+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
19+
use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
20+
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
21+
use PhpCsFixer\Tokenizer\Token;
22+
use PhpCsFixer\Tokenizer\Tokens;
23+
use PhpCsFixerCustomFixers\Analyzer\Analysis\ArgumentAnalysis;
24+
use PhpCsFixerCustomFixers\Analyzer\FunctionAnalyzer;
25+
26+
final class PhpUnitDedicatedAssertFixer extends AbstractFixer
27+
{
28+
private const ASSERTIONS = [
29+
'assertEquals',
30+
'assertNotEquals',
31+
'assertSame',
32+
'assertNotSame',
33+
];
34+
private const REPLACEMENTS_MAP = [
35+
'count' => [
36+
'positive' => 'assertCount',
37+
'negative' => 'assertNotCount',
38+
],
39+
'get_class' => [
40+
'positive' => 'assertInstanceOf',
41+
'negative' => 'assertNotInstanceOf',
42+
],
43+
'sizeof' => [
44+
'positive' => 'assertCount',
45+
'negative' => 'assertNotCount',
46+
],
47+
];
48+
49+
public function getDefinition(): FixerDefinitionInterface
50+
{
51+
return new FixerDefinition(
52+
'PHPUnit assertions like `assertCount` and `assertInstanceOf` must be used over `assertEquals`/`assertSame`.',
53+
[new CodeSample('<?php
54+
class FooTest extends TestCase {
55+
public function testFoo() {
56+
self::assertSame($size, count($elements));
57+
self::assertSame($className, get_class($object));
58+
}
59+
}
60+
')],
61+
null,
62+
'when original PHPUnit methods are overwritten'
63+
);
64+
}
65+
66+
/**
67+
* Must run after PhpUnitAssertArgumentsOrderFixer.
68+
*/
69+
public function getPriority(): int
70+
{
71+
return -1;
72+
}
73+
74+
public function isCandidate(Tokens $tokens): bool
75+
{
76+
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_EXTENDS, \T_FUNCTION]);
77+
}
78+
79+
public function isRisky(): bool
80+
{
81+
return true;
82+
}
83+
84+
public function fix(\SplFileInfo $file, Tokens $tokens): void
85+
{
86+
$phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
87+
88+
/** @var array<int> $indexes */
89+
foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indexes) {
90+
$this->fixAssertions($tokens, $indexes[0], $indexes[1]);
91+
}
92+
}
93+
94+
private function fixAssertions(Tokens $tokens, int $startIndex, int $endIndex): void
95+
{
96+
for ($index = $startIndex; $index < $endIndex; $index++) {
97+
if (!self::isAssertionCall($tokens, $index)) {
98+
continue;
99+
}
100+
101+
$arguments = FunctionAnalyzer::getFunctionArguments($tokens, $index);
102+
if (\count($arguments) < 2) {
103+
continue;
104+
}
105+
106+
self::fixAssertion($tokens, $index, $arguments[1]);
107+
}
108+
}
109+
110+
private static function isAssertionCall(Tokens $tokens, int $index): bool
111+
{
112+
static $assertions;
113+
114+
if ($assertions === null) {
115+
$assertions = \array_flip(
116+
\array_map(
117+
static function (string $name): string {
118+
return \strtolower($name);
119+
},
120+
self::ASSERTIONS
121+
)
122+
);
123+
}
124+
125+
if (!isset($assertions[\strtolower($tokens[$index]->getContent())])) {
126+
return false;
127+
}
128+
129+
/** @var int $openingBraceIndex */
130+
$openingBraceIndex = $tokens->getNextMeaningfulToken($index);
131+
132+
if (!$tokens[$openingBraceIndex]->equals('(')) {
133+
return false;
134+
}
135+
136+
return (new FunctionsAnalyzer())->isTheSameClassCall($tokens, $index);
137+
}
138+
139+
private static function fixAssertion(Tokens $tokens, int $assertionIndex, ArgumentAnalysis $secondArgument): void
140+
{
141+
$functionCallIndex = $secondArgument->getStartIndex();
142+
if ($tokens[$functionCallIndex]->isGivenKind(\T_NS_SEPARATOR)) {
143+
/** @var int $functionCallIndex */
144+
$functionCallIndex = $tokens->getNextMeaningfulToken($functionCallIndex);
145+
}
146+
147+
if (!(new FunctionsAnalyzer())->isGlobalFunctionCall($tokens, $functionCallIndex)) {
148+
return;
149+
}
150+
151+
$arguments = FunctionAnalyzer::getFunctionArguments($tokens, $functionCallIndex);
152+
if (\count($arguments) !== 1) {
153+
return;
154+
}
155+
156+
$functionName = \strtolower($tokens[$functionCallIndex]->getContent());
157+
158+
if (!isset(self::REPLACEMENTS_MAP[$functionName])) {
159+
return;
160+
}
161+
162+
$newAssertion = self::REPLACEMENTS_MAP[$functionName][\stripos($tokens[$assertionIndex]->getContent(), 'not', 6) === false ? 'positive' : 'negative'];
163+
164+
/** @var int $openParenthesisIndex */
165+
$openParenthesisIndex = $tokens->getNextMeaningfulToken($functionCallIndex);
166+
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex);
167+
168+
if ($closeParenthesisIndex < $secondArgument->getEndIndex()) {
169+
return;
170+
}
171+
172+
$tokens[$assertionIndex] = new Token([\T_STRING, $newAssertion]);
173+
$tokens->clearRange($secondArgument->getStartIndex(), $openParenthesisIndex - 1);
174+
$tokens->clearTokenAndMergeSurroundingWhitespace($openParenthesisIndex);
175+
$tokens->clearTokenAndMergeSurroundingWhitespace($closeParenthesisIndex);
176+
}
177+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Tests\Fixer;
15+
16+
use PhpCsFixerCustomFixers\Fixer\PhpUnitDedicatedAssertFixer;
17+
18+
/**
19+
* @internal
20+
*
21+
* @covers \PhpCsFixerCustomFixers\Fixer\PhpUnitDedicatedAssertFixer
22+
*/
23+
final class PhpUnitDedicatedAssertFixerTest extends AbstractFixerTestCase
24+
{
25+
public function testIsRisky(): void
26+
{
27+
self::assertTrue($this->fixer->isRisky());
28+
}
29+
30+
/**
31+
* @dataProvider provideFixCases
32+
*/
33+
public function testFix(string $expected, ?string $input = null): void
34+
{
35+
$this->doTest($expected, $input);
36+
}
37+
38+
/**
39+
* @return iterable<array{0: string, 1?: string}>
40+
*/
41+
public static function provideFixCases(): iterable
42+
{
43+
foreach (self::getFixCases() as $name => $fixCase) {
44+
yield $name => \array_map(
45+
static function (string $case): string {
46+
return \sprintf('<?php
47+
class FooTest extends TestCase {
48+
public function testFoo() {
49+
%s
50+
}
51+
}', $case);
52+
},
53+
$fixCase
54+
);
55+
}
56+
}
57+
58+
/**
59+
* @return iterable<array{0: string, 1?: string}>
60+
*/
61+
private static function getFixCases(): iterable
62+
{
63+
yield 'ignore class on not $this' => ['$notThis->assertSame(3, count($array));'];
64+
yield 'ignore property' => ['self::assertSame;'];
65+
yield 'ignore assertion with no arguments' => ['self::assertSame();'];
66+
yield 'ignore assertion with single argument' => ['self::assertSame(count($array));'];
67+
yield 'ignore other assertions' => ['self::assertGreaterThan(2, count($array));'];
68+
yield 'ignore other functions' => ['self::assertSame(2, countIncorrectly($array));'];
69+
yield 'ignore function in first argument' => ['self::assertSame(count($array), 2);'];
70+
yield 'ignore function from namespace' => ['self::assertSame(2, count\better_count($array));'];
71+
yield 'ignore function used with 2 arguments' => ['self::assertSame(3, count($array, COUNT_RECURSIVE));'];
72+
yield 'ignore assertion with code after function' => ['self::assertSame(3, count($array) + 1);'];
73+
74+
yield 'fix count' => [
75+
'$this->assertCount(3, $array);',
76+
'$this->assertSame(3, count($array));',
77+
];
78+
79+
yield 'fix sizeof' => [
80+
'static::assertCount(3, $array);',
81+
'static::assertSame(3, sizeof($array));',
82+
];
83+
84+
yield 'fix instanceof' => [
85+
'self::assertInstanceOf("stdClass", $object);',
86+
'self::assertSame("stdClass", get_class($object));',
87+
];
88+
89+
yield 'fix not instanceof' => [
90+
'self::assertNotInstanceOf("Closure", $object);',
91+
'self::assertNotSame("Closure", get_class($object));',
92+
];
93+
94+
yield 'fix different casing' => [
95+
'self::assertCount(3, $array);',
96+
'self::assertSame(3, COUNT($array));',
97+
];
98+
99+
yield 'fix expected being variable' => [
100+
'self::assertCount($arrayCount, $array);',
101+
'self::assertSame($arrayCount, count($array));',
102+
];
103+
104+
yield 'fix with leading slash' => [
105+
'self::assertCount(3, $array);',
106+
'self::assertSame(3, \count($array));',
107+
];
108+
109+
yield 'fix with many spaces' => [
110+
'$this->assertCount ( 3 , $array ) ;',
111+
'$this->assertSame ( 3 , \count ( $array ) ) ;',
112+
];
113+
114+
$reflection = new \ReflectionClass(PhpUnitDedicatedAssertFixer::class);
115+
foreach ($reflection->getConstant('ASSERTIONS') as $assertion) {
116+
$expected = 'self::assertCount(3, $array);';
117+
$input = \sprintf('self::%s(3, count($array));', $assertion);
118+
119+
if (\stripos($assertion, 'Not', 6) !== false) {
120+
$expected = \str_replace('assert', 'assertNot', $expected);
121+
$expected = \str_replace('3', '4', $expected);
122+
$input = \str_replace('3', '4', $input);
123+
}
124+
125+
yield \sprintf('Test assertion "%s"', $assertion) => [$expected, $input];
126+
}
127+
128+
yield 'fix multiple assertions' => [
129+
'
130+
if (false) self::assertSame(1);
131+
self::assertSame(3, $arrayCount);
132+
self::assertCount(3, $array);
133+
if (false) self::assertSame(4);
134+
',
135+
'
136+
if (false) self::assertSame(1);
137+
self::assertSame(3, $arrayCount);
138+
self::assertSame(3, count($array));
139+
if (false) self::assertSame(4);
140+
',
141+
];
142+
}
143+
}

tests/PriorityTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,25 @@ public function testFoo() {
417417
',
418418
];
419419

420+
yield [
421+
new CustomFixer\PhpUnitAssertArgumentsOrderFixer(),
422+
new CustomFixer\PhpUnitDedicatedAssertFixer(),
423+
'<?php
424+
class FooTest extends TestCase {
425+
public function testFoo() {
426+
$this->assertCount(3, $elements);
427+
}
428+
}
429+
',
430+
'<?php
431+
class FooTest extends TestCase {
432+
public function testFoo() {
433+
$this->assertSame(count($elements), 3);
434+
}
435+
}
436+
',
437+
];
438+
420439
$noExtraBlankLinesFixer = new Fixer\Whitespace\NoExtraBlankLinesFixer();
421440
$noExtraBlankLinesFixer->configure(['tokens' => ['curly_brace_block']]);
422441
yield [

0 commit comments

Comments
 (0)