From f47abea8ff9b40c6ffc617f49d0e37c2db64c039 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Jul 2026 12:21:06 +0200 Subject: [PATCH 1/4] [CodeQuality] Rename and refocus rule to flip void-method with(callback) to willReturnCallback --- config/sets/phpunit-code-quality.php | 4 +- .../Fixture/return_throw_and_null.php.inc | 53 -------------- .../Fixture/will_return_callback_void.php.inc | 43 ------------ .../config/configured_rule.php | 10 --- .../Fixture/skip_already_void.php.inc | 4 +- .../Fixture/skip_non_void_method.php.inc | 4 +- .../Fixture/skip_will_return_callback.php.inc | 20 ++++++ .../skip_will_return_callback_throw.php.inc | 24 +++++++ .../Fixture/with_callback_void_setup.php.inc | 14 ++-- .../Source/SomeEntityManager.php | 2 +- ...allbackToWillReturnCallbackRectorTest.php} | 4 +- .../config/configured_rule.php | 10 +++ ...ithCallbackToWillReturnCallbackRector.php} | 70 ++++++++++--------- 13 files changed, 106 insertions(+), 156 deletions(-) delete mode 100644 rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/return_throw_and_null.php.inc delete mode 100644 rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/will_return_callback_void.php.inc delete mode 100644 rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/config/configured_rule.php rename rules-tests/CodeQuality/Rector/Class_/{RemoveReturnFromVoidMethodMockCallbackRector => VoidMethodWithCallbackToWillReturnCallbackRector}/Fixture/skip_already_void.php.inc (57%) rename rules-tests/CodeQuality/Rector/Class_/{RemoveReturnFromVoidMethodMockCallbackRector => VoidMethodWithCallbackToWillReturnCallbackRector}/Fixture/skip_non_void_method.php.inc (56%) create mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc rename rules-tests/CodeQuality/Rector/Class_/{RemoveReturnFromVoidMethodMockCallbackRector => VoidMethodWithCallbackToWillReturnCallbackRector}/Fixture/with_callback_void_setup.php.inc (62%) rename rules-tests/CodeQuality/Rector/Class_/{RemoveReturnFromVoidMethodMockCallbackRector => VoidMethodWithCallbackToWillReturnCallbackRector}/Source/SomeEntityManager.php (66%) rename rules-tests/CodeQuality/Rector/Class_/{RemoveReturnFromVoidMethodMockCallbackRector/RemoveReturnFromVoidMethodMockCallbackRectorTest.php => VoidMethodWithCallbackToWillReturnCallbackRector/VoidMethodWithCallbackToWillReturnCallbackRectorTest.php} (72%) create mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/config/configured_rule.php rename rules/CodeQuality/Rector/Class_/{RemoveReturnFromVoidMethodMockCallbackRector.php => VoidMethodWithCallbackToWillReturnCallbackRector.php} (84%) diff --git a/config/sets/phpunit-code-quality.php b/config/sets/phpunit-code-quality.php index e37c210b..f5789b3e 100644 --- a/config/sets/phpunit-code-quality.php +++ b/config/sets/phpunit-code-quality.php @@ -12,10 +12,10 @@ use Rector\PHPUnit\CodeQuality\Rector\Class_\NarrowUnusedSetUpDefinedPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\RemoveNeverUsedMockPropertyRector; -use Rector\PHPUnit\CodeQuality\Rector\Class_\RemoveReturnFromVoidMethodMockCallbackRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\SingleMockPropertyTypeRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\TestWithToDataProviderRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\TypeWillReturnCallableArrowFunctionRector; +use Rector\PHPUnit\CodeQuality\Rector\Class_\VoidMethodWithCallbackToWillReturnCallbackRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\AddInstanceofAssertForNullableArgumentRector; use Rector\PHPUnit\CodeQuality\Rector\ClassMethod\AddInstanceofAssertForNullableInstanceRector; @@ -94,7 +94,7 @@ // type declarations TypeWillReturnCallableArrowFunctionRector::class, - RemoveReturnFromVoidMethodMockCallbackRector::class, + VoidMethodWithCallbackToWillReturnCallbackRector::class, StringCastAssertStringContainsStringRector::class, AddParamTypeFromDependsRector::class, AddReturnTypeToDependedRector::class, diff --git a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/return_throw_and_null.php.inc b/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/return_throw_and_null.php.inc deleted file mode 100644 index ceb9faad..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/return_throw_and_null.php.inc +++ /dev/null @@ -1,53 +0,0 @@ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) use ($matcher) { - if (1 === $matcher->numberOfInvocations()) { - return null; - } - - if (2 === $matcher->numberOfInvocations()) { - return throw new \RuntimeException('boom'); - } - }); - } -} - -?> ------ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) use ($matcher): void { - if (1 === $matcher->numberOfInvocations()) { - return; - } - - if (2 === $matcher->numberOfInvocations()) { - throw new \RuntimeException('boom'); - } - }); - } -} - -?> diff --git a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/will_return_callback_void.php.inc b/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/will_return_callback_void.php.inc deleted file mode 100644 index fc89107b..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/will_return_callback_void.php.inc +++ /dev/null @@ -1,43 +0,0 @@ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) { - echo $entity; - - return true; - }); - } -} - -?> ------ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity): void { - echo $entity; - }); - } -} - -?> diff --git a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/config/configured_rule.php deleted file mode 100644 index edbb914a..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/config/configured_rule.php +++ /dev/null @@ -1,10 +0,0 @@ -rule(RemoveReturnFromVoidMethodMockCallbackRector::class); -}; diff --git a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/skip_already_void.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_already_void.php.inc similarity index 57% rename from rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/skip_already_void.php.inc rename to rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_already_void.php.inc index d7ed8c45..70940d3e 100644 --- a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/skip_already_void.php.inc +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_already_void.php.inc @@ -1,9 +1,9 @@ createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity) { + echo $entity; + + return true; + }); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc new file mode 100644 index 00000000..37a5a8ac --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc @@ -0,0 +1,24 @@ +createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity) use ($matcher) { + if (1 === $matcher->numberOfInvocations()) { + return null; + } + + if (2 === $matcher->numberOfInvocations()) { + return throw new \RuntimeException('boom'); + } + }); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/with_callback_void_setup.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/with_callback_void_setup.php.inc similarity index 62% rename from rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/with_callback_void_setup.php.inc rename to rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/with_callback_void_setup.php.inc index 8cb75416..158da771 100644 --- a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Fixture/with_callback_void_setup.php.inc +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/with_callback_void_setup.php.inc @@ -1,10 +1,10 @@ entityManager ->method('persist') - ->with($this->callback(function ($entity): bool { + ->with($this->callback(function ($entity) { $this->assertInstanceOf(\stdClass::class, $entity); return true; @@ -31,11 +31,11 @@ final class WithCallbackVoidSetup extends TestCase ----- entityManager ->method('persist') - ->with($this->callback(function ($entity): void { + ->willReturnCallback(function ($entity) { $this->assertInstanceOf(\stdClass::class, $entity); - })); + }); } } diff --git a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Source/SomeEntityManager.php b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Source/SomeEntityManager.php similarity index 66% rename from rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Source/SomeEntityManager.php rename to rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Source/SomeEntityManager.php index 0762ff09..955b86be 100644 --- a/rules-tests/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector/Source/SomeEntityManager.php +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Source/SomeEntityManager.php @@ -1,6 +1,6 @@ rule(VoidMethodWithCallbackToWillReturnCallbackRector::class); +}; diff --git a/rules/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector.php b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php similarity index 84% rename from rules/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector.php rename to rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php index ca8a4c3d..4a28155f 100644 --- a/rules/CodeQuality/Rector/Class_/RemoveReturnFromVoidMethodMockCallbackRector.php +++ b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php @@ -5,6 +5,7 @@ namespace Rector\PHPUnit\CodeQuality\Rector\Class_; use PhpParser\Node; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; @@ -39,9 +40,9 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; /** - * @see \Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\RemoveReturnFromVoidMethodMockCallbackRector\RemoveReturnFromVoidMethodMockCallbackRectorTest + * @see \Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\VoidMethodWithCallbackToWillReturnCallbackRector\VoidMethodWithCallbackToWillReturnCallbackRectorTest */ -final class RemoveReturnFromVoidMethodMockCallbackRector extends AbstractRector +final class VoidMethodWithCallbackToWillReturnCallbackRector extends AbstractRector { public function __construct( private readonly TestsNodeAnalyzer $testsNodeAnalyzer, @@ -54,7 +55,7 @@ public function __construct( public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Drop the value return and type the closure as void, when a mocked callback stubs a void method', + 'Flip a ->with($this->callback(...)) matcher on a void mocked method to a ->willReturnCallback() call, dropping the value return', [ new CodeSample( <<<'CODE_SAMPLE' @@ -66,11 +67,11 @@ public function test() { $this->createMock(SomeClass::class) ->method('run') - ->willReturnCallback(function ($arg) { + ->with($this->callback(function ($arg) { echo $arg; return true; - }); + })); } } @@ -91,7 +92,7 @@ public function test() { $this->createMock(SomeClass::class) ->method('run') - ->willReturnCallback(function ($arg): void { + ->willReturnCallback(function ($arg) { echo $arg; }); } @@ -144,7 +145,11 @@ public function refactor(Node $node): ?Class_ return null; } - $closure = $this->matchInnerClosure($subNode); + if (! $this->isName($subNode->name, 'with')) { + return null; + } + + $closure = $this->matchCallbackClosure($subNode); if (! $closure instanceof Closure) { return null; } @@ -153,10 +158,16 @@ public function refactor(Node $node): ?Class_ return null; } - if ($this->refactorClosureToVoid($closure)) { - $hasChanged = true; + if (! $this->removeValueReturns($closure)) { + return null; } + // flip ->with($this->callback($closure)) to ->willReturnCallback($closure) + $subNode->name = new Identifier('willReturnCallback'); + $subNode->args = [new Arg($closure)]; + + $hasChanged = true; + return null; }); @@ -167,32 +178,25 @@ public function refactor(Node $node): ?Class_ return $node; } - private function matchInnerClosure(MethodCall $methodCall): ?Closure + private function matchCallbackClosure(MethodCall $withMethodCall): ?Closure { - if ($this->isName($methodCall->name, 'willReturnCallback')) { - $innerArg = $methodCall->getArgs()[0]; - if ($innerArg->value instanceof Closure) { - return $innerArg->value; - } - + $withFirstArg = $withMethodCall->getArgs()[0] ?? null; + if (! $withFirstArg instanceof Arg) { return null; } - if ($this->isName($methodCall->name, 'with')) { - $withFirstArg = $methodCall->getArgs()[0]; - if (! $withFirstArg->value instanceof MethodCall) { - return null; - } + if (! $withFirstArg->value instanceof MethodCall) { + return null; + } - $nestedMethodCall = $withFirstArg->value; - if (! $this->isName($nestedMethodCall->name, 'callback')) { - return null; - } + $nestedMethodCall = $withFirstArg->value; + if (! $this->isName($nestedMethodCall->name, 'callback')) { + return null; + } - $nestedArg = $nestedMethodCall->getArgs()[0]; - if ($nestedArg->value instanceof Closure) { - return $nestedArg->value; - } + $nestedArg = $nestedMethodCall->getArgs()[0] ?? null; + if ($nestedArg instanceof Arg && $nestedArg->value instanceof Closure) { + return $nestedArg->value; } return null; @@ -254,7 +258,7 @@ private function isMockedVoidMethodCall( return $paramTypesAndReturnType->getReturnType() instanceof VoidType; } - private function refactorClosureToVoid(Closure $closure): bool + private function removeValueReturns(Closure $closure): bool { $nodeFinder = new NodeFinder(); @@ -272,7 +276,7 @@ private function refactorClosureToVoid(Closure $closure): bool $valueReturns = array_filter($returns, static fn (Return_ $return): bool => $return->expr instanceof Expr); - // nothing to drop, typing void is left to TypeWillReturnCallableArrowFunctionRector + // nothing to drop, keep the callback matcher as is if ($valueReturns === []) { return false; } @@ -299,7 +303,7 @@ private function refactorClosureToVoid(Closure $closure): bool return null; } - // "return throw new X;" is itself invalid in a void function, unwrap to "throw new X;" + // "return throw new X;" carries no value, unwrap to a bare "throw new X;" if ($subNode->expr instanceof Throw_) { return new Expression($subNode->expr); } @@ -317,8 +321,6 @@ private function refactorClosureToVoid(Closure $closure): bool array_pop($closure->stmts); } - $closure->returnType = new Identifier('void'); - return true; } From 59ba4a11fb397e063d931494ce3839ca934b4d96 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Jul 2026 12:22:49 +0200 Subject: [PATCH 2/4] Type flipped callback as void, matching the mocked void method --- .../Fixture/with_callback_void_setup.php.inc | 2 +- .../VoidMethodWithCallbackToWillReturnCallbackRector.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/with_callback_void_setup.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/with_callback_void_setup.php.inc index 158da771..42297965 100644 --- a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/with_callback_void_setup.php.inc +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/with_callback_void_setup.php.inc @@ -50,7 +50,7 @@ final class WithCallbackVoidSetup extends TestCase { $this->entityManager ->method('persist') - ->willReturnCallback(function ($entity) { + ->willReturnCallback(function ($entity): void { $this->assertInstanceOf(\stdClass::class, $entity); }); } diff --git a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php index 4a28155f..769718e8 100644 --- a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php +++ b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php @@ -92,7 +92,7 @@ public function test() { $this->createMock(SomeClass::class) ->method('run') - ->willReturnCallback(function ($arg) { + ->willReturnCallback(function ($arg): void { echo $arg; }); } @@ -321,6 +321,9 @@ private function removeValueReturns(Closure $closure): bool array_pop($closure->stmts); } + // the mocked method returns void, so type the callback the same way + $closure->returnType = new Identifier('void'); + return true; } From f7e092ea6d6e17a7dc0f338412d419d00cc363cb Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Jul 2026 12:27:50 +0200 Subject: [PATCH 3/4] Also handle plain willReturnCallback closures on void methods, not only with(callback) --- .../Fixture/return_throw_and_null.php.inc | 53 +++++++++++++++++++ .../Fixture/skip_will_return_callback.php.inc | 20 ------- .../skip_will_return_callback_throw.php.inc | 24 --------- .../Fixture/will_return_callback_void.php.inc | 43 +++++++++++++++ ...WithCallbackToWillReturnCallbackRector.php | 26 +++++++-- 5 files changed, 117 insertions(+), 49 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc delete mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc delete mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc new file mode 100644 index 00000000..5c6e2369 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc @@ -0,0 +1,53 @@ +createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity) use ($matcher) { + if (1 === $matcher->numberOfInvocations()) { + return null; + } + + if (2 === $matcher->numberOfInvocations()) { + return throw new \RuntimeException('boom'); + } + }); + } +} + +?> +----- +createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity) use ($matcher): void { + if (1 === $matcher->numberOfInvocations()) { + return; + } + + if (2 === $matcher->numberOfInvocations()) { + throw new \RuntimeException('boom'); + } + }); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc deleted file mode 100644 index 5e8ac366..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc +++ /dev/null @@ -1,20 +0,0 @@ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) { - echo $entity; - - return true; - }); - } -} diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc deleted file mode 100644 index 37a5a8ac..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc +++ /dev/null @@ -1,24 +0,0 @@ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) use ($matcher) { - if (1 === $matcher->numberOfInvocations()) { - return null; - } - - if (2 === $matcher->numberOfInvocations()) { - return throw new \RuntimeException('boom'); - } - }); - } -} diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc new file mode 100644 index 00000000..d05c1e40 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc @@ -0,0 +1,43 @@ +createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity) { + echo $entity; + + return true; + }); + } +} + +?> +----- +createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity): void { + echo $entity; + }); + } +} + +?> diff --git a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php index 769718e8..b93d5b96 100644 --- a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php +++ b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php @@ -55,7 +55,7 @@ public function __construct( public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Flip a ->with($this->callback(...)) matcher on a void mocked method to a ->willReturnCallback() call, dropping the value return', + 'On a void mocked method, drop the value return from a callback and type it as void; a ->with($this->callback(...)) matcher is also flipped to a ->willReturnCallback() call', [ new CodeSample( <<<'CODE_SAMPLE' @@ -145,11 +145,15 @@ public function refactor(Node $node): ?Class_ return null; } - if (! $this->isName($subNode->name, 'with')) { + $isWithCallback = $this->isName($subNode->name, 'with'); + $isWillReturnCallback = $this->isName($subNode->name, 'willReturnCallback'); + if (! $isWithCallback && ! $isWillReturnCallback) { return null; } - $closure = $this->matchCallbackClosure($subNode); + $closure = $isWithCallback ? $this->matchCallbackClosure($subNode) : $this->matchDirectClosure( + $subNode + ); if (! $closure instanceof Closure) { return null; } @@ -163,8 +167,10 @@ public function refactor(Node $node): ?Class_ } // flip ->with($this->callback($closure)) to ->willReturnCallback($closure) - $subNode->name = new Identifier('willReturnCallback'); - $subNode->args = [new Arg($closure)]; + if ($isWithCallback) { + $subNode->name = new Identifier('willReturnCallback'); + $subNode->args = [new Arg($closure)]; + } $hasChanged = true; @@ -178,6 +184,16 @@ public function refactor(Node $node): ?Class_ return $node; } + private function matchDirectClosure(MethodCall $willReturnCallbackMethodCall): ?Closure + { + $firstArg = $willReturnCallbackMethodCall->getArgs()[0] ?? null; + if ($firstArg instanceof Arg && $firstArg->value instanceof Closure) { + return $firstArg->value; + } + + return null; + } + private function matchCallbackClosure(MethodCall $withMethodCall): ?Closure { $withFirstArg = $withMethodCall->getArgs()[0] ?? null; From f336407c51227211425999c2f76bce83b6587e16 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Jul 2026 12:30:31 +0200 Subject: [PATCH 4/4] Handle only with(callback) form; leave plain willReturnCallback untouched --- .../Fixture/return_throw_and_null.php.inc | 53 ------------------- .../Fixture/skip_will_return_callback.php.inc | 20 +++++++ .../skip_will_return_callback_throw.php.inc | 24 +++++++++ .../Fixture/will_return_callback_void.php.inc | 43 --------------- ...WithCallbackToWillReturnCallbackRector.php | 26 ++------- 5 files changed, 49 insertions(+), 117 deletions(-) delete mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc delete mode 100644 rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc deleted file mode 100644 index 5c6e2369..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/return_throw_and_null.php.inc +++ /dev/null @@ -1,53 +0,0 @@ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) use ($matcher) { - if (1 === $matcher->numberOfInvocations()) { - return null; - } - - if (2 === $matcher->numberOfInvocations()) { - return throw new \RuntimeException('boom'); - } - }); - } -} - -?> ------ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) use ($matcher): void { - if (1 === $matcher->numberOfInvocations()) { - return; - } - - if (2 === $matcher->numberOfInvocations()) { - throw new \RuntimeException('boom'); - } - }); - } -} - -?> diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc new file mode 100644 index 00000000..5e8ac366 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback.php.inc @@ -0,0 +1,20 @@ +createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity) { + echo $entity; + + return true; + }); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc new file mode 100644 index 00000000..37a5a8ac --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/skip_will_return_callback_throw.php.inc @@ -0,0 +1,24 @@ +createMock(SomeEntityManager::class) + ->method('persist') + ->willReturnCallback(function ($entity) use ($matcher) { + if (1 === $matcher->numberOfInvocations()) { + return null; + } + + if (2 === $matcher->numberOfInvocations()) { + return throw new \RuntimeException('boom'); + } + }); + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc b/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc deleted file mode 100644 index d05c1e40..00000000 --- a/rules-tests/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector/Fixture/will_return_callback_void.php.inc +++ /dev/null @@ -1,43 +0,0 @@ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity) { - echo $entity; - - return true; - }); - } -} - -?> ------ -createMock(SomeEntityManager::class) - ->method('persist') - ->willReturnCallback(function ($entity): void { - echo $entity; - }); - } -} - -?> diff --git a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php index b93d5b96..bfb939bd 100644 --- a/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php +++ b/rules/CodeQuality/Rector/Class_/VoidMethodWithCallbackToWillReturnCallbackRector.php @@ -55,7 +55,7 @@ public function __construct( public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'On a void mocked method, drop the value return from a callback and type it as void; a ->with($this->callback(...)) matcher is also flipped to a ->willReturnCallback() call', + 'Flip a ->with($this->callback(...)) matcher on a void mocked method to a ->willReturnCallback() call, dropping the value return and typing the closure as void', [ new CodeSample( <<<'CODE_SAMPLE' @@ -145,15 +145,11 @@ public function refactor(Node $node): ?Class_ return null; } - $isWithCallback = $this->isName($subNode->name, 'with'); - $isWillReturnCallback = $this->isName($subNode->name, 'willReturnCallback'); - if (! $isWithCallback && ! $isWillReturnCallback) { + if (! $this->isName($subNode->name, 'with')) { return null; } - $closure = $isWithCallback ? $this->matchCallbackClosure($subNode) : $this->matchDirectClosure( - $subNode - ); + $closure = $this->matchCallbackClosure($subNode); if (! $closure instanceof Closure) { return null; } @@ -167,10 +163,8 @@ public function refactor(Node $node): ?Class_ } // flip ->with($this->callback($closure)) to ->willReturnCallback($closure) - if ($isWithCallback) { - $subNode->name = new Identifier('willReturnCallback'); - $subNode->args = [new Arg($closure)]; - } + $subNode->name = new Identifier('willReturnCallback'); + $subNode->args = [new Arg($closure)]; $hasChanged = true; @@ -184,16 +178,6 @@ public function refactor(Node $node): ?Class_ return $node; } - private function matchDirectClosure(MethodCall $willReturnCallbackMethodCall): ?Closure - { - $firstArg = $willReturnCallbackMethodCall->getArgs()[0] ?? null; - if ($firstArg instanceof Arg && $firstArg->value instanceof Closure) { - return $firstArg->value; - } - - return null; - } - private function matchCallbackClosure(MethodCall $withMethodCall): ?Closure { $withFirstArg = $withMethodCall->getArgs()[0] ?? null;