From 6720ce3076ae01ef559f97e88144bdd4c394a1ee Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 29 Jun 2026 10:33:18 +0200 Subject: [PATCH] [PHPUnit60] Fix false positive in AddDoesNotPerformAssertionToNonAssertingTestRector The nesting-depth counter in AssertCallAnalyzer was incremented on every containsAssertCall() but never decremented, so it accumulated across the whole depth-first walk of a single test method instead of tracking real recursion depth. When a non-asserting call (e.g. a production static call) was visited before an assert-performing helper, it exhausted the level-5 budget; the later helper was then short-circuited to "no assertions" and the test wrongly got @doesNotPerformAssertions. Wrap the body in try/finally so the level is restored on unwind. Add a keep fixture with a deep non-asserting chain preceding an assert helper. --- ...ep_deep_calls_before_assert_helper.php.inc | 47 +++++++++++++++++++ src/NodeAnalyzer/AssertCallAnalyzer.php | 41 +++++++++------- 2 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 rules-tests/PHPUnit60/Rector/ClassMethod/AddDoesNotPerformAssertionToNonAssertingTestRector/Fixture/keep_deep_calls_before_assert_helper.php.inc diff --git a/rules-tests/PHPUnit60/Rector/ClassMethod/AddDoesNotPerformAssertionToNonAssertingTestRector/Fixture/keep_deep_calls_before_assert_helper.php.inc b/rules-tests/PHPUnit60/Rector/ClassMethod/AddDoesNotPerformAssertionToNonAssertingTestRector/Fixture/keep_deep_calls_before_assert_helper.php.inc new file mode 100644 index 000000000..b9f7c775e --- /dev/null +++ b/rules-tests/PHPUnit60/Rector/ClassMethod/AddDoesNotPerformAssertionToNonAssertingTestRector/Fixture/keep_deep_calls_before_assert_helper.php.inc @@ -0,0 +1,47 @@ +prepare(); + + $this->checkResult(true); + } + + private function prepare(): void + { + $this->stepTwo(); + } + + private function stepTwo(): void + { + $this->stepThree(); + } + + private function stepThree(): void + { + $this->stepFour(); + } + + private function stepFour(): void + { + $this->stepFive(); + } + + private function stepFive(): void + { + // no assertion here + } + + private function checkResult(bool $bool): void + { + self::assertTrue($bool); + } +} diff --git a/src/NodeAnalyzer/AssertCallAnalyzer.php b/src/NodeAnalyzer/AssertCallAnalyzer.php index 7cd318d6d..dcdc9f300 100644 --- a/src/NodeAnalyzer/AssertCallAnalyzer.php +++ b/src/NodeAnalyzer/AssertCallAnalyzer.php @@ -61,29 +61,34 @@ public function containsAssertCall(ClassMethod $classMethod): bool { ++$this->classMethodNestingLevel; - // probably no assert method in the end - if ($this->classMethodNestingLevel > self::MAX_NESTED_METHOD_CALL_LEVEL) { - return false; - } + try { + // probably no assert method in the end + if ($this->classMethodNestingLevel > self::MAX_NESTED_METHOD_CALL_LEVEL) { + return false; + } - $cacheHash = md5($this->betterStandardPrinter->prettyPrint([$classMethod])); + $cacheHash = md5($this->betterStandardPrinter->prettyPrint([$classMethod])); - if (isset($this->containsAssertCallByClassMethod[$cacheHash])) { - return $this->containsAssertCallByClassMethod[$cacheHash]; - } + if (isset($this->containsAssertCallByClassMethod[$cacheHash])) { + return $this->containsAssertCallByClassMethod[$cacheHash]; + } - // A. try "->assert" shallow search first for performance - $hasDirectAssertOrMockCall = $this->hasDirectAssertOrMockCall($classMethod); - if ($hasDirectAssertOrMockCall) { - $this->containsAssertCallByClassMethod[$cacheHash] = $hasDirectAssertOrMockCall; - return true; - } + // A. try "->assert" shallow search first for performance + $hasDirectAssertOrMockCall = $this->hasDirectAssertOrMockCall($classMethod); + if ($hasDirectAssertOrMockCall) { + $this->containsAssertCallByClassMethod[$cacheHash] = $hasDirectAssertOrMockCall; + return true; + } - // B. look for nested calls - $hasNestedAssertOrMockCall = $this->hasNestedAssertCall($classMethod); - $this->containsAssertCallByClassMethod[$cacheHash] = $hasNestedAssertOrMockCall; + // B. look for nested calls + $hasNestedAssertOrMockCall = $this->hasNestedAssertCall($classMethod); + $this->containsAssertCallByClassMethod[$cacheHash] = $hasNestedAssertOrMockCall; - return $hasNestedAssertOrMockCall; + return $hasNestedAssertOrMockCall; + } finally { + // restore depth so sibling calls in the same DFS keep their full budget + --$this->classMethodNestingLevel; + } } public function isAssertMethodCall(MethodCall|StaticCall $call): bool