Skip to content

Commit 7ca99bc

Browse files
authored
[phpunit 12] Add ExpressionCreateMockToCreateStubRector (#626)
* [phpunit 12] Add /ExpressionCreateMockToCreateStubRector * fixup! [phpunit 12] Add /ExpressionCreateMockToCreateStubRector
1 parent 74914f2 commit 7ca99bc

File tree

10 files changed

+354
-0
lines changed

10 files changed

+354
-0
lines changed

config/sets/phpunit120.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector;
77
use Rector\PHPUnit\PHPUnit120\Rector\Class_\AssertIsTypeMethodCallRector;
88
use Rector\PHPUnit\PHPUnit120\Rector\Class_\RemoveOverrideFinalConstructTestCaseRector;
9+
use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector;
910

1011
return static function (RectorConfig $rectorConfig): void {
1112
$rectorConfig->rules([
@@ -14,6 +15,7 @@
1415

1516
// stubs over mocks
1617
CreateStubOverCreateMockArgRector::class,
18+
ExpressionCreateMockToCreateStubRector::class,
1719

1820
// experimental, from PHPUnit 12.5.2
1921
// @see https://github.com/sebastianbergmann/phpunit/commit/24c208d6a340c3071f28a9b5cce02b9377adfd43
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector\Source;
4+
5+
final class InstanceWithMock
6+
{
7+
public function __construct(private $object)
8+
{
9+
}
10+
11+
public function getInner(): object
12+
{
13+
return $this->object;
14+
}
15+
}
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\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ExpressionCreateMockToCreateStubRectorTest 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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;
7+
8+
final class SomeTest extends TestCase
9+
{
10+
public function test()
11+
{
12+
$mock = $this->createMock(\stdClass::class);
13+
14+
$someObject = new ClassWithDependency($mock);
15+
$this->assertSame($mock, $someObject->getDependency());
16+
}
17+
}
18+
19+
?>
20+
-----
21+
<?php
22+
23+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;
24+
25+
use PHPUnit\Framework\TestCase;
26+
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;
27+
28+
final class SomeTest extends TestCase
29+
{
30+
public function test()
31+
{
32+
$mock = $this->createStub(\stdClass::class);
33+
34+
$someObject = new ClassWithDependency($mock);
35+
$this->assertSame($mock, $someObject->getDependency());
36+
}
37+
}
38+
39+
?>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;
7+
8+
final class SkipMocking extends TestCase
9+
{
10+
public function test()
11+
{
12+
$mock = $this->createMock(\stdClass::class);
13+
$mock->expects($this->once())->method('someMethod')->willReturn('someValue');
14+
15+
$someObject = new ClassWithDependency($mock);
16+
$this->assertSame($mock, $someObject->getDependency());
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;
7+
8+
final class SkipPropertyForExternalScope extends TestCase
9+
{
10+
private \PHPUnit\Framework\MockObject\MockObject $mock;
11+
12+
public function test()
13+
{
14+
$this->mock = $this->createMock(\stdClass::class);
15+
16+
$someObject = new ClassWithDependency($this->mock);
17+
$this->assertSame($this->mock, $someObject->getDependency());
18+
}
19+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source\ClassWithDependency;
7+
8+
final class SkipUsedOutsideArg extends TestCase
9+
{
10+
public function test()
11+
{
12+
$mock = $this->createMock(\stdClass::class);
13+
14+
if ($mock instanceof \stdClass) {
15+
// do something
16+
}
17+
18+
$someObject = new ClassWithDependency($mock);
19+
$this->assertSame($mock, $someObject->getDependency());
20+
}
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\Source;
4+
5+
final class ClassWithDependency
6+
{
7+
public function __construct(
8+
private $dependency,
9+
) {
10+
}
11+
12+
public function getDependency()
13+
{
14+
return $this->dependency;
15+
}
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\PHPUnit\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector;
7+
8+
return RectorConfig::configure()
9+
->withRules(rules: [ExpressionCreateMockToCreateStubRector::class]);
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\PHPUnit120\Rector\ClassMethod;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr\Assign;
10+
use PhpParser\Node\Expr\CallLike;
11+
use PhpParser\Node\Expr\MethodCall;
12+
use PhpParser\Node\Expr\New_;
13+
use PhpParser\Node\Expr\StaticCall;
14+
use PhpParser\Node\Expr\Variable;
15+
use PhpParser\Node\Identifier;
16+
use PhpParser\Node\Stmt\ClassMethod;
17+
use PhpParser\Node\Stmt\Expression;
18+
use Rector\PhpParser\Node\BetterNodeFinder;
19+
use Rector\PHPUnit\CodeQuality\NodeAnalyser\AssignedMocksCollector;
20+
use Rector\PHPUnit\CodeQuality\NodeFinder\VariableFinder;
21+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
22+
use Rector\Rector\AbstractRector;
23+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
24+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
25+
26+
/**
27+
* @see \Rector\PHPUnit\Tests\PHPUnit120\Rector\ClassMethod\ExpressionCreateMockToCreateStubRector\ExpressionCreateMockToCreateStubRectorTest
28+
*/
29+
final class ExpressionCreateMockToCreateStubRector extends AbstractRector
30+
{
31+
public function __construct(
32+
private readonly AssignedMocksCollector $assignedMocksCollector,
33+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
34+
private readonly VariableFinder $variableFinder,
35+
private readonly BetterNodeFinder $betterNodeFinder,
36+
) {
37+
}
38+
39+
public function getRuleDefinition(): RuleDefinition
40+
{
41+
return new RuleDefinition(
42+
'Replace createMock() assigned to variable that is only used as arg with no expectations, to createStub()',
43+
[
44+
new CodeSample(
45+
<<<'CODE_SAMPLE'
46+
use PHPUnit\Framework\TestCase;
47+
48+
final class SomeTest extends TestCase
49+
{
50+
public function test(): void
51+
{
52+
$mock = $this->createMock(SomeClass::class);
53+
54+
$someObject = new SomeClass($mock);
55+
$this->assertSame($mock, $someObject->getDependency());
56+
}
57+
}
58+
CODE_SAMPLE
59+
,
60+
<<<'CODE_SAMPLE'
61+
use PHPUnit\Framework\TestCase;
62+
63+
final class SomeTest extends TestCase
64+
{
65+
public function test(): void
66+
{
67+
$mock = $this->createStub(SomeClass::class);
68+
69+
$someObject = new SomeClass($mock);
70+
$this->assertSame($mock, $someObject->getDependency());
71+
}
72+
}
73+
CODE_SAMPLE
74+
),
75+
76+
]
77+
);
78+
}
79+
80+
public function getNodeTypes(): array
81+
{
82+
return [ClassMethod::class];
83+
}
84+
85+
/**
86+
* @param ClassMethod $node
87+
*/
88+
public function refactor(Node $node): ?ClassMethod
89+
{
90+
if (! $this->testsNodeAnalyzer->isTestClassMethod($node)) {
91+
return null;
92+
}
93+
94+
if ($node->stmts === null || count($node->stmts) < 2) {
95+
return null;
96+
}
97+
98+
$hasChanged = false;
99+
100+
foreach ($node->stmts as $stmt) {
101+
if (! $stmt instanceof Expression) {
102+
continue;
103+
}
104+
105+
if (! $stmt->expr instanceof Assign) {
106+
continue;
107+
}
108+
109+
$typeArg = $this->assignedMocksCollector->matchCreateMockArgAssignedToVariable($stmt->expr);
110+
if (! $typeArg instanceof Arg) {
111+
continue;
112+
}
113+
114+
/** @var Assign $assign */
115+
$assign = $stmt->expr;
116+
117+
if (! $assign->var instanceof Variable) {
118+
continue;
119+
}
120+
121+
$assignedVariable = $assign->var;
122+
$variableName = $this->getName($assignedVariable);
123+
if ($variableName === null) {
124+
continue;
125+
}
126+
127+
// find variable usages outside call like and inside it
128+
$usedVariables = $this->variableFinder->find($node, $variableName);
129+
130+
// used variable in calls
131+
/** @var array<StaticCall|MethodCall|New_> $callLikes */
132+
$callLikes = $this->betterNodeFinder->findInstancesOfScoped($node->stmts, [CallLike::class]);
133+
134+
$callLikeUsedVariables = $this->collectVariableInCallLikeArg($callLikes, $variableName);
135+
136+
if (count($usedVariables) - 1 !== count($callLikeUsedVariables)) {
137+
continue;
138+
}
139+
140+
// here we can flip the createMock() to createStub()
141+
142+
if (! $assign->expr instanceof MethodCall) {
143+
continue;
144+
}
145+
146+
$methodCall = $assign->expr;
147+
$methodCall->name = new Identifier('createStub');
148+
149+
$hasChanged = true;
150+
}
151+
152+
if ($hasChanged) {
153+
return $node;
154+
}
155+
156+
return null;
157+
}
158+
159+
/**
160+
* @param CallLike[] $callLikes
161+
* @return Variable[]
162+
*/
163+
private function collectVariableInCallLikeArg(array $callLikes, string $variableName): array
164+
{
165+
$callLikeUsedVariables = [];
166+
167+
foreach ($callLikes as $callLike) {
168+
if ($callLike->isFirstClassCallable()) {
169+
continue;
170+
}
171+
172+
foreach ($callLike->getArgs() as $arg) {
173+
if (! $arg->value instanceof Variable) {
174+
continue;
175+
}
176+
177+
if (! $this->isName($arg->value, $variableName)) {
178+
continue;
179+
}
180+
181+
$callLikeUsedVariables[] = $arg->value;
182+
}
183+
}
184+
185+
return $callLikeUsedVariables;
186+
}
187+
}

0 commit comments

Comments
 (0)