Skip to content

Commit 424bde0

Browse files
committed
[TypeDeclaration] Add ClosureReturnTypeInTestCaseRector for closures and arrow functions in test cases
1 parent e12a222 commit 424bde0

6 files changed

Lines changed: 284 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ClosureReturnTypeInTestCaseRectorTest 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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class ArrowFunctionInTestCase extends TestCase
8+
{
9+
public function test()
10+
{
11+
$callback = fn (\DateTime $dateTime) => $dateTime;
12+
}
13+
}
14+
15+
?>
16+
-----
17+
<?php
18+
19+
namespace Rector\Tests\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector\Fixture;
20+
21+
use PHPUnit\Framework\TestCase;
22+
23+
final class ArrowFunctionInTestCase extends TestCase
24+
{
25+
public function test()
26+
{
27+
$callback = fn (\DateTime $dateTime): \DateTime => $dateTime;
28+
}
29+
}
30+
31+
?>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
final class ClosureInTestCase extends TestCase
8+
{
9+
public function test()
10+
{
11+
$callback = function (object $event) {
12+
return $event;
13+
};
14+
}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\Tests\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector\Fixture;
22+
23+
use PHPUnit\Framework\TestCase;
24+
25+
final class ClosureInTestCase extends TestCase
26+
{
27+
public function test()
28+
{
29+
$callback = function (object $event): object {
30+
return $event;
31+
};
32+
}
33+
}
34+
35+
?>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector\Fixture;
4+
5+
final class SkipOutsideTestCase
6+
{
7+
public function run()
8+
{
9+
$callback = function (object $event) {
10+
return $event;
11+
};
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector;
7+
use Rector\ValueObject\PhpVersion;
8+
9+
return static function (RectorConfig $rectorConfig): void {
10+
$rectorConfig->rule(ClosureReturnTypeInTestCaseRector::class);
11+
$rectorConfig->phpVersion(PhpVersion::PHP_82);
12+
};
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\TypeDeclaration\Rector\Closure;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\ArrayDimFetch;
9+
use PhpParser\Node\Expr\ArrowFunction;
10+
use PhpParser\Node\Expr\Closure;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Reflection\ClassReflection;
13+
use PHPStan\Type\MixedType;
14+
use PHPStan\Type\NeverType;
15+
use PHPStan\Type\NullType;
16+
use PHPStan\Type\Type;
17+
use Rector\Enum\ClassName;
18+
use Rector\PHPStan\ScopeFetcher;
19+
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
20+
use Rector\Rector\AbstractRector;
21+
use Rector\StaticTypeMapper\StaticTypeMapper;
22+
use Rector\TypeDeclaration\TypeInferer\ReturnTypeInferer;
23+
use Rector\ValueObject\PhpVersionFeature;
24+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
25+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
26+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
27+
28+
/**
29+
* @see \Rector\Tests\TypeDeclaration\Rector\Closure\ClosureReturnTypeInTestCaseRector\ClosureReturnTypeInTestCaseRectorTest
30+
*/
31+
final class ClosureReturnTypeInTestCaseRector extends AbstractRector implements MinPhpVersionInterface
32+
{
33+
public function __construct(
34+
private readonly ReturnTypeInferer $returnTypeInferer,
35+
private readonly StaticTypeMapper $staticTypeMapper,
36+
) {
37+
}
38+
39+
public function getRuleDefinition(): RuleDefinition
40+
{
41+
return new RuleDefinition(
42+
'Add known return type to closures and arrow functions inside PHPUnit test case classes',
43+
[
44+
new CodeSample(
45+
<<<'CODE_SAMPLE'
46+
use PHPUnit\Framework\TestCase;
47+
48+
final class SomeTest extends TestCase
49+
{
50+
public function test()
51+
{
52+
$callback = function (object $event) {
53+
return $event;
54+
};
55+
}
56+
}
57+
CODE_SAMPLE
58+
,
59+
<<<'CODE_SAMPLE'
60+
use PHPUnit\Framework\TestCase;
61+
62+
final class SomeTest extends TestCase
63+
{
64+
public function test()
65+
{
66+
$callback = function (object $event): object {
67+
return $event;
68+
};
69+
}
70+
}
71+
CODE_SAMPLE
72+
),
73+
]
74+
);
75+
}
76+
77+
/**
78+
* @return array<class-string<Node>>
79+
*/
80+
public function getNodeTypes(): array
81+
{
82+
return [Closure::class, ArrowFunction::class];
83+
}
84+
85+
/**
86+
* @param Closure|ArrowFunction $node
87+
*/
88+
public function refactor(Node $node): ?Node
89+
{
90+
// type is already set
91+
if ($node->returnType instanceof Node) {
92+
return null;
93+
}
94+
95+
$scope = ScopeFetcher::fetch($node);
96+
if (! $this->isInsideTestCaseClass($scope)) {
97+
return null;
98+
}
99+
100+
$returnType = $node instanceof Closure
101+
? $this->inferClosureReturnType($node)
102+
: $this->inferArrowFunctionReturnType($node);
103+
104+
if (! $returnType instanceof Type) {
105+
return null;
106+
}
107+
108+
$returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($returnType, TypeKind::RETURN);
109+
if (! $returnTypeNode instanceof Node) {
110+
return null;
111+
}
112+
113+
$node->returnType = $returnTypeNode;
114+
115+
return $node;
116+
}
117+
118+
public function provideMinPhpVersion(): int
119+
{
120+
return PhpVersionFeature::SCALAR_TYPES;
121+
}
122+
123+
private function inferClosureReturnType(Closure $closure): ?Type
124+
{
125+
$closureReturnType = $this->returnTypeInferer->inferFunctionLike($closure);
126+
127+
// handled by other rules
128+
if ($closureReturnType instanceof NeverType) {
129+
return null;
130+
}
131+
132+
return $closureReturnType;
133+
}
134+
135+
private function inferArrowFunctionReturnType(ArrowFunction $arrowFunction): ?Type
136+
{
137+
// to allow array shape
138+
$type = $arrowFunction->expr instanceof ArrayDimFetch
139+
? $this->getType($arrowFunction->expr)
140+
: $this->nodeTypeResolver->getNativeType($arrowFunction->expr);
141+
142+
// not valid to add explicit type in PHP
143+
if ($type->isVoid()->yes()) {
144+
return null;
145+
}
146+
147+
$docblockType = $this->getType($arrowFunction->expr);
148+
if ($type instanceof MixedType && $docblockType instanceof NullType) {
149+
return null;
150+
}
151+
152+
return $type;
153+
}
154+
155+
private function isInsideTestCaseClass(Scope $scope): bool
156+
{
157+
$classReflection = $scope->getClassReflection();
158+
if (! $classReflection instanceof ClassReflection) {
159+
return false;
160+
}
161+
162+
// is phpunit test case?
163+
return $classReflection->is(ClassName::TEST_CASE_CLASS);
164+
}
165+
}

0 commit comments

Comments
 (0)