Restrict list type preservation in IntersectionType::setOffsetValueType to offsets within list key range#5455
Conversation
…ype` to offsets within list key range - In `IntersectionType::setOffsetValueType`, change the condition for re-adding `AccessoryArrayListType` from checking if the offset is any integer (`$offsetType->toArrayKey()->isInteger()->yes()`) to checking if the offset is within the list's key type range (`$this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes()`). - This prevents list type from being incorrectly preserved when assigning with an arbitrary `int` key (which includes negative values and could create gaps), while still preserving list type for `int<0, max>` offsets (valid list key range) used in nested modifications like `$list[$k]['key'] = value`. - Update bug-10089 test assertion: `$matrix[$size - 1][8] = 3` where `$size` is `int<min, 8>` correctly degrades the list since `int<min, 7>` is not within the `int<0, max>` key range.
staabm
left a comment
There was a problem hiding this comment.
the suggested change does not fix the reproducer of phpstan/phpstan#14336
… reset When processing nested array dim fetch assignments like `$arr[$key][$int] = $value` where the container was previously reset (`$arr[$key] = []`), the list type was incorrectly preserved. This happened because `produceArrayDimFetchAssignValueToWrite` used `setExistingOffsetValueType` (which preserves list structure) based on the general type from the parent's type system, while the actual tracked container type was an empty array where the offset couldn't exist. The fix adds a check at container levels (inner dimensions of nested dim fetches) to verify that the scope's actual tracked type of the container also supports the offset before using `setExistingOffsetValueType`. This correctly falls through to `setOffsetValueType` when the container was just reset to `[]`, which properly degrades the list type. This addresses the actual reproducer from phpstan/phpstan#14336 where the previous IntersectionType::setOffsetValueType fix alone was insufficient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The fix has been pushed. Here's a summary of what was done: Root Cause AnalysisThe reviewer (staabm) was correct - the original $xsdFiles[$xmlNamespace] = []; // Reset to empty array
foreach (...) {
$xsdFiles[$xmlNamespace][$int] = $viewHelper; // Nested dim fetch with arbitrary int
}
// $xsdFiles[$xmlNamespace] was wrongly kept as list<...>The bug was in
But the actual tracked type of the container was FixAdded a check at container levels (inner dimensions of nested dim fetches) that also verifies the scope's actual tracked type supports the offset: $isContainerLevel = $i + 1 < count($dimFetchStack);
// ...
&& (!$isContainerLevel || !$scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->no())This only applies at inner levels where The |
Summary
When assigning to a
list<T>with an arbitraryintkey (e.g.,$list[$intKey] = valuewhere$intKeyhas typeint), the list type was incorrectly preserved. The list should degrade toarray<int, T>because an arbitraryintoffset can include negative values or values that create gaps in the sequential key structure.Changes
src/Type/IntersectionType.php: Changed the condition insetOffsetValueTypethat re-addsAccessoryArrayListTypeafter an offset assignment. Previously it checked$offsetType->toArrayKey()->isInteger()->yes()(any integer offset), now it checks$this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes()(offset must be within the list's key type range, i.e.,int<0, max>).tests/PHPStan/Analyser/nsrt/bug-10089.php: Updated assertion that was relying on the buggy behavior.$matrix[$size - 1][8] = 3where$sizeisint<min, 8>(could be negative) correctly degrades the list sinceint<min, 7>is not a subtype ofint<0, max>.tests/PHPStan/Analyser/nsrt/bug-14336.php: New regression test covering:intkey in loop → list degraded to arrayintkey outside loop → list degradedint<0, max>key with array value → list preserved (valid key range for nested modifications)int<0, max>key with scalar value → list degraded (lines 987-994 only apply for array values)[]) → list preservedRoot cause
IntersectionType::setOffsetValueTypehad a guard condition that re-addedAccessoryArrayListTypewhen the original type was a list, the offset was any integer, and the iterable value type was an array. This was originally added to preserve list types during nested dim fetch operations (e.g.,$list[$k]['key'] = value), but the condition$offsetType->toArrayKey()->isInteger()->yes()was too broad — it also fired for arbitraryintoffsets like those fromforeach ($intMap as $intKey => ...)where$intKeyhas typeint(including negatives).The fix tightens this to
$this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes(), which for lists checks that the offset is withinint<0, max>. This correctly rejectsint(includes negatives) while still acceptingint<0, max>(the type of foreach keys when iterating a list).Analogous constructs probed
IntersectionType::setExistingOffsetValueType: No similar pattern; delegates directly to member types.AccessoryArrayListType::setExistingOffsetValueTypereturns$thiswhich is correct for known-existing offsets.IntersectionType::unsetOffset: No list re-addition logic; delegates to member types.UnionType::setOffsetValueType: NoAccessoryArrayListTypehandling.ConstantArrayType::setOffsetValueType: UsesConstantArrayTypeBuilderwhich properly checks for gaps via internal$isListtracking. Already correct.Test
tests/PHPStan/Analyser/nsrt/bug-14336.php: Regression test for the reported bug (arbitrary int key in loop) plus 7 additional edge cases covering negative ints,int<0, max>offsets, nested assignments, and safe patterns (append, constant 0).tests/PHPStan/Analyser/nsrt/bug-10089.phpto reflect corrected behavior.Fixes phpstan/phpstan#14336