Skip to content

Commit 879ea50

Browse files
authored
[CodeQuality] Add ChangeMockObjectReturnUnionToIntersectionRector (#703)
* [CodeQuality] Add ChangeMockObjectReturnUnionToIntersectionRector * extend rule to Stub return union
1 parent 63a8471 commit 879ea50

9 files changed

Lines changed: 320 additions & 0 deletions

File tree

config/sets/phpunit-code-quality.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\AddInstanceofAssertForNullableArgumentRector;
2020
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\AddInstanceofAssertForNullableInstanceRector;
2121
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\BareCreateMockAssignToDirectUseRector;
22+
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector;
2223
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\DataProviderArrayItemsNewLinedRector;
2324
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\EntityDocumentCreateMockToDirectNewRector;
2425
use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\NoSetupWithParentCallOverrideRector;
@@ -155,6 +156,7 @@
155156
EntityDocumentCreateMockToDirectNewRector::class,
156157
ReplaceAtMethodWithDesiredMatcherRector::class,
157158
BareCreateMockAssignToDirectUseRector::class,
159+
ChangeMockObjectReturnUnionToIntersectionRector::class,
158160
DecorateWillReturnMapWithExpectsMockRector::class,
159161

160162
// dead code
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\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ChangeMockObjectReturnUnionToIntersectionRectorTest 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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class SomeTest extends TestCase
8+
{
9+
/**
10+
* @return Event|\PHPUnit\Framework\MockObject\MockObject
11+
*/
12+
private function createEvent(): \PHPUnit\Framework\MockObject\MockObject
13+
{
14+
return $this->createMock(Event::class);
15+
}
16+
}
17+
18+
?>
19+
-----
20+
<?php
21+
22+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
23+
24+
use PHPUnit\Framework\TestCase;
25+
26+
final class SomeTest extends TestCase
27+
{
28+
/**
29+
* @return Event&\PHPUnit\Framework\MockObject\MockObject
30+
*/
31+
private function createEvent(): \PHPUnit\Framework\MockObject\MockObject
32+
{
33+
return $this->createMock(Event::class);
34+
}
35+
}
36+
37+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
4+
5+
use PHPUnit\Framework\MockObject\MockObject;
6+
use PHPUnit\Framework\TestCase;
7+
8+
final class ShortMockObjectTest extends TestCase
9+
{
10+
/**
11+
* @return Event|MockObject
12+
*/
13+
private function createEvent(): MockObject
14+
{
15+
return $this->createMock(Event::class);
16+
}
17+
}
18+
19+
?>
20+
-----
21+
<?php
22+
23+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
24+
25+
use PHPUnit\Framework\MockObject\MockObject;
26+
use PHPUnit\Framework\TestCase;
27+
28+
final class ShortMockObjectTest extends TestCase
29+
{
30+
/**
31+
* @return Event&MockObject
32+
*/
33+
private function createEvent(): MockObject
34+
{
35+
return $this->createMock(Event::class);
36+
}
37+
}
38+
39+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class SkipNonMockUnionTest extends TestCase
8+
{
9+
/**
10+
* @return Event|Campaign
11+
*/
12+
private function resolve()
13+
{
14+
return $this->createEvent();
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
4+
5+
final class SkipNonTestClass
6+
{
7+
/**
8+
* @return Event|\PHPUnit\Framework\MockObject\MockObject
9+
*/
10+
private function createEvent()
11+
{
12+
return $this->createMock(Event::class);
13+
}
14+
}
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\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
4+
5+
use PHPUnit\Framework\MockObject\Stub;
6+
use PHPUnit\Framework\TestCase;
7+
8+
final class StubTest extends TestCase
9+
{
10+
/**
11+
* @return Event|Stub
12+
*/
13+
private function createEvent(): Stub
14+
{
15+
return $this->createStub(Event::class);
16+
}
17+
}
18+
19+
?>
20+
-----
21+
<?php
22+
23+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\Fixture;
24+
25+
use PHPUnit\Framework\MockObject\Stub;
26+
use PHPUnit\Framework\TestCase;
27+
28+
final class StubTest extends TestCase
29+
{
30+
/**
31+
* @return Event&Stub
32+
*/
33+
private function createEvent(): Stub
34+
{
35+
return $this->createStub(Event::class);
36+
}
37+
}
38+
39+
?>
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\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector;
7+
8+
return RectorConfig::configure()
9+
->withRules([ChangeMockObjectReturnUnionToIntersectionRector::class]);
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\CodeQuality\Rector\ClassMethod;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\ClassMethod;
9+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
10+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
11+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12+
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
13+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
14+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
15+
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
16+
use Rector\BetterPhpDocParser\ValueObject\Type\BracketsAwareIntersectionTypeNode;
17+
use Rector\PHPUnit\Enum\PHPUnitClassName;
18+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
19+
use Rector\Rector\AbstractRector;
20+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
21+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
22+
23+
/**
24+
* @see \Rector\PHPUnit\Tests\CodeQuality\Rector\ClassMethod\ChangeMockObjectReturnUnionToIntersectionRector\ChangeMockObjectReturnUnionToIntersectionRectorTest
25+
*/
26+
final class ChangeMockObjectReturnUnionToIntersectionRector extends AbstractRector
27+
{
28+
public function __construct(
29+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
30+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
31+
private readonly PhpDocTypeChanger $phpDocTypeChanger,
32+
) {
33+
}
34+
35+
public function getNodeTypes(): array
36+
{
37+
return [ClassMethod::class];
38+
}
39+
40+
/**
41+
* @param ClassMethod $node
42+
*/
43+
public function refactor(Node $node): ?ClassMethod
44+
{
45+
if (! $this->testsNodeAnalyzer->isInTestClass($node)) {
46+
return null;
47+
}
48+
49+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);
50+
if (! $phpDocInfo instanceof PhpDocInfo) {
51+
return null;
52+
}
53+
54+
$returnTagValueNode = $phpDocInfo->getReturnTagValue();
55+
if (! $returnTagValueNode instanceof ReturnTagValueNode) {
56+
return null;
57+
}
58+
59+
$returnTypeNode = $returnTagValueNode->type;
60+
if (! $returnTypeNode instanceof UnionTypeNode) {
61+
return null;
62+
}
63+
64+
// must contain a MockObject or Stub member to be a mock union
65+
if (! $this->hasMockObjectOrStubType($returnTypeNode)) {
66+
return null;
67+
}
68+
69+
$bracketsAwareIntersectionTypeNode = new BracketsAwareIntersectionTypeNode($returnTypeNode->types);
70+
$this->phpDocTypeChanger->changeReturnTypeNode($node, $phpDocInfo, $bracketsAwareIntersectionTypeNode);
71+
72+
return $node;
73+
}
74+
75+
public function getRuleDefinition(): RuleDefinition
76+
{
77+
return new RuleDefinition(
78+
'Change a MockObject @return union docblock to an intersection type',
79+
[
80+
new CodeSample(
81+
<<<'CODE_SAMPLE'
82+
use PHPUnit\Framework\TestCase;
83+
84+
final class SomeTest extends TestCase
85+
{
86+
/**
87+
* @return Event|\PHPUnit\Framework\MockObject\MockObject
88+
*/
89+
private function createEvent(): \PHPUnit\Framework\MockObject\MockObject
90+
{
91+
return $this->createMock(Event::class);
92+
}
93+
}
94+
CODE_SAMPLE
95+
,
96+
<<<'CODE_SAMPLE'
97+
use PHPUnit\Framework\TestCase;
98+
99+
final class SomeTest extends TestCase
100+
{
101+
/**
102+
* @return Event&\PHPUnit\Framework\MockObject\MockObject
103+
*/
104+
private function createEvent(): \PHPUnit\Framework\MockObject\MockObject
105+
{
106+
return $this->createMock(Event::class);
107+
}
108+
}
109+
CODE_SAMPLE
110+
),
111+
]
112+
);
113+
}
114+
115+
private function hasMockObjectOrStubType(UnionTypeNode $unionTypeNode): bool
116+
{
117+
return array_any($unionTypeNode->types, fn (TypeNode $typeNode): bool => $this->isMockObjectOrStubType(
118+
$typeNode
119+
));
120+
}
121+
122+
private function isMockObjectOrStubType(TypeNode $typeNode): bool
123+
{
124+
if (! $typeNode instanceof IdentifierTypeNode) {
125+
return false;
126+
}
127+
128+
$typeName = ltrim($typeNode->name, '\\');
129+
130+
return in_array(
131+
$typeName,
132+
[PHPUnitClassName::MOCK_OBJECT, 'MockObject', PHPUnitClassName::STUB, 'Stub'],
133+
true
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)