From 40ccae6e8a20a1fe3e28f2853bb154480da72ae9 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Sat, 16 May 2026 16:04:18 +0000 Subject: [PATCH] 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 https://github.com/phpstan/phpstan/issues/14624 --- src/Analyser/ExprHandler/AssignHandler.php | 36 +++++---- tests/bench/data/bug-14624.php | 91 ++++++++++++++++++++++ 2 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 tests/bench/data/bug-14624.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 5d2a084ec04..d80219d9577 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1061,6 +1061,8 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam return $scope; } + private const ARRAY_DIM_FETCH_WRITE_DEPTH_LIMIT = 5; + /** * @param non-empty-list $dimFetchStack * @param non-empty-list $offsetTypes @@ -1073,28 +1075,34 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $offsetValueTypeStack = [$offsetValueType]; $generalizeOnWrite = $offsetTypes[array_key_last($offsetTypes)][0] !== null; + $dimDepth = 0; foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) { + $dimDepth++; if ($offsetType === null) { $offsetValueType = new ConstantArrayType([], []); $generalizeOnWrite = false; } else { - $has = $offsetValueType->hasOffsetValueType($offsetType); - if ($has->yes()) { - if ($scope->hasExpressionType($dimFetch)->yes()) { - $offsetValueType = $scope->getType($dimFetch); + if ($dimDepth > self::ARRAY_DIM_FETCH_WRITE_DEPTH_LIMIT && $offsetValueType->isOversizedArray()->yes()) { + $offsetValueType = new MixedType(); + } else { + $has = $offsetValueType->hasOffsetValueType($offsetType); + if ($has->yes()) { + if ($scope->hasExpressionType($dimFetch)->yes()) { + $offsetValueType = $scope->getType($dimFetch); + } else { + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + } + } elseif ($has->maybe()) { + if ($scope->hasExpressionType($dimFetch)->yes()) { + $generalizeOnWrite = false; + $offsetValueType = $scope->getType($dimFetch); + } else { + $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], [])); + } } else { - $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); - } - } elseif ($has->maybe()) { - if ($scope->hasExpressionType($dimFetch)->yes()) { $generalizeOnWrite = false; - $offsetValueType = $scope->getType($dimFetch); - } else { - $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], [])); + $offsetValueType = new ConstantArrayType([], []); } - } else { - $generalizeOnWrite = false; - $offsetValueType = new ConstantArrayType([], []); } } diff --git a/tests/bench/data/bug-14624.php b/tests/bench/data/bug-14624.php new file mode 100644 index 00000000000..aafc111c7a4 --- /dev/null +++ b/tests/bench/data/bug-14624.php @@ -0,0 +1,91 @@ +db->query(''); + + while ($row = $rows->fetch_assoc()) { + $row['group_id'] = intval($row['group_id']); + $bucket = intval($row['bucket']); + + if (!isset($out[$row['a']])) { + $out[$row['a']] = ['id' => $row['a'], 'label' => $row['a_label'], 'groups' => []]; + } + + if (!isset($out[$row['a']]['groups'][$bucket])) { + $out[$row['a']]['groups'][$bucket] = []; + } + + if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']])) { + $out[$row['a']]['groups'][$bucket][$row['group_id']] = ['id' => $row['group_id'], 'label' => $row['group_label'], 'sections' => []]; + } + + if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']])) { + $out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']] = ['id' => $row['section_id'], 'label' => $row['section_label'], 'items' => []]; + } + + if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']])) { + $row['csv_ids'] = $row['csv_ids'] ? array_map('intval', explode(',', $row['csv_ids'])) : []; + $out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']] = [ + 'id' => $row['item_id'], 'title' => $row['item_title'], 'code' => $row['item_code'], + 'type' => $row['item_type'], 'state' => $row['item_state'], 'priority' => $row['item_priority'], + 'csv_ids' => $row['csv_ids'], 'related_rows' => [], 'details' => [], 'tags' => [], + ]; + if ($row['csv_ids']) { + $relatedRows = $this->db->query(''); + while ($relatedRow = $relatedRows->fetch_assoc()) { + $out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['related_rows'][] = $relatedRow; + } + } + } + + if (!isset($out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['details'][$row['detail_id']])) { + $out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['details'][$row['detail_id']] = [ + 'id' => $row['detail_id'], 'title' => $row['detail_title'], 'code' => $row['detail_code'], + 'kind' => $row['detail_kind'], 'amount' => $row['detail_amount'], + 'records' => [], 'notes' => [], 'flags' => [], + ]; + } + + 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']])) { + $out[$row['a']]['groups'][$bucket][$row['group_id']]['sections'][$row['section_id']]['items'][$row['item_id']]['details'][$row['detail_id']]['records'][$row['record_id']] = [ + 'id' => $row['record_id'], 'name' => $row['record_name'], 'code' => $row['record_code'], + 'version' => $row['record_version'], 'payload' => $row['record_payload'], + ]; + } + } + + return $out; + } +}