From 8b82d66ad49d936e8a20e798c0b7c12ed760c308 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Jul 2026 11:04:42 +0200 Subject: [PATCH 1/2] [CodeQuality] Skip nested value compare in CallbackSingleAssertToSimplerRector --- .../Fixture/skip_nested_compare.php.inc | 21 ++++++++++++ .../CallbackSingleAssertToSimplerRector.php | 32 +++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare.php.inc diff --git a/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare.php.inc new file mode 100644 index 00000000..c19d0537 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare.php.inc @@ -0,0 +1,21 @@ +getMockBuilder('AnyType')->getMock(); + + $builder->expects($this->exactly(2)) + ->method('add') + ->with($this->callback(function (array $args): bool { + $this->assertSame($args['label'], 'Test Label'); + + return true; + })); + } +} diff --git a/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php b/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php index 95db43ad..fba0b9dc 100644 --- a/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php +++ b/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php @@ -9,6 +9,7 @@ use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Return_; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; @@ -153,8 +154,35 @@ private function matchCallbackSoleAssertSameExpected(Expr $expr): ?Expr return null; } - return $firstStmtExpr->getArgs()[0] - ->value; + $assertSameArgs = $firstStmtExpr->getArgs(); + if (count($assertSameArgs) !== 2) { + return null; + } + + // the actual value must be the whole closure parameter, not a nested access (e.g. $args['label']) + if (! $this->isClosureSoleParam($innerClosure, $assertSameArgs[1]->value)) { + return null; + } + + return $assertSameArgs[0]->value; + } + + private function isClosureSoleParam(Closure $closure, Expr $expr): bool + { + if (count($closure->params) !== 1) { + return false; + } + + if (! $expr instanceof Variable) { + return false; + } + + $soleParam = $closure->params[0]; + if (! $soleParam->var instanceof Variable) { + return false; + } + + return $this->nodeComparator->areNodesEqual($soleParam->var, $expr); } private function isTrueReturn(Return_ $return): bool From 54e39d1b1dd9c5ec22316e35b6423cb156555b3f Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 13 Jul 2026 11:08:29 +0200 Subject: [PATCH 2/2] handle flipped param order, skip nested compare both orders --- .../Fixture/flipped_param_order.php.inc | 43 +++++++++++++++++++ .../skip_nested_compare_expected_left.php.inc | 21 +++++++++ .../CallbackSingleAssertToSimplerRector.php | 16 +++++-- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/flipped_param_order.php.inc create mode 100644 rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare_expected_left.php.inc diff --git a/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/flipped_param_order.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/flipped_param_order.php.inc new file mode 100644 index 00000000..73fefbcc --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/flipped_param_order.php.inc @@ -0,0 +1,43 @@ +getMockBuilder('AnyType')->getMock(); + + $builder->expects($this->exactly(2)) + ->method('add') + ->with($this->callback(function ($type): bool { + $this->assertSame($type, TextType::class); + + return true; + })); + } +} + +?> +----- +getMockBuilder('AnyType')->getMock(); + + $builder->expects($this->exactly(2)) + ->method('add') + ->with($this->equalTo(TextType::class)); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare_expected_left.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare_expected_left.php.inc new file mode 100644 index 00000000..3703c47d --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector/Fixture/skip_nested_compare_expected_left.php.inc @@ -0,0 +1,21 @@ +getMockBuilder('AnyType')->getMock(); + + $builder->expects($this->exactly(2)) + ->method('add') + ->with($this->callback(function (array $args): bool { + $this->assertSame('Test Label', $args['label']); + + return true; + })); + } +} diff --git a/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php b/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php index fba0b9dc..67fd6c7a 100644 --- a/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php +++ b/rules/CodeQuality/Rector/MethodCall/CallbackSingleAssertToSimplerRector.php @@ -159,12 +159,20 @@ private function matchCallbackSoleAssertSameExpected(Expr $expr): ?Expr return null; } - // the actual value must be the whole closure parameter, not a nested access (e.g. $args['label']) - if (! $this->isClosureSoleParam($innerClosure, $assertSameArgs[1]->value)) { - return null; + $firstValue = $assertSameArgs[0]->value; + $secondValue = $assertSameArgs[1]->value; + + // one side must be the whole closure parameter; the other side is the expected value + // nested access (e.g. $args['label']) is skipped, as equalTo() matches the whole argument + if ($this->isClosureSoleParam($innerClosure, $secondValue)) { + return $firstValue; + } + + if ($this->isClosureSoleParam($innerClosure, $firstValue)) { + return $secondValue; } - return $assertSameArgs[0]->value; + return null; } private function isClosureSoleParam(Closure $closure, Expr $expr): bool