Skip to content

Preserve ArrayDimFetch expression tracking through loop generalization#5661

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

Preserve ArrayDimFetch expression tracking through loop generalization#5661
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-bic8xz0

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

  • Fixes false positive "Offset ... on array ... might not exist" errors that occurred when an array dim fetch like $tmp[$id] was explicitly assigned inside a loop but its expression holder was removed during loop generalization of the parent variable
  • Introduces a trackingOnly flag on ExpressionTypeHolder that preserves expression tracking through generalization while letting resolveType compute types fresh from the parent array via the ExprHandler
  • Adds isExpressionTrackingOnly() guards in AssignHandler to prevent tracking-only holders from triggering setExistingOffsetValueType paths (which would cause type precision regressions)

Test plan

  • New regression test testBug14598 with three scenarios: original reproducer, simple nested foreach, nested while loop — all produce no errors
  • Existing pr-4390 test passes (no type precision regression on int<0, 9>)
  • Existing bug-10438 test passes (no type precision regression on list<string>|string)
  • make tests — 12063 tests, 79683 assertions pass
  • make phpstan — no errors
  • make cs-fix — no violations

Closes phpstan/phpstan#14598

When a parent expression is generalized during loop fixpoint iteration,
child ArrayDimFetch expressions are now preserved with a trackingOnly
flag instead of being removed. This prevents false positive
"nonexistent offset" errors on array dim fetches that were explicitly
assigned earlier in the same code path.

The trackingOnly flag decouples expression tracking (hasExpressionType
returns Yes, so the NonexistentOffsetInArrayDimFetchRule skips the
check) from type resolution (resolveType falls through to the
ExprHandler to compute the type fresh from the parent, avoiding stale
stored types).

Closes phpstan/phpstan#14598
Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reading the error message

Offset 'A'|'B'|'C' might not exist on non-empty-array{A?: list<'00'|'01'|'02'|'03'>, B?: list<'00'|'01'|'02'|'03'>, C?: list<'00'|'01'|'02'|'03'>}.

I think the type-system should be able to tell that a non-empty-array with known keys A|B|C will contain offset A|B|C

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

The branch is already up to date with origin — no additional commits or pushes needed.

Summary: After thorough investigation of the type-system approach, I've confirmed the trackingOnly mechanism is the minimal correct solution. The type-system cannot distinguish "this key was explicitly assigned in the current loop iteration" from "this key may or may not exist in a non-empty array" — that's a scope/flow-analysis concern, not a type concern. The trackingOnly flag bridges this gap by preserving expression tracking through generalization while preventing stale types from causing precision regressions.

@ondrejmirtes
Copy link
Copy Markdown
Member

@staabm Hey, the message is correct.

@ondrejmirtes
Copy link
Copy Markdown
Member

Offset 'A'|'B'|'C' might not exist on non-empty-array{A?: list<'00'|'01'|'02'|'03'>, B?: list<'00'|'01'|'02'|'03'>, C?: list<'00'|'01'|'02'|'03'>}.

You might have an array with key A (it's non empty and conforms to the type) and the offset 'A'|'B'|'C' might B so PHP will emit a notice about undefined offset.

@staabm
Copy link
Copy Markdown
Contributor

staabm commented May 14, 2026

You are right. Sealed vs. Unsealed territory

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.

3 participants