Skip to content

Commit 7abaa93

Browse files
authored
Fix infinite recursion in ConstantArrayType::isCallable() via RecursionGuard (#5423)
1 parent f570f88 commit 7abaa93

File tree

3 files changed

+52
-12
lines changed

3 files changed

+52
-12
lines changed

src/Type/Constant/ConstantArrayType.php

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use PHPStan\Type\MixedType;
4343
use PHPStan\Type\NeverType;
4444
use PHPStan\Type\NullType;
45+
use PHPStan\Type\RecursionGuard;
4546
use PHPStan\Type\Traits\ArrayTypeTrait;
4647
use PHPStan\Type\Traits\NonObjectTypeTrait;
4748
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
@@ -494,21 +495,29 @@ public function equals(Type $type): bool
494495

495496
public function isCallable(): TrinaryLogic
496497
{
497-
$hasNonExistentMethod = false;
498-
$typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod);
499-
if ($typeAndMethods === []) {
500-
return TrinaryLogic::createNo();
501-
}
498+
$result = RecursionGuard::run($this, function (): TrinaryLogic {
499+
$hasNonExistentMethod = false;
500+
$typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod);
501+
if ($typeAndMethods === []) {
502+
return TrinaryLogic::createNo();
503+
}
502504

503-
$results = array_map(
504-
static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(),
505-
$typeAndMethods,
506-
);
505+
$results = array_map(
506+
static fn (ConstantArrayTypeAndMethod $typeAndMethod): TrinaryLogic => $typeAndMethod->getCertainty(),
507+
$typeAndMethods,
508+
);
509+
510+
$result = TrinaryLogic::createYes()->and(...$results);
507511

508-
$result = TrinaryLogic::createYes()->and(...$results);
512+
if ($hasNonExistentMethod) {
513+
$result = $result->and(TrinaryLogic::createMaybe());
514+
}
509515

510-
if ($hasNonExistentMethod) {
511-
$result = $result->and(TrinaryLogic::createMaybe());
516+
return $result;
517+
});
518+
519+
if ($result instanceof ErrorType) {
520+
return TrinaryLogic::createNo();
512521
}
513522

514523
return $result;

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ public function testInfiniteRecursionWithCallable(): void
9393
$this->assertNoErrors($errors);
9494
}
9595

96+
#[RequiresPhp('>= 8.0')]
97+
public function testConstantArrayCallableDoesNotCauseInfiniteRecursion(): void
98+
{
99+
// Previously caused infinite recursion / OOM via ConstantArrayType::isCallable()
100+
// resolving getMethod() which triggered return type resolution that called isCallable() again
101+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-constant-array-callable-recursion.php');
102+
$this->assertCount(3, $errors);
103+
}
104+
96105
public function testClassThatExtendsUnknownClassIn3rdPartyPropertyTypeShouldNotCauseAutoloading(): void
97106
{
98107
// no error about PHPStan\Tests\Baz not being able to be autoloaded
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace BugConstantArrayCallableRecursion;
4+
5+
class Period
6+
{
7+
8+
/**
9+
* @return array{'BugConstantArrayCallableRecursion\Period', 'endIteration'}
10+
*/
11+
public function endIteration(): callable
12+
{
13+
return [self::class, 'endIteration'];
14+
}
15+
16+
}
17+
18+
function test(): void
19+
{
20+
$cb = ['BugConstantArrayCallableRecursion\Period', 'endIteration'];
21+
is_callable($cb);
22+
}

0 commit comments

Comments
 (0)