Skip contradictory accessory types on empty arrays and use inner traverser in optimizeConstantArrays value generalization#5575
Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Conversation
…erser in `optimizeConstantArrays` value generalization
- In `processArrayTypes`, filter out accessory types that reject empty
arrays (e.g. `OversizedArrayType`, `NonEmptyArrayType`) before
intersecting them with reduced arrays that are empty. Without this,
`processArrayAccessoryTypes`'s special promotion of `OversizedArrayType`
(when present on some but not all union members) produces contradictory
intersections like `array{}&oversized-array`.
- In `optimizeConstantArrays` stage 2, use `$innerTraverse` (the inner
`TypeTraverser::map` callback) instead of `$traverse` (the outer one)
when recursing into value positions. The outer callback fully generalizes
a sealed `ConstantArrayType` into `array<intKey, V>&...`, which is
correct at the top level but wrong inside a value position — it treats
nested shapes inconsistently depending on how they are reached.
- Also skip wrapping empty constant arrays with `OversizedArrayType` in
the inner traverser, for the same contradictory-intersection reason.
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
Fixes a regression introduced in 2.1.52 where the oversized-array generalization in
TypeCombinatorproduced a type that was not a super-type of the variants it was derived from, causinggenerator.valueType(and similar) false positives where a closure's inferred Generator value type rejected the very yields it was synthesized from.Changes
src/Type/TypeCombinator.php—processArrayTypes(~line 962): When applying common array accessory types to a reduced array result that is already known empty (isIterableAtLeastOnce()->no()), filter out accessory types that reject empty arrays usingaccepts(). This prevents contradictory intersections likearray{}&oversized-array.src/Type/TypeCombinator.php—optimizeConstantArrays(~line 1070): Two sub-fixes in the stage 2 inner value traverser:OversizedArrayType— they would produce the same contradictory intersection.$innerTraverse(the innerTypeTraverser::mapcallback) instead of$traverse(the outer one). The outer callback fully generalizes sealedConstantArrayTypes intoarray<intKey, V>&..., which is correct at the top level but produces asymmetric treatment inside value positions.use function array_filter;import.Analogous cases probed:
OversizedArrayTypeconstruction sites (ConstantArrayTypeBuilder,OversizedArrayBuilder,MutatingScope,MixedType) were checked — they all guard against empty arrays or require both sides to be oversized. No analogous bugs found.NonEmptyArrayType,AccessoryArrayListType,HasOffsetType,HasOffsetValueType) are not promoted via theOversizedArrayTypespecial case inprocessArrayAccessoryTypes, so they are not affected by this specific bug path.Root cause
The regression was exposed by commit 2f66c45 ("Preserve constant array when assigning a union of scalars") in 2.1.52. That commit was a correct precision improvement, but it exposed two latent bugs:
processArrayAccessoryTypeshas a special case that promotesOversizedArrayTypeto common accessory types even when not all array union members have it. When the reduced union includes an empty array (array{}), intersecting it withOversizedArrayTypeproduces the contradictoryarray{}&oversized-array, which fails to accept the empty array values it represents.The inner
TypeTraverser::mapcallback inoptimizeConstantArraysstage 2 captured the outer$traversecallback instead of using its own$innerTraverse. This caused nested constant arrays in value positions to be fully generalized (decomposed intoarray<key, value>&oversized-array) instead of merely wrapped — producing an asymmetric mix of shapes thatprocessArrayTypescould not unify cleanly.Test
tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php::testBug14560with data filetests/PHPStan/Rules/Generators/data/bug-14560.php— a domain-agnostic reproducer with 9 yields containing deeply nested constant arrays with empty sub-arrays. Verified to fail without the fix (9+ errors) and pass with it.tests/PHPStan/Analyser/nsrt/bug-14560.php— verifies that empty sub-arrays in oversized union contexts preserve theirarray{}type rather than being tagged with contradictory&oversized-array.Fixes phpstan/phpstan#14560