Skip to content

Commit 7e07096

Browse files
phpstan-botstaabmclaude
authored
Fix phpstan/phpstan#14319: PHPStan takes extremely long analyzing if-offset checks on array|object (#5242)
Co-authored-by: Markus Staab <maggus.staab@googlemail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Markus Staab <markus.staab@redaxo.de>
1 parent b08e976 commit 7e07096

File tree

3 files changed

+114
-1
lines changed

3 files changed

+114
-1
lines changed

src/Analyser/MutatingScope.php

Lines changed: 26 additions & 1 deletion
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_HAS_OFFSET_VALUE_TYPE_LIMIT = 16;
143+
142144
/** @var Type[] */
143145
private array $resolvedTypes = [];
144146

@@ -2689,16 +2691,39 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType,
26892691
$exprVarType = $scope->getType($expr->var);
26902692
$isArray = $exprVarType->isArray();
26912693
if (!$exprVarType instanceof MixedType && !$isArray->no()) {
2694+
$tooManyHasOffsetValueTypes = false;
2695+
26922696
$varType = $exprVarType;
26932697
if (!$isArray->yes()) {
26942698
if ($dimType->isInteger()->yes()) {
26952699
$varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetAccessibleType());
26962700
} else {
26972701
$varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetAccessibleType());
26982702
}
2703+
2704+
if ($exprVarType instanceof UnionType) {
2705+
$hasOffsetAccessoryCount = 0;
2706+
foreach ($exprVarType->getTypes() as $innerType) {
2707+
foreach (TypeUtils::getAccessoryTypes($innerType) as $accessoryType) {
2708+
if (!($accessoryType instanceof HasOffsetValueType)) {
2709+
continue;
2710+
}
2711+
2712+
$hasOffsetAccessoryCount++;
2713+
2714+
if ($hasOffsetAccessoryCount > self::ARRAY_DIM_FETCH_UNION_HAS_OFFSET_VALUE_TYPE_LIMIT) {
2715+
$tooManyHasOffsetValueTypes = true;
2716+
break 2;
2717+
}
2718+
}
2719+
}
2720+
}
26992721
}
27002722

2701-
if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
2723+
if (
2724+
!$tooManyHasOffsetValueTypes
2725+
&& ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType)
2726+
) {
27022727
$varType = TypeCombinator::intersect(
27032728
$varType,
27042729
new HasOffsetValueType($dimType, $type),
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
if ($rows['rap_ks']) {
26+
$raport .= 'K: '.$rows['rap_ks'].", \n";
27+
}
28+
if ($rows['rap_br']) {
29+
$raport .= 'B: '.$rows['rap_br'].", \n";
30+
}
31+
if ($rows['rap_cz']) {
32+
$raport .= 'C: '.$rows['rap_cz'].", \n";
33+
}
34+
if ($rows['rap_fil']) {
35+
$raport .= 'Fil: '.$rows['rap_fil'].", \n";
36+
}
37+
if ($rows['rap_roz']) {
38+
$raport .= 'Roz: '.$rows['rap_roz'].", \n";
39+
}
40+
if ($rows['rap_roz2']) {
41+
$raport .= 'Roz: '.$rows['rap_roz2'].", \n";
42+
}
43+
if ($rows['rap_roz3']) {
44+
$raport .= 'Roz: '.$rows['rap_roz3'].", \n";
45+
}
46+
assertType("(non-empty-array&hasOffsetValue('rap_br', mixed)&hasOffsetValue('rap_ks', mixed)&hasOffsetValue('rap_tr', mixed))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))", $rows);
47+
}
48+
}
49+
}

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)