Skip to content

Fix phpstan/phpstan#10172: Walk through a generic array is messed up with non-empty-array type#5356

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

Fix phpstan/phpstan#10172: Walk through a generic array is messed up with non-empty-array type#5356
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-sehpq7w

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When iterating over an array dim fetch like $a['data'] where $a has a template type T of array{data: array<mixed>}, PHPStan's foreach handling would split the scope into "entered loop" and "didn't enter loop" branches. This caused the template type T to be replaced with array{data: array{}}|array{data: non-empty-array<mixed>}, producing a false positive on the return type check.

Changes

  • Modified src/Analyser/MutatingScope.php: Added a check in specifyExpressionType() to skip HasOffsetValueType propagation from an ArrayDimFetch to its parent variable when the parent has a TemplateType. The dim fetch expression's type is still stored separately and resolved correctly.
  • Added tests/PHPStan/Analyser/nsrt/bug-10172.php: Regression test verifying the template type is preserved after foreach iteration over an array dim fetch.

Root cause

In MutatingScope::specifyExpressionType(), when narrowing an ArrayDimFetch expression (e.g., $a['data'] to array{}), the method propagates the narrowing to the parent variable by intersecting with HasOffsetValueType. For template types like T of array{data: array<mixed>}, this intersection destroys the template identity, replacing it with a concrete type. After the foreach's scope merge (combining the "loop executed" and "loop didn't execute" branches), the result was a union of concrete types instead of the original template type.

The fix skips this propagation when the parent variable has a template type. This is safe because the dim fetch expression's narrowed type is stored independently in the expression types map, and getType() resolves it directly without needing the parent to be narrowed.

Test

Added tests/PHPStan/Analyser/nsrt/bug-10172.php which asserts that after foreach ($a['data'] as $i), the type of $a remains T of array{data: array<mixed>} instead of being split into array{data: array{}}|array{data: non-empty-array<mixed>}.

Fixes phpstan/phpstan#10172

…te type

- Skip HasOffsetValueType propagation to parent variable in specifyExpressionType when the variable has a template type
- The root cause was that foreach's empty/non-empty scope split narrowed the parent variable through ArrayDimFetch, destroying the template identity
- New regression test in tests/PHPStan/Analyser/nsrt/bug-10172.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