From 7e359b13018bb1eabbfc10c1ab66d37e10826fe7 Mon Sep 17 00:00:00 2001 From: Sam Anglin Date: Tue, 7 Apr 2026 13:57:58 +0300 Subject: [PATCH 1/3] Fix infinite recursion in ConstantArrayType::isCallable() via RecursionGuard --- src/Type/Constant/ConstantArrayType.php | 34 ++++++++++++------- .../Analyser/AnalyserIntegrationTest.php | 8 +++++ .../bug-constant-array-callable-recursion.php | 22 ++++++++++++ 3 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-constant-array-callable-recursion.php diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index aa711029a8d..32c5daa583c 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -42,6 +42,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; +use PHPStan\Type\RecursionGuard; use PHPStan\Type\Traits\ArrayTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; @@ -494,23 +495,32 @@ public function equals(Type $type): bool public function isCallable(): TrinaryLogic { - $hasNonExistentMethod = false; - $typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod); - if ($typeAndMethods === []) { - return TrinaryLogic::createNo(); - } + $result = RecursionGuard::run($this, function (): TrinaryLogic { + $hasNonExistentMethod = false; + $typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod); + if ($typeAndMethods === []) { + return TrinaryLogic::createNo(); + } - $results = array_map( - static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(), - $typeAndMethods, - ); + $results = array_map( + static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(), + $typeAndMethods, + ); - $result = TrinaryLogic::createYes()->and(...$results); + $result = TrinaryLogic::createYes()->and(...$results); - if ($hasNonExistentMethod) { - $result = $result->and(TrinaryLogic::createMaybe()); + if ($hasNonExistentMethod) { + $result = $result->and(TrinaryLogic::createMaybe()); + } + + return $result; + }); + + if ($result instanceof ErrorType) { + return TrinaryLogic::createNo(); } + return $result; } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 9020c4b3c4a..6cb682edbcc 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -93,6 +93,14 @@ public function testInfiniteRecursionWithCallable(): void $this->assertNoErrors($errors); } + public function testConstantArrayCallableDoesNotCauseInfiniteRecursion(): void + { + // Previously caused infinite recursion / OOM via ConstantArrayType::isCallable() + // resolving getMethod() which triggered return type resolution that called isCallable() again + $errors = $this->runAnalyse(__DIR__ . '/data/bug-constant-array-callable-recursion.php'); + $this->assertCount(3, $errors); + } + public function testClassThatExtendsUnknownClassIn3rdPartyPropertyTypeShouldNotCauseAutoloading(): void { // no error about PHPStan\Tests\Baz not being able to be autoloaded diff --git a/tests/PHPStan/Analyser/data/bug-constant-array-callable-recursion.php b/tests/PHPStan/Analyser/data/bug-constant-array-callable-recursion.php new file mode 100644 index 00000000000..db244a276f7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-constant-array-callable-recursion.php @@ -0,0 +1,22 @@ + Date: Tue, 7 Apr 2026 16:44:28 +0300 Subject: [PATCH 2/3] Address coding standards --- src/Type/Constant/ConstantArrayType.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 32c5daa583c..c5cf80b83c0 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -520,7 +520,6 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - return $result; } From 206c197498f1fbadd22ff2cf12ae462971e236b5 Mon Sep 17 00:00:00 2001 From: Sam <90829439+MizouziE@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:14:17 +0300 Subject: [PATCH 3/3] Update tests/PHPStan/Analyser/AnalyserIntegrationTest.php Co-authored-by: Markus Staab --- tests/PHPStan/Analyser/AnalyserIntegrationTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 6cb682edbcc..0288da8ba15 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -93,6 +93,7 @@ public function testInfiniteRecursionWithCallable(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] public function testConstantArrayCallableDoesNotCauseInfiniteRecursion(): void { // Previously caused infinite recursion / OOM via ConstantArrayType::isCallable()