Skip to content

Add regression test for $this narrowing to never inside isset()-guarded boolean decomposition#5840

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

Add regression test for $this narrowing to never inside isset()-guarded boolean decomposition#5840
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-w51v4bo

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

PHPStan 2.2.2 introduced a regression where, inside an if (isset($this->prop[$key]))
block that followed an earlier compound if ($enum === X && isset($this->other[$key]))
guard, $this was narrowed to *NEVER*. This marked the rest of the block unreachable
and produced cascading false positives such as Cannot access offset *NEVER* on mixed.

The root cause was already fixed on the 2.2.x branch (the never-narrowing guard in
ConditionalExpressionHolderHelper). This change adds the missing regression test that
captures the exact reproduction from the issue and proves the family of cases is covered.

Changes

  • Added tests/PHPStan/Analyser/nsrt/bug-14804.php, a faithful copy of the container
    resolve() method from the issue, asserting that $this keeps its
    $this(Bug14804\Container) type before and inside the nested if ($lifecycle === Lifecycle::PERSISTENT)
    block, and that $lifecycle stays Lifecycle::PERSISTENT|Lifecycle::TRANSIENT.

Root cause

The cross-kind conditional-expression holders created when decomposing a
BooleanAnd/BooleanOr condition containing isset() could compute a holder type
that collapses to NeverType (e.g. removing a non-nullable property's full type after
the isset() truthy/falsey swap). Applying such a holder in a later scope narrowed an
unrelated expression — here $this — to never, marking the whole scope unreachable.
ConditionalExpressionHolderHelper now skips any holder whose computed narrowing is
NeverType while the original expression type is not never, since that result is an
artifact rather than a real contradiction. The guard lives in the shared helper, so it
covers instance/static properties, isset()/empty(), and both the BooleanAnd
(false context) and BooleanOr (true context) paths.

Test

  • tests/PHPStan/Analyser/nsrt/bug-14804.php reproduces the reported bug. It fails with
    Actual: *NEVER* when the NeverType guard in ConditionalExpressionHolderHelper is
    removed and passes with it in place.
  • Additionally probed analogous constructs — empty(), array_key_exists(), ??,
    BooleanOr early returns, static properties, narrowing non-$this locals and match
    results, and deeper nesting — and confirmed each is already correctly handled by the
    shared guard, so no further source change is required.

Fixes phpstan/phpstan#14804

…uarded boolean decomposition

- Adds tests/PHPStan/Analyser/nsrt/bug-14804.php reproducing the reported case
  verbatim from the issue's container code: an outer `$enum === X && isset($this->prop[$key])`
  guard followed by an inner `isset($this->other[$key])` block and a nested
  `if ($enum === X)`, where `$this` was incorrectly narrowed to `*NEVER*`.
- The test asserts `$this` keeps its `$this(Bug14804\Container)` type both before
  and inside the nested narrowing, and that `$lifecycle` stays the full enum union.
- The root fix already lives in ConditionalExpressionHolderHelper: the
  boolean-decomposition holder that collapsed an expression to `NeverType`
  (an artifact of the cross-kind holder + isset() truthy/falsey swap) is skipped.
  Verified the new test fails with `*NEVER*` when that guard is removed and passes
  with it.
- Probed analogous constructs (empty(), array_key_exists(), `??`, BooleanOr early
  returns, static properties, narrowing non-`$this` locals and match results, and
  deeper nesting) — all already handled by the shared guard, no residual bug.
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.

PHPStan 2.2.2 regression: Type narrowing $this to *NEVER*

1 participant