diff --git a/rules-tests/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector/Fixture/skip_never_return_call_after_bool_returns.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector/Fixture/skip_never_return_call_after_bool_returns.php.inc new file mode 100644 index 00000000..11e2138f --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector/Fixture/skip_never_return_call_after_bool_returns.php.inc @@ -0,0 +1,26 @@ +exactly(2); + + $eventMock = $this->createMock(\stdClass::class); + $eventMock->expects($matcher)->method('checkContext')->willReturnCallback(function (string $id) use ($matcher) { + if ($id === 'service') { + return true; + } + + if ($id === 'other') { + return false; + } + + self::fail('Unknown ID requested: ' . $id); + }); + } +} diff --git a/rules-tests/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector/Fixture/skip_throw_after_bool_returns.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector/Fixture/skip_throw_after_bool_returns.php.inc new file mode 100644 index 00000000..693507a7 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector/Fixture/skip_throw_after_bool_returns.php.inc @@ -0,0 +1,26 @@ +exactly(2); + + $eventMock = $this->createMock(\stdClass::class); + $eventMock->expects($matcher)->method('checkContext')->willReturnCallback(function (string $id) use ($matcher) { + if ($id === 'service') { + return true; + } + + if ($id === 'other') { + return false; + } + + throw new \Exception(); + }); + } +} diff --git a/rules/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector.php b/rules/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector.php index 6b444601..1bb6c3f1 100644 --- a/rules/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector.php +++ b/rules/CodeQuality/Rector/MethodCall/WillReturnCallbackFallbackToReturnFalseRector.php @@ -5,11 +5,16 @@ namespace Rector\PHPUnit\CodeQuality\Rector\MethodCall; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\Throw_; +use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Return_; +use PHPStan\Type\NeverType; use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; use Rector\Rector\AbstractRector; @@ -132,6 +137,12 @@ public function refactor(Node $node): ?MethodCall return null; } + // a closure that always throws or ends in a never-returning call + // cannot fall through, so no "return false" fallback is needed + if ($lastStmt instanceof Stmt && $this->isTerminated($lastStmt)) { + return null; + } + if (! $this->hasOnlyBoolReturns($closure)) { return null; } @@ -141,6 +152,23 @@ public function refactor(Node $node): ?MethodCall return $node; } + private function isTerminated(Stmt $stmt): bool + { + if ($stmt instanceof Expression) { + $stmt = $stmt->expr; + } + + if ($stmt instanceof Throw_) { + return true; + } + + if (! $stmt instanceof Expr) { + return false; + } + + return $this->nodeTypeResolver->getNativeType($stmt) instanceof NeverType; + } + private function hasOnlyBoolReturns(Closure $closure): bool { /** @var Return_[] $returns */