diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index aa711029a8d..c5cf80b83c0 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,21 +495,29 @@ 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..0288da8ba15 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -93,6 +93,15 @@ public function testInfiniteRecursionWithCallable(): void $this->assertNoErrors($errors); } + #[RequiresPhp('>= 8.0')] + 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 @@ +