Skip to content

Narrow array key type after type-checking the key variable inside a foreach loop#5505

Merged
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-n6ot4vw
Apr 21, 2026
Merged

Narrow array key type after type-checking the key variable inside a foreach loop#5505
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-n6ot4vw

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When iterating over an array with foreach and narrowing the key type inside the loop body (e.g. via is_string($key) + throw), the narrowed key type was not reflected in the array type after the loop. This extends the value-type narrowing mechanism (added in #4534) to also narrow key types.

Changes

  • Modified src/Analyser/NodeScopeResolver.php: In the foreach post-processing block that already handles value-type narrowing, added parallel collection of key variable types from the same scopes. When either the value type or key type (or both) changed, the new array type uses the narrowed types.
  • Added tests/PHPStan/Analyser/nsrt/bug-7076.php with comprehensive test cases covering:
    • Key narrowing via is_string() + throw
    • Key narrowing via is_int() + throw
    • Key narrowing to int keys
    • Key narrowing via return
    • Key narrowing via assert()
    • Combined key + value narrowing
    • Non-empty-array preservation with key narrowing
    • Property access as iterable expression
    • Cases that correctly do NOT narrow: continue without narrowing, break, key reassignment, no key variable, partial narrowing with continue

Root cause

The existing foreach post-processing (from PR #4534) already collected scopes where the key variable hadn't been reassigned (scopesWithIterableValueType) and used them to detect value-type changes via $scope->getType(new ArrayDimFetch($expr, $keyVar)). However, it did not also check whether the key variable's own type had been narrowed in those same scopes. The fix adds parallel key-type collection using $scope->getType($stmt->keyVar) and applies key narrowing when the combined key type differs from the original iterable key type.

Analogous cases probed

  • Other loop constructs (while, for): Not applicable — only foreach provides direct key/value binding to an array's structure.
  • continue exit points: Tested and correctly handled — when a non-narrowed continue path exists, the key type union includes the original type, so no narrowing occurs.
  • break exit points: Already blocked by the existing count($breakExitPoints) === 0 guard.
  • Key reassignment: Already blocked by the existing OriginalForeachKeyExpr mechanism.
  • Property access iterables: Tested via $this->prop — works correctly through the assignExpression fallback path.

Test

Regression test tests/PHPStan/Analyser/nsrt/bug-7076.php with 13 test functions covering the reported bug and all analogous edge cases listed above.

Fixes phpstan/phpstan#7076

…foreach` loop

- Extend the existing value-type narrowing mechanism in NodeScopeResolver's
  foreach handling to also track and apply key-type narrowing
- Collect key variable types from loop body end scopes and continue exit
  point scopes (same scopes used for value narrowing via OriginalForeachKeyExpr)
- When the combined key type differs from the original iterable key type,
  create a new array type with the narrowed key type
- Supports both key-only narrowing and combined key+value narrowing
- Correctly does NOT narrow when: key variable is reassigned, break is used,
  continue without narrowing on all paths, or no explicit key variable
@ondrejmirtes ondrejmirtes merged commit d2d6328 into phpstan:2.1.x Apr 21, 2026
655 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-n6ot4vw branch April 21, 2026 19:42
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