Skip to content

Fix phpstan/phpstan#14455: missing has-offset via conditional type#5446

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

Fix phpstan/phpstan#14455: missing has-offset via conditional type#5446
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-cu143yt

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a compound condition like if (empty($aggregation['field']) && $type === 'filter') { return; } was used as an early return, PHPStan failed to narrow $aggregation to hasOffset('field') inside a subsequent if ($type === 'filter') branch.

The type system correctly tracks that after the early return, !empty($aggregation['field']) || $type !== 'filter' holds. However, when later checking $type === 'filter', it should deduce via modus tollens that !empty($aggregation['field']) must be true, meaning $aggregation has the 'field' offset. This deduction was not happening.

Changes

  • Added processBooleanNotSureSureConditionalTypes method to src/Analyser/TypeSpecifier.php that creates conditional expression holders by cross-pairing sureNotType conditions with sureType results
  • Registered the new method in both BooleanAnd (false context) and BooleanOr (true context) conditional holder generation alongside the existing four methods

Root cause

The TypeSpecifier creates ConditionalExpressionHolder objects to track deferred type narrowing relationships in compound boolean conditions. Two existing methods handle this:

  • processBooleanSureConditionalTypes: pairs sureTypes (conditions) with sureTypes (results)
  • processBooleanNotSureConditionalTypes: pairs sureNotTypes (conditions) with sureNotTypes (results)

In the bug scenario, empty($aggregation['field']) in the falsey context produces a sureType for $aggregation (HasOffsetType), while $type === 'filter' in the falsey context produces a sureNotType for $type (ConstantStringType). Since no method cross-paired sureNotTypes with sureTypes, the conditional relationship was never recorded.

Additionally, the existing processBooleanSureConditionalTypes couldn't use the HasOffsetType as a condition because TypeCombinator::remove(array<string, mixed>, HasOffsetType('field')) returns the same type (the condition is "trivially true"), causing it to be skipped.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14455.php which reproduces the exact scenario from the issue: an early return with empty($arr['key']) && $type === 'filter', followed by a check on $type === 'filter' where $arr should be narrowed to non-empty-array<string, mixed>&hasOffset('field').

Fixes phpstan/phpstan#14455

- Added processBooleanNotSureSureConditionalTypes method to TypeSpecifier
  that cross-pairs sureNotType conditions with sureType results
- Registered the new method in both BooleanAnd (false context) and
  BooleanOr (true context) conditional holder generation
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14455.php
- Root cause: existing methods only paired sureTypes with sureTypes and
  sureNotTypes with sureNotTypes, missing the cross-pairing needed when
  one side of a compound condition produces sureNotTypes and the other
  produces sureTypes (e.g. empty($arr['key']) && $type === 'filter')
@staabm staabm closed this Apr 12, 2026
@staabm staabm deleted the create-pull-request/patch-cu143yt branch April 12, 2026 10:23
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