diff --git a/docs/rules.md b/docs/rules.md index 653b1d9..3857101 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -1,4 +1,4 @@ -# 60 Rules Overview +# 61 Rules Overview ## ChainExpectCallsRector @@ -30,6 +30,20 @@ Chains consecutive `expect()` calls into a single chained expectation, combining
+## ConvertAndToExpectRector + +Splits `->and()` calls in `expect()` chains into separate `expect()` statements + +- class: [`Pest\Rector\Rules\ConvertAndToExpectRector`](../src/Rules/ConvertAndToExpectRector.php) + +```diff +-expect($a)->toBe(10)->and($b)->toBe(20); ++expect($a)->toBe(10); ++expect($b)->toBe(20); +``` + +
+ ## ConvertAssertToExpectRector Converts `$this->assert*()` calls to Pest `expect()` chains diff --git a/src/AbstractRector.php b/src/AbstractRector.php index 29cc379..6b374e4 100644 --- a/src/AbstractRector.php +++ b/src/AbstractRector.php @@ -29,6 +29,7 @@ use PhpParser\Node\Stmt\TryCatch; use PhpParser\Node\Stmt\While_; use PhpParser\Node\VariadicPlaceholder; +use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\PhpParser\Enum\NodeGroup; use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector as BaseAbstractRector; @@ -281,6 +282,43 @@ protected function rebuildMethodChain(Expr $base, array $methods): Expr return $result; } + protected function applyNewlineAttributes(Expr $chain): void + { + if (! defined(AttributeKey::class.'::NEWLINE_ON_FLUENT_CALL')) { + return; + } + + $current = $chain; + + while ($current instanceof MethodCall) { + $var = $current->var; + + if ($var instanceof FuncCall) { + break; + } + + if ($var instanceof PropertyFetch) { + $current = $var->var; + + continue; + } + + if (! $var instanceof MethodCall) { + break; + } + + if ($this->isName($var->name, 'and')) { + $var->setAttribute(AttributeKey::NEWLINE_ON_FLUENT_CALL, true); + $current = $var->var; + + continue; + } + + $current->setAttribute(AttributeKey::NEWLINE_ON_FLUENT_CALL, true); + $current = $var; + } + } + /** * @param array $sources */ diff --git a/src/Rules/ChainExpectCallsRector.php b/src/Rules/ChainExpectCallsRector.php index f5e7d8f..b4758c8 100644 --- a/src/Rules/ChainExpectCallsRector.php +++ b/src/Rules/ChainExpectCallsRector.php @@ -13,7 +13,6 @@ use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\NullsafePropertyFetch; use PhpParser\Node\Expr\PropertyFetch; @@ -420,41 +419,4 @@ private function mergeDifferentVariableChains(array &$stmts, int $key): bool return true; } - - private function applyNewlineAttributes(Expr $chain): void - { - if (! defined(AttributeKey::class.'::NEWLINE_ON_FLUENT_CALL')) { - return; - } - - $current = $chain; - - while ($current instanceof MethodCall) { - $var = $current->var; - - if ($var instanceof FuncCall) { - break; - } - - if ($var instanceof PropertyFetch) { - $current = $var->var; - - continue; - } - - if (! $var instanceof MethodCall) { - break; - } - - if ($this->isName($var->name, 'and')) { - $var->setAttribute(AttributeKey::NEWLINE_ON_FLUENT_CALL, true); - $current = $var->var; - - continue; - } - - $current->setAttribute(AttributeKey::NEWLINE_ON_FLUENT_CALL, true); - $current = $var; - } - } } diff --git a/src/Rules/ConvertAndToExpectRector.php b/src/Rules/ConvertAndToExpectRector.php new file mode 100644 index 0000000..8576ff3 --- /dev/null +++ b/src/Rules/ConvertAndToExpectRector.php @@ -0,0 +1,119 @@ +and()` calls in expect() chains into separate expect() statements', + [ + new CodeSample( + <<<'CODE_SAMPLE' +expect($a)->toBe(10)->and($b)->toBe(20); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +expect($a)->toBe(10); +expect($b)->toBe(20); +CODE_SAMPLE + ), + ] + ); + } + + // @codeCoverageIgnoreEnd + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Expression::class]; + } + + /** + * @param Expression $node + * @return array|null + */ + public function refactor(Node $node): ?array + { + if (! $node->expr instanceof MethodCall) { + return null; + } + + if (! $this->isExpectChain($node->expr)) { + return null; + } + + $segments = []; + $segmentOutermost = $node->expr; + $current = $node->expr; + $child = null; + + while ($current instanceof MethodCall || $current instanceof PropertyFetch) { + if (! $current instanceof MethodCall || ! $this->isName($current->name, 'and')) { + $child = $current; + $current = $current->var; + + continue; + } + + if (! isset($current->args[0]) || ! $current->args[0] instanceof Arg) { + return null; + } + + $andValue = $current->args[0]->value; + $andValue->setAttribute(AttributeKey::ORIGINAL_NODE, null); + $expectCall = new FuncCall(new Name('expect'), [new Arg($andValue)]); + + if ($child === null) { + $segments[] = $expectCall; + } else { + $child->var = $expectCall; + $segments[] = $segmentOutermost; + } + + $segmentOutermost = $current->var; + $current = $current->var; + $child = null; + } + + if ($segments === []) { + return null; + } + + $segments[] = $segmentOutermost; + $segments = array_reverse($segments); + + $newStmts = []; + foreach ($segments as $index => $segmentExpr) { + if ($index === 0) { + $node->expr = $segmentExpr; + $newStmts[] = $node; + + continue; + } + + $newStmts[] = new Expression($segmentExpr); + } + + return $newStmts; + } +} diff --git a/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/ChainExpectCallsRectorAndConvertAndToExpectTest.php b/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/ChainExpectCallsRectorAndConvertAndToExpectTest.php new file mode 100644 index 0000000..57dd32f --- /dev/null +++ b/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/ChainExpectCallsRectorAndConvertAndToExpectTest.php @@ -0,0 +1,15 @@ +doTestFile($filePath); +})->with( + fn (): Iterator => FixtureFileFinder::yieldDirectory(__DIR__.'/Fixture') +); diff --git a/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/Fixture/and_chain_split_and_rechained.php.inc b/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/Fixture/and_chain_split_and_rechained.php.inc new file mode 100644 index 0000000..d552af2 --- /dev/null +++ b/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/Fixture/and_chain_split_and_rechained.php.inc @@ -0,0 +1,20 @@ +toBe(10)->and($b)->toBe(20)->not->toBeNull(); +expect($b)->toBeInt(); + +?> +----- +toBe(10); +expect($b)->toBe(20)->not->toBeNull() + ->toBeInt(); + +?> diff --git a/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/config/configured_rule.php b/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/config/configured_rule.php new file mode 100644 index 0000000..33fee73 --- /dev/null +++ b/tests/Rules/ChainExpectCallsRectorAndConvertAndToExpect/config/configured_rule.php @@ -0,0 +1,14 @@ +rule(ConvertAndToExpectRector::class); + $rectorConfig->ruleWithConfiguration(ChainExpectCallsRector::class, [ + ChainExpectCallsRector::MERGE_DIFFERENT_VARIABLES => false, + ]); +}; diff --git a/tests/Rules/ConvertAndToExpectRector/ConvertAndToExpectRectorTest.php b/tests/Rules/ConvertAndToExpectRector/ConvertAndToExpectRectorTest.php new file mode 100644 index 0000000..57dd32f --- /dev/null +++ b/tests/Rules/ConvertAndToExpectRector/ConvertAndToExpectRectorTest.php @@ -0,0 +1,15 @@ +doTestFile($filePath); +})->with( + fn (): Iterator => FixtureFileFinder::yieldDirectory(__DIR__.'/Fixture') +); diff --git a/tests/Rules/ConvertAndToExpectRector/Fixture/and_chain_split.php.inc b/tests/Rules/ConvertAndToExpectRector/Fixture/and_chain_split.php.inc new file mode 100644 index 0000000..edcff98 --- /dev/null +++ b/tests/Rules/ConvertAndToExpectRector/Fixture/and_chain_split.php.inc @@ -0,0 +1,18 @@ +toBe(10)->and($b)->toBe(20); + +?> +----- +toBe(10); +expect($b)->toBe(20); + +?> diff --git a/tests/Rules/ConvertAndToExpectRector/Fixture/multiline_chain_formatting_preserved.php.inc b/tests/Rules/ConvertAndToExpectRector/Fixture/multiline_chain_formatting_preserved.php.inc new file mode 100644 index 0000000..eaba2fb --- /dev/null +++ b/tests/Rules/ConvertAndToExpectRector/Fixture/multiline_chain_formatting_preserved.php.inc @@ -0,0 +1,25 @@ +status->toBe('draft') + ->approved_at->toBeNull() + ->riskGroups->toHaveCount(2) + ->and($riskAssessment->status)->toBe('approved'); + +?> +----- +status->toBe('draft') + ->approved_at->toBeNull() + ->riskGroups->toHaveCount(2); +expect($riskAssessment->status)->toBe('approved'); + +?> diff --git a/tests/Rules/ConvertAndToExpectRector/Fixture/multiple_and_segments.php.inc b/tests/Rules/ConvertAndToExpectRector/Fixture/multiple_and_segments.php.inc new file mode 100644 index 0000000..980a370 --- /dev/null +++ b/tests/Rules/ConvertAndToExpectRector/Fixture/multiple_and_segments.php.inc @@ -0,0 +1,21 @@ +toBe(10)->toBeInt()->and($b)->toBe(20)->and($c)->not->toBeInt(); + +?> +----- +toBe(10)->toBeInt(); +expect($b)->toBe(20); +expect($c)->not->toBeInt(); + +?> diff --git a/tests/Rules/ConvertAndToExpectRector/Fixture/no_and_untouched.php.inc b/tests/Rules/ConvertAndToExpectRector/Fixture/no_and_untouched.php.inc new file mode 100644 index 0000000..1c025c7 --- /dev/null +++ b/tests/Rules/ConvertAndToExpectRector/Fixture/no_and_untouched.php.inc @@ -0,0 +1,5 @@ +toBe(10)->toBeInt(); diff --git a/tests/Rules/ConvertAndToExpectRector/config/configured_rule.php b/tests/Rules/ConvertAndToExpectRector/config/configured_rule.php new file mode 100644 index 0000000..b50d1ad --- /dev/null +++ b/tests/Rules/ConvertAndToExpectRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(ConvertAndToExpectRector::class); +};