Skip to content

Commit 177cd03

Browse files
committed
Fix phpstan/phpstan#14319: Prevent exponential type growth in array dim fetch narrowing
- Added ARRAY_DIM_FETCH_UNION_TYPE_LIMIT (8) to MutatingScope to cap parent type narrowing - When array dim fetch narrows a parent variable type that is already a UnionType with more than 8 members, skip propagating HasOffsetValueType intersections to the parent - This prevents 2^N exponential growth when N consecutive if-blocks check different array offsets on an array|object typed variable - New regression test in tests/PHPStan/Analyser/nsrt/bug-14319.php - New benchmark test in tests/bench/data/bug-14319.php
1 parent 7861fe4 commit 177cd03

3 files changed

Lines changed: 96 additions & 8 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ class MutatingScope implements Scope, NodeCallbackInvoker
139139
public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid';
140140
private const CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME = 'containsSuperGlobal';
141141

142+
private const ARRAY_DIM_FETCH_UNION_TYPE_LIMIT = 8;
143+
142144
/** @var Type[] */
143145
private array $resolvedTypes = [];
144146

@@ -2690,27 +2692,30 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType,
26902692
$isArray = $exprVarType->isArray();
26912693
if (!$exprVarType instanceof MixedType && !$isArray->no()) {
26922694
$varType = $exprVarType;
2693-
if (!$isArray->yes()) {
2695+
$tooComplex = $exprVarType instanceof UnionType && count($exprVarType->getTypes()) > self::ARRAY_DIM_FETCH_UNION_TYPE_LIMIT;
2696+
if (!$tooComplex && !$isArray->yes()) {
26942697
if ($dimType->isInteger()->yes()) {
26952698
$varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetAccessibleType());
26962699
} else {
26972700
$varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetAccessibleType());
26982701
}
26992702
}
27002703

2701-
if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
2704+
if (!$tooComplex && ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType)) {
27022705
$varType = TypeCombinator::intersect(
27032706
$varType,
27042707
new HasOffsetValueType($dimType, $type),
27052708
);
27062709
}
27072710

2708-
$scope = $scope->specifyExpressionType(
2709-
$expr->var,
2710-
$varType,
2711-
$scope->getNativeType($expr->var),
2712-
$certainty,
2713-
);
2711+
if (!$tooComplex) {
2712+
$scope = $scope->specifyExpressionType(
2713+
$expr->var,
2714+
$varType,
2715+
$scope->getNativeType($expr->var),
2716+
$certainty,
2717+
);
2718+
}
27142719
}
27152720
}
27162721
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Bug14319;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function foo(string $a, int $b): array|object
8+
{
9+
return $a;
10+
}
11+
12+
13+
final class test
14+
{
15+
protected function edit(int|string|null $IdNum = null): void
16+
{
17+
$rows = foo("SELECT *", $IdNum);
18+
assertType('array|object', $rows);
19+
20+
if ($_POST['edycja'] === 'edycja' ) {
21+
$raport = '';
22+
if ($rows['rap_tr']) {
23+
$raport .= 'T: '.$rows['rap_tr'].", \n";
24+
}
25+
assertType('string', $raport);
26+
if ($rows['rap_ks']) {
27+
$raport .= 'K: '.$rows['rap_ks'].", \n";
28+
}
29+
if ($rows['rap_br']) {
30+
$raport .= 'B: '.$rows['rap_br'].", \n";
31+
}
32+
if ($rows['rap_cz']) {
33+
$raport .= 'C: '.$rows['rap_cz'].", \n";
34+
}
35+
if ($rows['rap_fil']) {
36+
$raport .= 'Fil: '.$rows['rap_fil'].", \n";
37+
}
38+
if ($rows['rap_roz']) {
39+
$raport .= 'Roz: '.$rows['rap_roz'].", \n";
40+
}
41+
assertType('string', $raport);
42+
}
43+
}
44+
}

tests/bench/data/bug-14319.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace BenchBug14319;
4+
5+
function foo(string $a, int $b): array|object
6+
{
7+
return $a;
8+
}
9+
10+
11+
final class test
12+
{
13+
protected function edit(int|string|null $IdNum = null): void
14+
{
15+
$rows = foo("SELECT *", $IdNum);
16+
17+
if ($_POST['edycja'] === 'edycja' ) {
18+
$raport = '';
19+
if ($rows['rap_tr']) {
20+
$raport .= 'T: '.$rows['rap_tr'].", \n";
21+
}
22+
if ($rows['rap_ks']) {
23+
$raport .= 'K: '.$rows['rap_ks'].", \n";
24+
}
25+
if ($rows['rap_br']) {
26+
$raport .= 'B: '.$rows['rap_br'].", \n";
27+
}
28+
if ($rows['rap_cz']) {
29+
$raport .= 'C: '.$rows['rap_cz'].", \n";
30+
}
31+
if ($rows['rap_fil']) {
32+
$raport .= 'Fil: '.$rows['rap_fil'].", \n";
33+
}
34+
if ($rows['rap_roz']) {
35+
$raport .= 'Roz: '.$rows['rap_roz'].", \n";
36+
}
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)