Skip to content

Fix phpstan/phpstan#14369: Null coalescing assignment operator combined with throwing#5297

Merged
VincentLanglet merged 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-0ehzlyp
Mar 25, 2026
Merged

Fix phpstan/phpstan#14369: Null coalescing assignment operator combined with throwing#5297
VincentLanglet merged 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-0ehzlyp

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When using $test ??= throw new Exception(), PHPStan incorrectly treated the entire expression as always-terminating, causing false positives: "Function always throws an exception" and "Unreachable statement - code above always terminates." The RHS of ??= only executes when the LHS is null, so the expression should not be considered always-terminating.

Changes

  • Fixed src/Analyser/ExprHandler/AssignOpHandler.php: Changed isAlwaysTerminating from $exprResult->isAlwaysTerminating() to false for Expr\AssignOp\Coalesce, since the RHS only runs in the null branch
  • Added tests/PHPStan/Analyser/nsrt/bug-14369.php: NSRT test verifying type narrowing to string after ??= throw
  • Added tests/PHPStan/Rules/DeadCode/data/bug-14369.php and test method in UnreachableStatementRuleTest.php: Rule test verifying no false positive unreachable statement

Root cause

In AssignOpHandler, when processing ??=, the isAlwaysTerminating flag from the RHS expression result was propagated directly. When the RHS is throw new Exception(), this flag is true, making PHPStan think the entire ??= expression always terminates. However, ??= only evaluates the RHS when the LHS is null — the non-null branch continues execution normally. The fix sets isAlwaysTerminating to false for coalesce assignment expressions.

Test

  • tests/PHPStan/Analyser/nsrt/bug-14369.php — verifies that after $test ??= throw new Exception(), $test is correctly narrowed to string
  • tests/PHPStan/Rules/DeadCode/data/bug-14369.php — verifies no unreachable statement false positive after ??= throw

Fixes phpstan/phpstan#14369

…always terminating

- Fixed AssignOpHandler to not propagate isAlwaysTerminating from the RHS of ??=
- The RHS of ??= only executes when the LHS is null, so the expression cannot always terminate
- Added NSRT regression test verifying type narrowing after ??= throw
- Added UnreachableStatementRule test verifying no false positive unreachable code
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

Add a test where $test is always null and so the expression is always terminating.

Also add a test where the left expr is always terminating (so the whole left + right expression should be considered as always terminating)

…assignment

When the LHS of ??= is always null, the RHS always executes, so the
expression should be considered always-terminating if the RHS is.
When the LHS evaluation itself always terminates (e.g. calling a
never-returning function), the whole expression is always-terminating.

Added tests for both cases as requested in review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

The background make tests task also completed successfully (all 11658 tests pass). Both CI checks passed and changes are already pushed.

@VincentLanglet VincentLanglet requested a review from staabm March 25, 2026 13:21
Comment on lines +19 to +20
$test = null;
$test ??= throw new Exception();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would add another test in which $test is maybe null

like

if (rand(0,1)) {
  $test = null;
} else {
  $test = 'hallo';
}

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet Mar 25, 2026

Choose a reason for hiding this comment

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

That's already the original issue with $test being ?string (line 12)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The working tree is clean and all the reviewer feedback has already been addressed in previous commits:

  1. VincentLanglet's latest comment is just noting that the ?string case is already covered at line 12 — no action needed.
  2. staabm's request for a "maybe null" test was already addressed in commit 6435689ae.
  3. Both reviewers have approved the PR.

There are no outstanding changes to make. The branch is already up to date with the remote.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This task already completed too — all 11658 tests passed. The commit addressing staabm's review feedback has already been pushed. Everything is done.

Addresses review feedback: adds a test where the variable is
sometimes null (via rand branch) to cover the maybe-null path
for ??= with throw.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet merged commit 9df075b into phpstan:2.1.x Mar 25, 2026
656 of 657 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-0ehzlyp branch March 25, 2026 13:50
phpstan-bot added a commit to phpstan-bot/phpstan-src that referenced this pull request Apr 7, 2026
…ed with throwing (phpstan#5297)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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