Skip to content

Commit 8b9e885

Browse files
authored
Add new Rector rules and improve compatibility (#6)
1 parent bac5190 commit 8b9e885

35 files changed

+1025
-3
lines changed

.github/workflows/validate.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- highest
3838

3939
steps:
40-
- uses: actions/checkout@master
40+
- uses: actions/checkout@v4
4141

4242
- uses: shivammathur/setup-php@v2
4343
with:
@@ -50,3 +50,31 @@ jobs:
5050
dependency-versions: ${{ matrix.dependencies }}
5151

5252
- run: vendor/bin/phpstan analyse --configuration=phpstan.neon
53+
54+
tests:
55+
runs-on: ubuntu-latest
56+
57+
strategy:
58+
fail-fast: false
59+
matrix:
60+
php-version:
61+
- "8.3"
62+
- "8.4"
63+
dependencies:
64+
- lowest
65+
- highest
66+
67+
steps:
68+
- uses: actions/checkout@v4
69+
70+
- uses: shivammathur/setup-php@v2
71+
with:
72+
coverage: none
73+
extensions: mbstring
74+
php-version: "${{ matrix.php-version }}"
75+
76+
- uses: ramsey/composer-install@v3
77+
with:
78+
dependency-versions: "${{ matrix.dependencies }}"
79+
80+
- run: vendor/bin/phpunit

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea
2+
.phpunit.cache
23
vendor
34
composer.lock
45
.php-cs-fixer.cache

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ You can find and compare releases at the [GitHub release page](https://github.co
99

1010
## Unreleased
1111

12+
## v3.2.0
13+
14+
### Added
15+
16+
- Add `IfThrowToCoalesceThrowRector` to transform null-check-then-throw patterns to use the null coalesce operator with throw expression https://github.com/mll-lab/rector-config/pull/6
17+
- Add `ElvisToCoalesceRector` to convert elvis operator (`?:`) to null coalesce (`??`) when the expression can only be falsy via null https://github.com/mll-lab/rector-config/pull/6
18+
19+
### Changed
20+
21+
- Select `FirstClassCallableRector` or `ArrayToFirstClassCallableRector` based on Rector version https://github.com/mll-lab/rector-config/pull/6
22+
23+
### Fixed
24+
25+
- Rector 1.x compatibility for `IfThrowToCoalesceThrowRector` https://github.com/mll-lab/rector-config/pull/6
26+
1227
## v3.1.1
1328

1429
### Removed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@ normalize: ## Normalize composer.json
2121
stan: vendor ## Runs a static analysis with phpstan
2222
vendor/bin/phpstan analyse --configuration=phpstan.neon
2323

24+
.PHONY: test
25+
test: vendor ## Run tests
26+
vendor/bin/phpunit
27+
2428
vendor: composer.json ## Install dependencies through composer
2529
composer update

composer.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,22 @@
2222
"laravel/framework": "^11 || ^12",
2323
"mll-lab/php-cs-fixer-config": "^5",
2424
"phpstan/extension-installer": "^1",
25-
"phpstan/phpstan": "^1.12 || ^2.1"
25+
"phpstan/phpstan": "^1.12 || ^2.1",
26+
"phpunit/phpunit": "^11"
2627
},
2728
"autoload": {
29+
"psr-4": {
30+
"MLL\\RectorConfig\\": "src/"
31+
},
2832
"files": [
2933
"config.php"
3034
]
3135
},
36+
"autoload-dev": {
37+
"psr-4": {
38+
"MLL\\RectorConfig\\Tests\\": "tests/"
39+
}
40+
},
3241
"config": {
3342
"allow-plugins": {
3443
"ergebnis/composer-normalize": true,

config.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
/** Configure rector with PHP rules. */
88
function config(RectorConfig $rectorConfig): void
99
{
10-
$rectorConfig->rule(\Rector\Php81\Rector\Array_\FirstClassCallableRector::class);
10+
// Use ArrayToFirstClassCallableRector in Rector 2.x, FirstClassCallableRector in 1.x
11+
/** @var class-string<\Rector\Contract\Rector\RectorInterface> $firstClassCallableRector */
12+
$firstClassCallableRector = class_exists(\Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector::class)
13+
? \Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector::class
14+
: \Rector\Php81\Rector\Array_\FirstClassCallableRector::class;
15+
$rectorConfig->rule($firstClassCallableRector);
16+
17+
$rectorConfig->rule(\MLL\RectorConfig\Rector\ElvisToCoalesceRector::class);
18+
$rectorConfig->rule(\MLL\RectorConfig\Rector\IfThrowToCoalesceThrowRector::class);
1119
}
1220

1321
/** Configure rector with Laravel rules. */

phpunit.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
cacheDirectory=".phpunit.cache">
7+
<testsuites>
8+
<testsuite name="Test Suite">
9+
<directory>tests</directory>
10+
</testsuite>
11+
</testsuites>
12+
</phpunit>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\RectorConfig\Rector;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\BinaryOp\Coalesce;
7+
use PhpParser\Node\Expr\Ternary;
8+
use Rector\Rector\AbstractRector;
9+
use Rector\ValueObject\PhpVersion;
10+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
11+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
12+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
13+
14+
/**
15+
* @see \MLL\RectorConfig\Tests\Rector\ElvisToCoalesceRector\ElvisToCoalesceRectorTest
16+
*/
17+
final class ElvisToCoalesceRector extends AbstractRector implements MinPhpVersionInterface
18+
{
19+
use NullFalsyTypeTrait;
20+
21+
public function getRuleDefinition(): RuleDefinition
22+
{
23+
return new RuleDefinition('Convert elvis operator (?:) to null coalesce (??) when expression type can only be falsy via null', [
24+
new CodeSample(
25+
<<<'CODE_SAMPLE'
26+
$object = $this->fetchNullableObject() ?: throw new \RuntimeException();
27+
CODE_SAMPLE,
28+
<<<'CODE_SAMPLE'
29+
$object = $this->fetchNullableObject() ?? throw new \RuntimeException();
30+
CODE_SAMPLE,
31+
),
32+
]);
33+
}
34+
35+
/** @return array<class-string<Node>> */
36+
public function getNodeTypes(): array
37+
{
38+
return [Ternary::class];
39+
}
40+
41+
/** @param Ternary $node */
42+
public function refactor(Node $node): ?Node
43+
{
44+
if ($node->if !== null) {
45+
return null;
46+
}
47+
48+
if (! $this->isOnlyNullFalsy($node->cond)) {
49+
return null;
50+
}
51+
52+
return new Coalesce($node->cond, $node->else);
53+
}
54+
55+
public function provideMinPhpVersion(): int
56+
{
57+
return PhpVersion::PHP_70;
58+
}
59+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\RectorConfig\Rector;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\Assign;
8+
use PhpParser\Node\Expr\BinaryOp\Coalesce;
9+
use PhpParser\Node\Expr\BinaryOp\Equal;
10+
use PhpParser\Node\Expr\BinaryOp\Identical;
11+
use PhpParser\Node\Expr\BooleanNot;
12+
use PhpParser\Node\Expr\Ternary;
13+
use PhpParser\Node\Expr\Throw_ as ExprThrow;
14+
use PhpParser\Node\Expr\Variable;
15+
use PhpParser\Node\Stmt\ClassMethod;
16+
use PhpParser\Node\Stmt\Expression;
17+
use PhpParser\Node\Stmt\Function_;
18+
use PhpParser\Node\Stmt\If_;
19+
use PhpParser\Node\Stmt\Throw_ as StmtThrow;
20+
use Rector\PhpParser\Node\Value\ValueResolver;
21+
use Rector\Rector\AbstractRector;
22+
use Rector\ValueObject\PhpVersion;
23+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
24+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
25+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
26+
27+
/**
28+
* @see \MLL\RectorConfig\Tests\Rector\IfThrowToCoalesceThrowRector\IfThrowToCoalesceThrowRectorTest
29+
*/
30+
final class IfThrowToCoalesceThrowRector extends AbstractRector implements MinPhpVersionInterface
31+
{
32+
use NullFalsyTypeTrait;
33+
34+
public function __construct(
35+
private readonly ValueResolver $valueResolver,
36+
) {}
37+
38+
public function getRuleDefinition(): RuleDefinition
39+
{
40+
return new RuleDefinition('Transform if-null/falsy-throw patterns to coalesce throw expressions', [
41+
new CodeSample(
42+
<<<'CODE_SAMPLE'
43+
$result = $this->fetchNullable();
44+
if ($result === null) {
45+
throw new NullException;
46+
}
47+
CODE_SAMPLE,
48+
<<<'CODE_SAMPLE'
49+
$result = $this->fetchNullable()
50+
?? throw new NullException;
51+
CODE_SAMPLE,
52+
),
53+
new CodeSample(
54+
<<<'CODE_SAMPLE'
55+
$result = $this->fetchMaybeFalsy();
56+
if (! $result) {
57+
throw new FalsyException;
58+
}
59+
CODE_SAMPLE,
60+
<<<'CODE_SAMPLE'
61+
$result = $this->fetchMaybeFalsy()
62+
?: throw new FalsyException;
63+
CODE_SAMPLE,
64+
),
65+
]);
66+
}
67+
68+
/** @return array<class-string<Node>> */
69+
public function getNodeTypes(): array
70+
{
71+
return [ClassMethod::class, Function_::class];
72+
}
73+
74+
/** @param ClassMethod|Function_ $node */
75+
public function refactor(Node $node): ?Node
76+
{
77+
if ($node->stmts === null) {
78+
return null;
79+
}
80+
81+
$hasChanged = false;
82+
83+
foreach ($node->stmts as $key => $stmt) {
84+
if (! $stmt instanceof Expression) {
85+
continue;
86+
}
87+
88+
$expr = $stmt->expr;
89+
if (! $expr instanceof Assign) {
90+
continue;
91+
}
92+
93+
if (! $expr->var instanceof Variable) {
94+
continue;
95+
}
96+
97+
$nextStmt = $node->stmts[$key + 1] ?? null;
98+
if (! $nextStmt instanceof If_) {
99+
continue;
100+
}
101+
102+
if ($nextStmt->else !== null || $nextStmt->elseifs !== []) {
103+
continue;
104+
}
105+
106+
if (count($nextStmt->stmts) !== 1) {
107+
continue;
108+
}
109+
110+
$ifStmt = $nextStmt->stmts[0];
111+
112+
$throwExpr = null;
113+
if ($ifStmt instanceof StmtThrow) {
114+
$throwExpr = new ExprThrow($ifStmt->expr);
115+
} elseif ($ifStmt instanceof Expression
116+
&& $ifStmt->expr instanceof ExprThrow
117+
) {
118+
$throwExpr = $ifStmt->expr;
119+
}
120+
121+
if ($throwExpr === null) {
122+
continue;
123+
}
124+
125+
$result = $this->matchCoalescePattern($expr, $nextStmt, $throwExpr);
126+
if ($result === null) {
127+
continue;
128+
}
129+
130+
$expr->expr = $result;
131+
unset($node->stmts[$key + 1]);
132+
$hasChanged = true;
133+
}
134+
135+
return $hasChanged
136+
? $node
137+
: null;
138+
}
139+
140+
public function provideMinPhpVersion(): int
141+
{
142+
return PhpVersion::PHP_80;
143+
}
144+
145+
private function matchCoalescePattern(Assign $assign, If_ $if, ExprThrow $throw): ?Expr
146+
{
147+
$variable = $assign->var;
148+
$condition = $if->cond;
149+
150+
if (($condition instanceof Identical || $condition instanceof Equal)
151+
&& $this->isNullComparison($condition, $variable)
152+
) {
153+
return new Coalesce($assign->expr, $throw);
154+
}
155+
156+
if ($condition instanceof BooleanNot
157+
&& $this->nodeComparator->areNodesEqual($condition->expr, $variable)
158+
) {
159+
if ($this->isOnlyNullFalsy($assign->expr)) {
160+
return new Coalesce($assign->expr, $throw);
161+
}
162+
163+
return new Ternary($assign->expr, null, $throw);
164+
}
165+
166+
return null;
167+
}
168+
169+
private function isNullComparison(Identical|Equal $comparison, Variable $variable): bool
170+
{
171+
$left = $comparison->left;
172+
$right = $comparison->right;
173+
174+
if ($this->nodeComparator->areNodesEqual($left, $variable)
175+
&& $this->valueResolver->isNull($right)
176+
) {
177+
return true;
178+
}
179+
180+
return $this->valueResolver->isNull($left)
181+
&& $this->nodeComparator->areNodesEqual($right, $variable);
182+
}
183+
}

0 commit comments

Comments
 (0)