Skip to content

Commit 4649b0f

Browse files
authored
Assert class to type hinted closure (#321)
* working but needs phpstan * PHPStan fixes
1 parent 7bb8e3c commit 4649b0f

12 files changed

+307
-1
lines changed

Diff for: docs/rector_rules_overview.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 78 Rules Overview
1+
# 79 Rules Overview
22

33
## AbortIfRector
44

@@ -333,6 +333,21 @@ Replace `(new \Illuminate\Testing\TestResponse)->assertStatus(200)` with `(new \
333333

334334
<br>
335335

336+
## AssertWithClassStringToTypeHintedClosureRector
337+
338+
Changes assert calls to use a type hinted closure.
339+
340+
- class: [`RectorLaravel\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector`](../src/Rector/StaticCall/AssertWithClassStringToTypeHintedClosureRector.php)
341+
342+
```diff
343+
-Bus::assertDispatched(OrderCreated::class, function ($job) {
344+
+Bus::assertDispatched(function (OrderCreated $job) {
345+
return true;
346+
});
347+
```
348+
349+
<br>
350+
336351
## AvoidNegatedCollectionContainsOrDoesntContainRector
337352

338353
Convert negated calls to `contains` to `doesntContain`, or vice versa.

Diff for: src/NodeAnalyzer/FacadeAssertionAnalyzer.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace RectorLaravel\NodeAnalyzer;
4+
5+
use PhpParser\Node\Expr\StaticCall;
6+
use PHPStan\Type\ObjectType;
7+
use Rector\NodeNameResolver\NodeNameResolver;
8+
use Rector\NodeTypeResolver\NodeTypeResolver;
9+
10+
final readonly class FacadeAssertionAnalyzer
11+
{
12+
public function __construct(
13+
private NodeTypeResolver $nodeTypeResolver,
14+
private NodeNameResolver $nodeNameResolver,
15+
) {}
16+
17+
public function isFacadeAssertion(StaticCall $staticCall): bool
18+
{
19+
return match (true) {
20+
$this->nodeTypeResolver->isObjectType($staticCall->class, new ObjectType('Illuminate\Support\Facades\Bus'))
21+
&& $this->nodeNameResolver->isName($staticCall->name, 'assertDispatched') => true,
22+
$this->nodeTypeResolver->isObjectType($staticCall->class, new ObjectType('Illuminate\Support\Facades\Event'))
23+
&& $this->nodeNameResolver->isName($staticCall->name, 'assertDispatched') => true,
24+
default => false,
25+
};
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace RectorLaravel\Rector\StaticCall;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr\ArrowFunction;
10+
use PhpParser\Node\Expr\Closure;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Name\FullyQualified;
13+
use RectorLaravel\AbstractRector;
14+
use RectorLaravel\NodeAnalyzer\FacadeAssertionAnalyzer;
15+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
16+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
17+
18+
/**
19+
* @see \RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\AssertWithClassStringToTypeHintedClosureRectorTest
20+
*/
21+
final class AssertWithClassStringToTypeHintedClosureRector extends AbstractRector
22+
{
23+
public function __construct(
24+
private readonly FacadeAssertionAnalyzer $facadeAssertionAnalyzer
25+
) {}
26+
27+
public function getRuleDefinition(): RuleDefinition
28+
{
29+
return new RuleDefinition(
30+
'Changes assert calls to use a type hinted closure.',
31+
[new CodeSample(
32+
<<<'CODE_SAMPLE'
33+
Bus::assertDispatched(OrderCreated::class, function ($job) {
34+
return true;
35+
});
36+
CODE_SAMPLE,
37+
<<<'CODE_SAMPLE'
38+
Bus::assertDispatched(function (OrderCreated $job) {
39+
return true;
40+
});
41+
CODE_SAMPLE
42+
)]
43+
);
44+
}
45+
46+
/**
47+
* @return array<class-string<Node>>
48+
*/
49+
public function getNodeTypes(): array
50+
{
51+
return [StaticCall::class];
52+
}
53+
54+
/**
55+
* @param StaticCall $node
56+
*/
57+
public function refactor(Node $node): ?StaticCall
58+
{
59+
if (! $this->facadeAssertionAnalyzer->isFacadeAssertion($node)) {
60+
return null;
61+
}
62+
63+
if ($node->isFirstClassCallable()) {
64+
return null;
65+
}
66+
67+
if (count($node->getArgs()) !== 2) {
68+
return null;
69+
}
70+
71+
if (! $node->args[1] instanceof Arg) {
72+
return null;
73+
}
74+
75+
$functionLike = $node->args[1]->value;
76+
77+
if (
78+
(! $functionLike instanceof Closure
79+
&& ! $functionLike instanceof ArrowFunction)
80+
|| ! isset($functionLike->params[0])
81+
) {
82+
return null;
83+
}
84+
85+
if (! $node->args[0] instanceof Arg) {
86+
return null;
87+
}
88+
89+
$type = $this->getType($node->args[0]->value);
90+
91+
$classString = match (true) {
92+
/** @phpstan-ignore method.notFound */
93+
$type->isClassString()->yes() => $type->getClassStringObjectType()->getClassName(),
94+
$type->isString()->yes()
95+
/** @phpstan-ignore method.notFound */
96+
&& $type->getClassStringObjectType()->isObject()->yes() => $type->getClassStringObjectType()->getClassName(),
97+
default => null,
98+
};
99+
100+
if (! is_string($classString)) {
101+
return null;
102+
}
103+
104+
return $this->refactorClosure($node, $functionLike, $classString);
105+
}
106+
107+
public function refactorClosure(StaticCall $staticCall, Closure|ArrowFunction $closure, string $class): StaticCall
108+
{
109+
$closure->params[0]->type = new FullyQualified($class);
110+
111+
$staticCall->args = [
112+
new Arg($closure),
113+
];
114+
115+
return $staticCall;
116+
}
117+
}
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\StaticCall\AssertWithClassStringToTypeHintedClosureRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AssertWithClassStringToTypeHintedClosureRectorTest 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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Fixture;
4+
5+
use RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass;
6+
7+
\Illuminate\Support\Facades\Bus::assertDispatched(
8+
SomeClass::class,
9+
function ($job) {
10+
return true;
11+
}
12+
);
13+
14+
\Illuminate\Support\Facades\Bus::assertDispatched(
15+
'RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass',
16+
function ($job) {
17+
return true;
18+
}
19+
);
20+
21+
\Illuminate\Support\Facades\Bus::assertDispatched(
22+
SomeClass::class,
23+
fn ($job) => true,
24+
);
25+
26+
?>
27+
-----
28+
<?php
29+
30+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Fixture;
31+
32+
use RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass;
33+
34+
\Illuminate\Support\Facades\Bus::assertDispatched(function (\RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass $job) {
35+
return true;
36+
});
37+
38+
\Illuminate\Support\Facades\Bus::assertDispatched(function (\RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass $job) {
39+
return true;
40+
});
41+
42+
\Illuminate\Support\Facades\Bus::assertDispatched(fn (\RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass $job) => true);
43+
44+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Fixture;
4+
5+
\Illuminate\Support\Facades\Bus::assertDispatched(...);
6+
7+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Fixture;
4+
5+
\Illuminate\Support\Facades\Bus::assertDispatched(
6+
'RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass',
7+
function () {}
8+
);
9+
10+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Fixture;
4+
5+
\Illuminate\Support\Facades\Bus::assertDispatched(
6+
'RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass',
7+
);
8+
9+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Fixture;
4+
5+
\Illuminate\Support\Facades\Bus::assertDispatched(
6+
'FooBar',
7+
function ($job) {
8+
return true;
9+
}
10+
);
11+
12+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Fixture;
4+
5+
\Illuminate\Support\Facades\FooBar::assertDispatched(
6+
'RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass',
7+
function ($job) {
8+
return true;
9+
}
10+
);
11+
12+
\Illuminate\Support\Facades\Bus::assertDispatchedFoo(
13+
'RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source\SomeClass',
14+
function ($job) {
15+
return true;
16+
}
17+
);
18+
19+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace RectorLaravel\Tests\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector\Source;
4+
5+
class SomeClass {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use RectorLaravel\Rector\StaticCall\AssertWithClassStringToTypeHintedClosureRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(AssertWithClassStringToTypeHintedClosureRector::class);
10+
};

0 commit comments

Comments
 (0)