Skip to content

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
phpstan-bot:create-pull-request/patch-0owr5zt
Closed

Skip contradictory accessory types on empty arrays and use inner traverser in optimizeConstantArrays value generalization#5575
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-0owr5zt

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes a regression introduced in 2.1.52 where the oversized-array generalization in TypeCombinator produced a type that was not a super-type of the variants it was derived from, causing generator.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.phpprocessArrayTypes (~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 using accepts(). This prevents contradictory intersections like array{}&oversized-array.
  • src/Type/TypeCombinator.phpoptimizeConstantArrays (~line 1070): Two sub-fixes in the stage 2 inner value traverser:
    1. Skip wrapping empty constant arrays with OversizedArrayType — they would produce the same contradictory intersection.
    2. Fall through via $innerTraverse (the inner TypeTraverser::map callback) instead of $traverse (the outer one). The outer callback fully generalizes sealed ConstantArrayTypes into array<intKey, V>&..., which is correct at the top level but produces asymmetric treatment inside value positions.
  • Added use function array_filter; import.

Analogous cases probed:

  • All other OversizedArrayType construction sites (ConstantArrayTypeBuilder, OversizedArrayBuilder, MutatingScope, MixedType) were checked — they all guard against empty arrays or require both sides to be oversized. No analogous bugs found.
  • Other accessory array types (NonEmptyArrayType, AccessoryArrayListType, HasOffsetType, HasOffsetValueType) are not promoted via the OversizedArrayType special case in processArrayAccessoryTypes, 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:

  1. processArrayAccessoryTypes has a special case that promotes OversizedArrayType to common accessory types even when not all array union members have it. When the reduced union includes an empty array (array{}), intersecting it with OversizedArrayType produces the contradictory array{}&oversized-array, which fails to accept the empty array values it represents.

  2. The inner TypeTraverser::map callback in optimizeConstantArrays stage 2 captured the outer $traverse callback instead of using its own $innerTraverse. This caused nested constant arrays in value positions to be fully generalized (decomposed into array<key, value>&oversized-array) instead of merely wrapped — producing an asymmetric mix of shapes that processArrayTypes could not unify cleanly.

Test

  • Rule test: tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php::testBug14560 with data file tests/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.
  • NSRT test: tests/PHPStan/Analyser/nsrt/bug-14560.php — verifies that empty sub-arrays in oversized union contexts preserve their array{} type rather than being tagged with contradictory &oversized-array.

Fixes phpstan/phpstan#14560

…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.
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-0owr5zt branch May 3, 2026 10:56
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.

2 participants