Skip to content

Fix phpstan/phpstan#11533: Foreach to check the type of array is not taken into account#5318

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

Fix phpstan/phpstan#11533: Foreach to check the type of array is not taken into account#5318
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-1kica2g

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When iterating over a constant string array like ['need', 'field'] in a foreach loop, type narrowing from isset() and is_string() checks inside the loop body was not preserved after the loop. This meant PHPStan couldn't verify that the array had the expected shape after the loop completed.

Changes

  • Added constant array foreach unrolling in src/Analyser/NodeScopeResolver.php (after the existing foreach analysis): when the iterable is a small constant array that iterates at least once, the loop body is re-processed once per element with specific constant key/value types, accumulating type narrowing
  • Added applyNarrowingsFromForeachUnroll() method to src/Analyser/MutatingScope.php: selectively transfers only HasOffsetType and HasOffsetValueType accessory types from the unrolled scope to the final scope, without affecting base array structure (avoiding interference with array mutations in the loop body)
  • Added regression test in tests/PHPStan/Analyser/nsrt/bug-11533.php

Root cause

PHPStan's loop analysis uses fixed-point iteration where the loop variable holds the union of all possible values (e.g., 'need'|'field'). When isset($param[$field]) is evaluated with this union-typed key, the TypeSpecifier can only produce a NonEmptyArrayType narrowing (not per-key HasOffsetType), because narrowing for all union members would be unsound in non-loop contexts. Similarly, is_string($param[$field]) narrows the expression $param[$field] but can't decompose this into per-key HasOffsetValueType constraints on $param.

The fix adds a post-loop unrolling pass specifically for constant arrays: the loop body is re-analyzed once per element with the specific constant value, so isset($param['need']) and is_string($param['need']) produce the precise per-key narrowing. Only accessory types (HasOffsetType/HasOffsetValueType) are transferred from the unrolled scope to the final scope, ensuring array mutations in the loop body are unaffected.

Test

The regression test verifies that after foreach (['need', 'field'] as $field) { if (!isset($param[$field]) || !is_string($param[$field])) throw; }, the type of $param is correctly narrowed to non-empty-array<mixed>&hasOffsetValue('field', string)&hasOffsetValue('need', string). Tests with and without a key variable are included, as well as a function call test matching the original issue.

Fixes phpstan/phpstan#11533

… narrowing

- Added foreach unrolling for constant arrays in NodeScopeResolver: after the
  normal loop analysis, the body is re-processed once per element with specific
  constant values, accumulating type narrowing across iterations
- Added MutatingScope::applyNarrowingsFromForeachUnroll() to selectively transfer
  only HasOffsetType/HasOffsetValueType accessory types from the unrolled scope
  to the final scope, avoiding interference with array mutations
- New regression test in tests/PHPStan/Analyser/nsrt/bug-11533.php
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