Skip to content

Commit ca45e6d

Browse files
authored
Adds the TypeHintTappableCallRector (#244)
* Adds the TypeHintTappableCallRector * fix import * Feedback fixes
1 parent 037f997 commit ca45e6d

12 files changed

+303
-0
lines changed

docs/rector_rules_overview.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,6 +1326,26 @@ Change if throw to throw_if
13261326

13271327
<br>
13281328

1329+
## TypeHintTappableCallRector
1330+
1331+
Automatically type hints your tappable closures
1332+
1333+
- class: [`RectorLaravel\Rector\FuncCall\TypeHintTappableCallRector`](../src/Rector/FuncCall/TypeHintTappableCallRector.php)
1334+
1335+
```diff
1336+
-tap($collection, function ($collection) {}
1337+
+tap($collection, function (Collection $collection) {}
1338+
```
1339+
1340+
<br>
1341+
1342+
```diff
1343+
-(new Collection)->tap(function ($collection) {}
1344+
+(new Collection)->tap(function (Collection $collection) {}
1345+
```
1346+
1347+
<br>
1348+
13291349
## UnifyModelDatesWithCastsRector
13301350

13311351
Unify Model `$dates` property with `$casts`
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
namespace RectorLaravel\Rector\FuncCall;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\Closure;
7+
use PhpParser\Node\Expr\FuncCall;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Param;
10+
use PHPStan\Type\ObjectType;
11+
use Rector\NodeTypeResolver\TypeComparator\TypeComparator;
12+
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
13+
use Rector\Rector\AbstractRector;
14+
use Rector\StaticTypeMapper\StaticTypeMapper;
15+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
16+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
17+
18+
/**
19+
* @see \RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\TypeHintTappableCallRectorTest
20+
*/
21+
class TypeHintTappableCallRector extends AbstractRector
22+
{
23+
private const TAPPABLE_TRAIT = 'Illuminate\Support\Traits\Tappable';
24+
25+
public function __construct(
26+
private readonly TypeComparator $typeComparator,
27+
private readonly StaticTypeMapper $staticTypeMapper
28+
) {
29+
}
30+
31+
public function getRuleDefinition(): RuleDefinition
32+
{
33+
return new RuleDefinition(
34+
'Automatically type hints your tappable closures',
35+
[
36+
new CodeSample(<<<'CODE_SAMPLE'
37+
tap($collection, function ($collection) {}
38+
CODE_SAMPLE,
39+
<<<'CODE_SAMPLE'
40+
tap($collection, function (Collection $collection) {}
41+
CODE_SAMPLE
42+
),
43+
new CodeSample(<<<'CODE_SAMPLE'
44+
(new Collection)->tap(function ($collection) {}
45+
CODE_SAMPLE,
46+
<<<'CODE_SAMPLE'
47+
(new Collection)->tap(function (Collection $collection) {}
48+
CODE_SAMPLE
49+
),
50+
]
51+
);
52+
}
53+
54+
public function getNodeTypes(): array
55+
{
56+
return [MethodCall::class, FuncCall::class];
57+
}
58+
59+
/**
60+
* @param MethodCall|FuncCall $node
61+
*/
62+
public function refactor(Node $node): ?Node
63+
{
64+
if (! $this->isName($node->name, 'tap')) {
65+
return null;
66+
}
67+
68+
if ($node->isFirstClassCallable()) {
69+
return null;
70+
}
71+
72+
if ($node instanceof MethodCall && $node->getArgs() !== []) {
73+
return $this->refactorMethodCall($node);
74+
}
75+
76+
if (count($node->getArgs()) < 2 || ! $node->getArgs()[1]->value instanceof Closure) {
77+
return null;
78+
}
79+
80+
/** @var Closure $closure */
81+
$closure = $node->getArgs()[1]->value;
82+
83+
if ($closure->getParams() === []) {
84+
return null;
85+
}
86+
87+
$this->refactorParameter($closure->getParams()[0], $node->getArgs()[0]->value);
88+
89+
return $node;
90+
}
91+
92+
private function refactorParameter(Param $param, Node $node): void
93+
{
94+
$nodePhpStanType = $this->nodeTypeResolver->getType($node);
95+
96+
// already set → no change
97+
if ($param->type instanceof Node) {
98+
$currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type);
99+
if ($this->typeComparator->areTypesEqual($currentParamType, $nodePhpStanType)) {
100+
return;
101+
}
102+
}
103+
104+
$paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($nodePhpStanType, TypeKind::PARAM);
105+
$param->type = $paramTypeNode;
106+
}
107+
108+
private function refactorMethodCall(MethodCall $methodCall): ?MethodCall
109+
{
110+
if (! $this->isTappableCall($methodCall)) {
111+
return null;
112+
}
113+
114+
if (! $methodCall->getArgs()[0]->value instanceof Closure) {
115+
return null;
116+
}
117+
118+
/** @var Closure $closure */
119+
$closure = $methodCall->getArgs()[0]->value;
120+
121+
if ($closure->getParams() === []) {
122+
return null;
123+
}
124+
125+
$this->refactorParameter($closure->getParams()[0], $methodCall->var);
126+
127+
return $methodCall;
128+
}
129+
130+
private function isTappableCall(MethodCall $methodCall): bool
131+
{
132+
return $this->isObjectType($methodCall->var, new ObjectType(self::TAPPABLE_TRAIT));
133+
}
134+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Illuminate\Support\Traits;
4+
5+
if (trait_exists('Illuminate\Support\Traits\Tappable')) {
6+
return;
7+
}
8+
9+
trait Tappable
10+
{
11+
public function tap($callback = null)
12+
{
13+
}
14+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;
4+
5+
use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;
6+
7+
$example = new TappableExample();
8+
9+
$example->tap(function ($example) {
10+
11+
});
12+
13+
?>
14+
-----
15+
<?php
16+
17+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;
18+
19+
use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;
20+
21+
$example = new TappableExample();
22+
23+
$example->tap(function (\RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample $example) {
24+
25+
});
26+
27+
?>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;
4+
5+
use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;
6+
7+
tap('test');
8+
9+
$example = new TappableExample();
10+
11+
$example->tap();
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 RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;
4+
5+
use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;
6+
7+
tap('test', function () {});
8+
9+
$example = new TappableExample();
10+
11+
$example->tap(function () {
12+
13+
});
14+
15+
?>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;
4+
5+
nottap('test', function ($string) {});
6+
7+
?>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;
4+
5+
use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\NonTappableExample;
6+
7+
$example = new NonTappableExample();
8+
9+
$example->tap(function ($example) {
10+
11+
});
12+
13+
?>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source;
4+
5+
class NonTappableExample
6+
{
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source;
4+
5+
use Illuminate\Support\Traits\Tappable;
6+
7+
class TappableExample
8+
{
9+
use Tappable;
10+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class TypeHintTappableCallRectorTest extends AbstractRectorTestCase
12+
{
13+
public static function provideData(): Iterator
14+
{
15+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
16+
}
17+
18+
/**
19+
* @test
20+
*/
21+
#[DataProvider('provideData')]
22+
public function test(string $filePath): void
23+
{
24+
$this->doTestFile($filePath);
25+
}
26+
27+
public function provideConfigFilePath(): string
28+
{
29+
return __DIR__ . '/config/configured_rule.php';
30+
}
31+
}
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 RectorLaravel\Rector\FuncCall\TypeHintTappableCallRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->import(__DIR__ . '/../../../../../config/config.php');
10+
11+
$rectorConfig->rule(TypeHintTappableCallRector::class);
12+
};

0 commit comments

Comments
 (0)