Skip to content

Commit e436bad

Browse files
authored
[CodeQuality] Add AddIntersectionVarToMockObjectPropertyRector (#697)
* [CodeQuality] Add AddIntersectionVarToMockObjectPropertyRector for MockObject&mocked-class @var docblock * Register AddIntersectionVarToMockObjectPropertyRector in mocks-to-stubs set
1 parent 5048feb commit e436bad

8 files changed

Lines changed: 305 additions & 0 deletions

File tree

‎config/sets/phpunit-mock-to-stub.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use Rector\Config\RectorConfig;
6+
use Rector\PHPUnit\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector;
67
use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubInCoalesceArgRector;
78
use Rector\PHPUnit\PHPUnit120\Rector\CallLike\CreateStubOverCreateMockArgRector;
89
use Rector\PHPUnit\PHPUnit120\Rector\Class_\PropertyCreateMockToCreateStubRector;
@@ -19,5 +20,6 @@
1920
PropertyCreateMockToCreateStubRector::class,
2021
MockObjectVarToStubRector::class,
2122
BareVarToStubIntersectionRector::class,
23+
AddIntersectionVarToMockObjectPropertyRector::class,
2224
]);
2325
};
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\Class_\AddIntersectionVarToMockObjectPropertyRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AddIntersectionVarToMockObjectPropertyRectorTest 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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class SomeTest extends TestCase
8+
{
9+
private \PHPUnit\Framework\MockObject\MockObject $someServiceMock;
10+
11+
protected function setUp(): void
12+
{
13+
$this->someServiceMock = $this->createMock(\stdClass::class);
14+
}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector\Fixture;
22+
23+
use PHPUnit\Framework\TestCase;
24+
25+
final class SomeTest extends TestCase
26+
{
27+
/**
28+
* @var \PHPUnit\Framework\MockObject\MockObject&\stdClass
29+
*/
30+
private \PHPUnit\Framework\MockObject\MockObject $someServiceMock;
31+
32+
protected function setUp(): void
33+
{
34+
$this->someServiceMock = $this->createMock(\stdClass::class);
35+
}
36+
}
37+
38+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class OverwriteExistingVarTest extends TestCase
8+
{
9+
/**
10+
* @var \PHPUnit\Framework\MockObject\MockObject
11+
*/
12+
private \PHPUnit\Framework\MockObject\MockObject $someServiceMock;
13+
14+
protected function setUp(): void
15+
{
16+
$this->someServiceMock = $this->createMock(\stdClass::class);
17+
}
18+
}
19+
20+
?>
21+
-----
22+
<?php
23+
24+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector\Fixture;
25+
26+
use PHPUnit\Framework\TestCase;
27+
28+
final class OverwriteExistingVarTest extends TestCase
29+
{
30+
/**
31+
* @var \PHPUnit\Framework\MockObject\MockObject&\stdClass
32+
*/
33+
private \PHPUnit\Framework\MockObject\MockObject $someServiceMock;
34+
35+
protected function setUp(): void
36+
{
37+
$this->someServiceMock = $this->createMock(\stdClass::class);
38+
}
39+
}
40+
41+
?>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector\Fixture;
4+
5+
final class SkipNonTestClass
6+
{
7+
private \PHPUnit\Framework\MockObject\MockObject $someServiceMock;
8+
9+
protected function setUp(): void
10+
{
11+
$this->someServiceMock = $this->createMock(\stdClass::class);
12+
}
13+
}
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\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class SkipStubPropertyTest extends TestCase
8+
{
9+
private \PHPUnit\Framework\MockObject\Stub $someServiceMock;
10+
11+
protected function setUp(): void
12+
{
13+
$this->someServiceMock = $this->createMock(\stdClass::class);
14+
}
15+
}
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\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector;
7+
8+
return RectorConfig::configure()
9+
->withRules([AddIntersectionVarToMockObjectPropertyRector::class]);
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\CodeQuality\Rector\Class_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\ClassConstFetch;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Stmt\Class_;
11+
use PhpParser\Node\Stmt\ClassMethod;
12+
use PhpParser\Node\Stmt\Property;
13+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
14+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
15+
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
16+
use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareIntersectionTypeNode;
17+
use Rector\PHPUnit\CodeQuality\NodeAnalyser\MockObjectPropertyDetector;
18+
use Rector\PHPUnit\Enum\PHPUnitClassName;
19+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
20+
use Rector\Rector\AbstractRector;
21+
use Rector\ValueObject\MethodName;
22+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
23+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
24+
25+
/**
26+
* @see \Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\AddIntersectionVarToMockObjectPropertyRector\AddIntersectionVarToMockObjectPropertyRectorTest
27+
*/
28+
final class AddIntersectionVarToMockObjectPropertyRector extends AbstractRector
29+
{
30+
public function __construct(
31+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
32+
private readonly MockObjectPropertyDetector $mockObjectPropertyDetector,
33+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
34+
private readonly PhpDocTypeChanger $phpDocTypeChanger,
35+
) {
36+
}
37+
38+
public function getNodeTypes(): array
39+
{
40+
return [Class_::class];
41+
}
42+
43+
/**
44+
* @param Class_ $node
45+
*/
46+
public function refactor(Node $node): ?Class_
47+
{
48+
if (! $this->testsNodeAnalyzer->isInTestClass($node)) {
49+
return null;
50+
}
51+
52+
$setUpClassMethod = $node->getMethod(MethodName::SET_UP);
53+
if (! $setUpClassMethod instanceof ClassMethod) {
54+
return null;
55+
}
56+
57+
$propertyNamesToCreateMockMethodCalls = $this->mockObjectPropertyDetector->collectFromClassMethod(
58+
$setUpClassMethod
59+
);
60+
if ($propertyNamesToCreateMockMethodCalls === []) {
61+
return null;
62+
}
63+
64+
$hasChanged = false;
65+
66+
foreach ($propertyNamesToCreateMockMethodCalls as $propertyName => $createMockMethodCall) {
67+
$property = $node->getProperty($propertyName);
68+
if (! $property instanceof Property) {
69+
continue;
70+
}
71+
72+
// only properties typed as a bare native MockObject
73+
if (! $this->mockObjectPropertyDetector->detect($property)) {
74+
continue;
75+
}
76+
77+
$mockedClass = $this->resolveMockedClass($createMockMethodCall);
78+
if ($mockedClass === null) {
79+
continue;
80+
}
81+
82+
$intersectionTypeNode = new BracketsAwareIntersectionTypeNode([
83+
new IdentifierTypeNode('\\' . PHPUnitClassName::MOCK_OBJECT),
84+
new IdentifierTypeNode('\\' . $mockedClass),
85+
]);
86+
87+
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property);
88+
$this->phpDocTypeChanger->changeVarTypeNode($property, $phpDocInfo, $intersectionTypeNode);
89+
90+
$hasChanged = true;
91+
}
92+
93+
if (! $hasChanged) {
94+
return null;
95+
}
96+
97+
return $node;
98+
}
99+
100+
public function getRuleDefinition(): RuleDefinition
101+
{
102+
return new RuleDefinition(
103+
'Add a MockObject intersection @var docblock with the mocked class to a native MockObject property',
104+
[
105+
new CodeSample(
106+
<<<'CODE_SAMPLE'
107+
use PHPUnit\Framework\TestCase;
108+
109+
final class SomeTest extends TestCase
110+
{
111+
private \PHPUnit\Framework\MockObject\MockObject $someServiceMock;
112+
113+
protected function setUp(): void
114+
{
115+
$this->someServiceMock = $this->createMock(SomeService::class);
116+
}
117+
}
118+
CODE_SAMPLE
119+
,
120+
<<<'CODE_SAMPLE'
121+
use PHPUnit\Framework\TestCase;
122+
123+
final class SomeTest extends TestCase
124+
{
125+
/**
126+
* @var \PHPUnit\Framework\MockObject\MockObject&\SomeService
127+
*/
128+
private \PHPUnit\Framework\MockObject\MockObject $someServiceMock;
129+
130+
protected function setUp(): void
131+
{
132+
$this->someServiceMock = $this->createMock(SomeService::class);
133+
}
134+
}
135+
CODE_SAMPLE
136+
),
137+
]
138+
);
139+
}
140+
141+
private function resolveMockedClass(MethodCall $methodCall): ?string
142+
{
143+
$firstArg = $methodCall->getArgs()[0] ?? null;
144+
if ($firstArg === null) {
145+
return null;
146+
}
147+
148+
if (! $firstArg->value instanceof ClassConstFetch) {
149+
return null;
150+
}
151+
152+
$className = $this->getName($firstArg->value->class);
153+
if (! is_string($className)) {
154+
return null;
155+
}
156+
157+
return $className;
158+
}
159+
}

0 commit comments

Comments
 (0)