Skip to content

Commit 40ccae6

Browse files
staabmphpstan-bot
authored andcommitted
Bail to MixedType in deep dim fetch writes on oversized arrays
When produceArrayDimFetchAssignValueToWrite walks down through nested ArrayDimFetch nodes, each level calls getOffsetValueType on the current type. For deeply nested structures (6+ levels) where the type has already been marked as an oversized array (precision intentionally lost), these cascading operations produce exponential cost without useful precision gain. Stop drilling deeper when the dim fetch depth exceeds 5 AND the current offsetValueType is already an oversized array, substituting MixedType instead. This prevents expensive cascading intersectTypes/union operations at levels where precision cannot be maintained anyway. Closes phpstan/phpstan#14624
1 parent 9abd515 commit 40ccae6

2 files changed

Lines changed: 113 additions & 14 deletions

File tree

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,8 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam
10611061
return $scope;
10621062
}
10631063

1064+
private const ARRAY_DIM_FETCH_WRITE_DEPTH_LIMIT = 5;
1065+
10641066
/**
10651067
* @param non-empty-list<ArrayDimFetch> $dimFetchStack
10661068
* @param non-empty-list<array{Type|null, ArrayDimFetch}> $offsetTypes
@@ -1073,28 +1075,34 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
10731075

10741076
$offsetValueTypeStack = [$offsetValueType];
10751077
$generalizeOnWrite = $offsetTypes[array_key_last($offsetTypes)][0] !== null;
1078+
$dimDepth = 0;
10761079
foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) {
1080+
$dimDepth++;
10771081
if ($offsetType === null) {
10781082
$offsetValueType = new ConstantArrayType([], []);
10791083
$generalizeOnWrite = false;
10801084
} else {
1081-
$has = $offsetValueType->hasOffsetValueType($offsetType);
1082-
if ($has->yes()) {
1083-
if ($scope->hasExpressionType($dimFetch)->yes()) {
1084-
$offsetValueType = $scope->getType($dimFetch);
1085+
if ($dimDepth > self::ARRAY_DIM_FETCH_WRITE_DEPTH_LIMIT && $offsetValueType->isOversizedArray()->yes()) {
1086+
$offsetValueType = new MixedType();
1087+
} else {
1088+
$has = $offsetValueType->hasOffsetValueType($offsetType);
1089+
if ($has->yes()) {
1090+
if ($scope->hasExpressionType($dimFetch)->yes()) {
1091+
$offsetValueType = $scope->getType($dimFetch);
1092+
} else {
1093+
$offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
1094+
}
1095+
} elseif ($has->maybe()) {
1096+
if ($scope->hasExpressionType($dimFetch)->yes()) {
1097+
$generalizeOnWrite = false;
1098+
$offsetValueType = $scope->getType($dimFetch);
1099+
} else {
1100+
$offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], []));
1101+
}
10851102
} else {
1086-
$offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
1087-
}
1088-
} elseif ($has->maybe()) {
1089-
if ($scope->hasExpressionType($dimFetch)->yes()) {
10901103
$generalizeOnWrite = false;
1091-
$offsetValueType = $scope->getType($dimFetch);
1092-
} else {
1093-
$offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], []));
1104+
$offsetValueType = new ConstantArrayType([], []);
10941105
}
1095-
} else {
1096-
$generalizeOnWrite = false;
1097-
$offsetValueType = new ConstantArrayType([], []);
10981106
}
10991107
}
11001108

tests/bench/data/bug-14624.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Bug14624;
4+
5+
final class BenchDb extends \mysqli
6+
{
7+
/**
8+
* @param string $query
9+
* @param int $resultMode
10+
* @return BenchDbResult
11+
*/
12+
public function query($query, $resultMode = MYSQLI_STORE_RESULT)
13+
{
14+
throw new \RuntimeException();
15+
}
16+
}
17+
18+
final class BenchDbResult extends \mysqli_result
19+
{
20+
public function getIterator(): \Iterator
21+
{
22+
throw new \RuntimeException();
23+
}
24+
}
25+
26+
final class BenchRepro
27+
{
28+
private BenchDb $db;
29+
30+
/**
31+
* @return mixed[]
32+
*/
33+
public function build()
34+
{
35+
$out = [];
36+
$rows = $this->db->query('');
37+
38+
while ($row = $rows->fetch_assoc()) {
39+
$row['group_id'] = intval($row['group_id']);
40+
$bucket = intval($row['bucket']);
41+
42+
if (!isset($out[$row['a']])) {
43+
$out[$row['a']] = ['id' => $row['a'], 'label' => $row['a_label'], 'groups' => []];
44+
}
45+
46+
if (!isset($out[$row['a']]['groups'][$bucket])) {
47+
$out[$row['a']]['groups'][$bucket] = [];
48+
}
49+
50+
if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']])) {
51+
$out[$row['a']]['groups'][$bucket][$row['group_id']] = ['id' => $row['group_id'], 'label' => $row['group_label'], 'sections' => []];
52+
}
53+
54+
if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']])) {
55+
$out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']] = ['id' => $row['section_id'], 'label' => $row['section_label'], 'items' => []];
56+
}
57+
58+
if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']])) {
59+
$row['csv_ids'] = $row['csv_ids'] ? array_map('intval', explode(',', $row['csv_ids'])) : [];
60+
$out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']] = [
61+
'id' => $row['item_id'], 'title' => $row['item_title'], 'code' => $row['item_code'],
62+
'type' => $row['item_type'], 'state' => $row['item_state'], 'priority' => $row['item_priority'],
63+
'csv_ids' => $row['csv_ids'], 'related_rows' => [], 'details' => [], 'tags' => [],
64+
];
65+
if ($row['csv_ids']) {
66+
$relatedRows = $this->db->query('');
67+
while ($relatedRow = $relatedRows->fetch_assoc()) {
68+
$out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['related_rows'][] = $relatedRow;
69+
}
70+
}
71+
}
72+
73+
if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['details'][$row['detail_id']])) {
74+
$out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['details'][$row['detail_id']] = [
75+
'id' => $row['detail_id'], 'title' => $row['detail_title'], 'code' => $row['detail_code'],
76+
'kind' => $row['detail_kind'], 'amount' => $row['detail_amount'],
77+
'records' => [], 'notes' => [], 'flags' => [],
78+
];
79+
}
80+
81+
if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['details'][$row['detail_id']]['records'][$row['record_id']])) {
82+
$out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['details'][$row['detail_id']]['records'][$row['record_id']] = [
83+
'id' => $row['record_id'], 'name' => $row['record_name'], 'code' => $row['record_code'],
84+
'version' => $row['record_version'], 'payload' => $row['record_payload'],
85+
];
86+
}
87+
}
88+
89+
return $out;
90+
}
91+
}

0 commit comments

Comments
 (0)