Skip to content

Preserve constant scalar types in same-key constant array generalization during loop widening#5470

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k8qdlgr
Open

Preserve constant scalar types in same-key constant array generalization during loop widening#5470
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-k8qdlgr

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When constant string values in a fixed-shape array are conditionally reassigned inside a loop (using a boolean flag pattern), PHPStan's loop widening over-generalizes the value types from specific constants like 'xxx'|'yyy'|'zzz' to literal-string&non-falsy-string, causing false positive return type errors.

Changes

  • Modified src/Analyser/MutatingScope.php in the generalizeType() method's constant array same-keys handler
  • Before recursing into generalizeType for each key's value type, check whether the value types are converging (one side encompasses the other via isSuperTypeOf)
  • Only apply this optimization for non-integer, non-array value types to preserve:
    • Integer range-based widening (counters correctly widen to int<min, max>)
    • Nested array structure generalization (growing lists correctly generalize)
  • Updated tests/PHPStan/Analyser/nsrt/array-keys-branches.php — one assertion now reflects more precise constant string inference ('bar'|'baz'|'foo' instead of the previous literal-string&lowercase-string&non-falsy-string)

Root cause

During loop analysis, NodeScopeResolver calls generalizeWith() to widen scope types and ensure convergence. For constant arrays with fixed keys, generalizeType() recursed into each key's value type. When the value types were constant strings that differed slightly between iterations (due to conditional assignments controlled by a boolean flag), the constant string generalization converted them to literal-string&non-falsy-string, losing all constant information.

The fix adds a convergence check (isSuperTypeOf) specifically in the constant array same-keys handler. When one iteration's value type already encompasses the other's (indicating the types are converging from a bounded set), the broader type is kept without further generalization. This check is guarded to only apply for non-integer, non-array types, ensuring that integer counters and nested array structures continue to use the standard widening behavior.

Analogous cases probed

  • While/for loops: Same generalizeWith() code path — verified with regression tests (same fix applies)
  • Float constants in arrays: Same constant scalar handling — verified with regression test
  • Integer constants in arrays: Different code path (range-based widening) — intentionally excluded from the fix to preserve counter widening behavior
  • Standalone scalar variables: Not fixed by this change (would require modifying the general constant scalar block, which breaks string concatenation widening and growing-array key generalization)
  • Growing arrays (different keys): Uses a different code path — verified not affected
  • Nested array structures: Excluded via isArray()->no() guard — verified not affected

Test

  • tests/PHPStan/Analyser/nsrt/bug-12653.php — regression test for the reported bug: constant string array values with flag-based conditional assignment in a foreach loop
  • tests/PHPStan/Analyser/nsrt/bug-12653b.php — analogous cases: while loop, for loop, and float constants in arrays

Fixes phpstan/phpstan#12653

…ion during loop widening

- In `MutatingScope::generalizeType()`, when generalizing constant
  arrays with matching keys, skip recursive `generalizeType` for
  non-integer, non-array value types when one side's type already
  encompasses the other (convergence check via `isSuperTypeOf`)
- This prevents constant string and float values from being
  over-generalized to `literal-string&non-falsy-string` or `float`
  when they come from a bounded set of class constants
- Integer values still use the full `generalizeType` path for proper
  range-based widening (e.g. counters → `int<min, max>`)
- Array values still use the full path for proper structure
  generalization (e.g. growing lists)
- Updated one existing test assertion that now correctly infers more
  precise constant string types instead of the previous
  over-generalized `literal-string` type
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