Skip to content

Commit 093ebca

Browse files
committed
[code-quality] Add DecorateWillReturnMapWithExpectsMockRector
1 parent 5617724 commit 093ebca

File tree

4 files changed

+198
-0
lines changed

4 files changed

+198
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Expression\DecorateWillReturnMapWithExpectsMockRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class DecorateWillReturnMapWithExpectsMockRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Expression\DecorateWillReturnMapWithExpectsMockRector\Fixture;
6+
7+
final class SomeTest extends \PHPUnit\Framework\TestCase
8+
{
9+
public function test()
10+
{
11+
$someMock = $this->createMock(\stdClass::class);
12+
$someMock->method('some')->willReturnMap([1, 2]);
13+
}
14+
}
15+
16+
?>
17+
-----
18+
<?php
19+
20+
declare(strict_types=1);
21+
22+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Expression\DecorateWillReturnMapWithExpectsMockRector\Fixture;
23+
24+
final class SomeTest extends \PHPUnit\Framework\TestCase
25+
{
26+
public function test()
27+
{
28+
$someMock = $this->createMock(\stdClass::class);
29+
$someMock->expects($this->exactly(2))->method('some')->willReturnMap([1, 2]);
30+
}
31+
}
32+
33+
?>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
7+
return RectorConfig::configure()
8+
->withRules([\Rector\PHPUnit\CodeQuality\Rector\Expression\DecorateWillReturnMapWithExpectsMockRector::class]);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\CodeQuality\Rector\Expression;
6+
7+
use PhpParser\Node;
8+
use Rector\PhpParser\Node\Value\ValueResolver;
9+
use Rector\Rector\AbstractRector;
10+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
11+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
12+
13+
final class DecorateWillReturnMapWithExpectsMockRector extends AbstractRector
14+
{
15+
public function __construct(
16+
private readonly ValueResolver $valueResolver
17+
) {
18+
}
19+
20+
public function getRuleDefinition(): RuleDefinition
21+
{
22+
return new RuleDefinition('Decorate willReturnMap() calls with expects on the mock object if missing', [
23+
new CodeSample(
24+
<<<'CODE_SAMPLE'
25+
use PHPUnit\Framework\TestCase;
26+
use PHPUnit\Framework\MockObject\MockObject;
27+
28+
final class SomeTest extends TestCase
29+
{
30+
private MockObject $someMock;
31+
32+
protected function setUp(): void
33+
{
34+
$this->someMock = $this->createMock(SomeClass::class);
35+
36+
$this->someMock->method("someMethod")
37+
->willReturnMap([
38+
["arg1", "arg2", "result1"],
39+
["arg3", "arg4", "result2"],
40+
]);
41+
}
42+
}
43+
CODE_SAMPLE
44+
,
45+
<<<'CODE_SAMPLE'
46+
use PHPUnit\Framework\TestCase;
47+
use PHPUnit\Framework\MockObject\MockObject;
48+
49+
final class SomeTest extends TestCase
50+
{
51+
private MockObject $someMock;
52+
53+
protected function setUp(): void
54+
{
55+
$this->someMock = $this->createMock(SomeClass::class);
56+
57+
$this->someMock->expects($this->exactly(2))
58+
->method("someMethod")
59+
->willReturnMap([
60+
["arg1", "arg2", "result1"],
61+
["arg3", "arg4", "result2"],
62+
]);
63+
}
64+
}
65+
CODE_SAMPLE
66+
),
67+
68+
]);
69+
}
70+
71+
public function getNodeTypes(): array
72+
{
73+
return [Node\Stmt\Expression::class];
74+
}
75+
76+
/**
77+
* @param Node\Stmt\Expression $node
78+
* @return Node\Stmt\Expression|null
79+
*/
80+
public function refactor(Node $node)
81+
{
82+
if (! $node->expr instanceof Node\Expr\MethodCall) {
83+
return null;
84+
}
85+
86+
$methodCall = $node->expr;
87+
if (! $this->isName($methodCall->name, 'willReturnMap')) {
88+
return null;
89+
}
90+
91+
$topmostCall = $this->resolveTopmostCall($methodCall);
92+
93+
// already covered
94+
if ($this->isName($topmostCall->name, 'expects')) {
95+
return null;
96+
}
97+
98+
// count values in will map arg
99+
$willReturnMapArg = $methodCall->getArgs()[0];
100+
if (! $willReturnMapArg->value instanceof Node\Expr\Array_) {
101+
return null;
102+
}
103+
104+
$array = $willReturnMapArg->value;
105+
$mapCount = count($array->items);
106+
107+
$topmostCall->var = new Node\Expr\MethodCall(
108+
$topmostCall->var,
109+
new Node\Identifier('expects'),
110+
[new Node\Arg(new Node\Expr\MethodCall(
111+
new Node\Expr\Variable('this'),
112+
new Node\Identifier('exactly'),
113+
[new Node\Arg(new Node\Scalar\Int_($mapCount))]
114+
))]
115+
);
116+
117+
return $node;
118+
}
119+
120+
private function resolveTopmostCall(Node\Expr\MethodCall $methodCall): Node\Expr\MethodCall
121+
{
122+
$currentCall = $methodCall;
123+
while ($currentCall->var instanceof Node\Expr\MethodCall) {
124+
$currentCall = $currentCall->var;
125+
}
126+
127+
return $currentCall;
128+
}
129+
}

0 commit comments

Comments
 (0)