Skip to content

Resolve array offset access on never to never and treat never operands of ===/!== as undecided#5906

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-20i99q4
Open

Resolve array offset access on never to never and treat never operands of ===/!== as undecided#5906
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-20i99q4

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

An impossible identical comparison inside assert() over a constant array — e.g. assert($array[1] === null) where $array[1] is 0 — collapses the whole array to never (which is correct). The bug was that the following statements then produced a cascade of spurious Strict comparison using === between *NEVER* and null will always evaluate to false. errors, and that reading a further offset of the now-never array produced an *ERROR* type instead of never.

This was the regression behind the revert of d7ba1e3 ("More precise array-item types in loops"). This change fixes the root cause so the precision improvement no longer produces the cascade.

Changes

  • src/Analyser/ExprHandler/ArrayDimFetchHandler.php — short-circuit resolveType() when the offset-accessible type is NeverType, returning never. Previously, because never is a subtype of everything (including ArrayAccess), the fetch was routed through offsetGet() and produced *ERROR*.
  • src/Reflection/InitializerExprTypeResolver.phpresolveIdenticalType() now returns a non-constant BooleanType (instead of ConstantBooleanType(false)) when either operand is never, so ===/!== no longer reports always-true/false on already-unreachable code.
  • tests/PHPStan/Analyser/data/bug-9307.php — the loop case now correctly infers array<int, Bug9307\Item> instead of array<*ERROR*> (the inline comment already predicted this).
  • tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php — the last-match-arm expectations drop the two *NEVER* === 'ccc' reports, plus a new testBug14281.
  • Added regression tests tests/PHPStan/Analyser/nsrt/bug-14281.php and tests/PHPStan/Rules/Comparison/data/bug-14281.php.

Root cause

Two independent spots mishandled never:

  1. Offset read on never. ArrayDimFetchHandler checks (new ObjectType(ArrayAccess::class))->isSuperTypeOf($type)->yes() to decide whether to call offsetGet(). never satisfies that test (it is a subtype of every type), so an offset read on a never array was treated as an ArrayAccess::offsetGet() call and yielded *ERROR*. NeverType::getOffsetValueType() already returns never; the handler just never reached it.

  2. Identical comparison with a never operand. resolveIdenticalType() returned a constant false, which the StrictComparisonOfDifferentTypesRule / ImpossibleCheckType* rules report as "always false". A never operand denotes unreachable code that carries no comparable value, so the result should be undecided — consistent with how never already behaves as a boolean condition (if, &&, ||) and with NeverType::looseCompare() for ==/!=.

Test

  • nsrt/bug-14281.php asserts that after assert($array[1] === null) the array is *NEVER*, that $array[2] is *NEVER* (not *ERROR*), and that $i === null / $i !== null on a never variable are inferred as bool. It fails without the fix (Actual: *ERROR* and constant-boolean results).
  • Rules/Comparison/data/bug-14281.php + testBug14281 confirm the rule only reports the genuinely-impossible first comparisons (null === null, 0 === null, int !== int) and no longer reports the unreachable *NEVER* === null / *NEVER* !== null follow-ups.
  • Probed analogous comparisons: ==/!= already produce a non-constant boolean via NeverType::looseCompare(); <=>/</> produce never without an always-true/false report. No change needed for those.

Fixes phpstan/phpstan#14281

…perands of `===`/`!==` as undecided

- ArrayDimFetchHandler::resolveType() now short-circuits when the
  offset-accessible type is NeverType. Because never is a subtype of
  everything (including ArrayAccess), the fetch was otherwise resolved
  through offsetGet() and produced an *ERROR* type instead of never.
- InitializerExprTypeResolver::resolveIdenticalType() returns a
  non-constant BooleanType (instead of ConstantBooleanType(false)) when
  either operand is never. A never-typed operand has no value to compare,
  so the comparison is undecided. This mirrors how never already behaves
  as a boolean condition (if/&&/||) and stops StrictComparison /
  ImpossibleCheck rules from piling always-true/false errors onto
  already-unreachable code.
- Together these let an impossible assertion such as
  `assert($array[1] === null)` collapse the array to never without
  emitting a cascade of `*NEVER* === ...` comparison errors on the
  following statements.
- Updated the last-match-arm rule test (the `*NEVER* === 'ccc'` reports
  are now suppressed) and bug-9307 (`array<*ERROR*>` is now correctly
  inferred as `array<int, Bug9307\Item>`).
- Probed siblings: `==`/`!=` already resolve to a non-constant boolean
  via NeverType::looseCompare(); `<=>`/`<`/`>` yield never without an
  always-true/false report, so no change was needed there.
@staabm staabm force-pushed the create-pull-request/patch-20i99q4 branch from b5b4dbe to 661cf30 Compare June 21, 2026 15:46
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.

type narrowed too much after identical comparison

2 participants