Skip to content

Keep unions of general array types separate#5506

Draft
ondrejmirtes wants to merge 2 commits into2.1.xfrom
unions-of-arrays
Draft

Keep unions of general array types separate#5506
ondrejmirtes wants to merge 2 commits into2.1.xfrom
unions-of-arrays

Conversation

@ondrejmirtes
Copy link
Copy Markdown
Member

Summary

  • Under bleeding edge, TypeCombinator::processArrayTypes no longer collapses multiple general array members into a single ArrayType with unioned keys and values, so array<int>|array<string> stays distinct from array<int|string> (fixes Union of arrays is not array of unions phpstan#8963). Subsumption (array<int>|array<int|string>array<int|string>) is handled by the existing outer union() loop via compareTypesInUnion.
  • The array{} | non-empty-array<X>array<X> simplification is preserved by falling back to the old collapse path whenever an empty constant array is present — that merge is lossless and not expressible as a keep-separate result.
  • Narrowing works out of the box: is_string($list[0]) on list<int>|list<string> narrows to list<string> (with hasOffsetValue(0, string)).
  • 34 existing fixtures updated to reflect the more-precise output. 10 known regressions remain in cases like array<mixed~'foo'>|non-empty-array<mixed> where the old collapse was lossless; those need a follow-up heuristic before removing the WIP gate.

TMP WIP commit (8c3554c54c) runs the new path for everyone, not just bleeding edge, so CI exercises it. Revert before merge.

Test plan

  • tests/PHPStan/Analyser/nsrt/array-union-keep-separate.php added — covers the issue repro, subsumption cases, narrowing via offset access, constant + general mixing, iteration
  • NodeScopeResolverTest green except 10 known Bucket B files
  • LegacyNodeScopeResolverTest, core Type unit tests green
  • TypeCombinatorTest has 18 describe-output failures under the WIP (no bleedingEdge); expected, to be re-baselined
  • Address Bucket B collapse (array<X>|non-empty-array<Y>) before removing WIP gate

🤖 Generated with Claude Code

Previously `TypeCombinator::processArrayTypes` collapsed multiple general
array members into a single `ArrayType` with unioned keys and values, so
`array<int>|array<string>` was indistinguishable from `array<int|string>`
(phpstan/phpstan#8963). Under `BleedingEdgeToggle::isBleedingEdge()` the
members are now kept as distinct union branches; the outer `union()` loop
handles subsumption via `compareTypesInUnion`. The `array{} | non-empty-
array<X>` -> `array<X>` simplification is preserved by falling back to the
old collapse path whenever an empty constant array is present.
Drops the BleedingEdgeToggle gate so CI exercises the new path on all
configs. Revert before merging.
@ondrejmirtes ondrejmirtes marked this pull request as draft April 21, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant