Restrict list type preservation in IntersectionType::setOffsetValueType to offsets within list key range#5455
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
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.
Contributor
|
the suggested change does not fix the reproducer of phpstan/phpstan#14336 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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