Skip to content

Commit 9d63db2

Browse files
phpstan-botclaude
andcommitted
Handle always-null LHS and always-terminating LHS in null coalescing 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>
1 parent 1eb6eb9 commit 9d63db2

4 files changed

Lines changed: 41 additions & 2 deletions

File tree

src/Analyser/ExprHandler/AssignOpHandler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex
6666
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
6767
if ($expr instanceof Expr\AssignOp\Coalesce) {
6868
$nodeScopeResolver->storeBeforeScope($storage, $expr, $originalScope);
69+
$isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $originalScope->getType($expr->var)->isNull()->yes();
6970
return new ExpressionResult(
7071
$exprResult->getScope()->mergeWith($originalScope),
7172
$exprResult->hasYield(),
72-
false,
73+
$isAlwaysTerminating,
7374
$exprResult->getThrowPoints(),
7475
$exprResult->getImpurePoints(),
7576
);

tests/PHPStan/Analyser/nsrt/bug-14369.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ function test(string|null $test): void
1313

1414
assertType('string', $test);
1515
}
16+
17+
function testAlwaysNull(): void
18+
{
19+
$test = null;
20+
$test ??= throw new Exception();
21+
22+
assertType('*NEVER*', $test);
23+
}

tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,16 @@ public function testBug14328(): void
400400
public function testBug14369(): void
401401
{
402402
$this->treatPhpDocTypesAsCertain = true;
403-
$this->analyse([__DIR__ . '/data/bug-14369.php'], []);
403+
$this->analyse([__DIR__ . '/data/bug-14369.php'], [
404+
[
405+
'Unreachable statement - code above always terminates.',
406+
21,
407+
],
408+
[
409+
'Unreachable statement - code above always terminates.',
410+
28,
411+
],
412+
]);
404413
}
405414

406415
}

tests/PHPStan/Rules/DeadCode/data/bug-14369.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,24 @@ function test(string|null $test): void
1212

1313
echo $test;
1414
}
15+
16+
function testAlwaysNull(): void
17+
{
18+
$test = null;
19+
$test ??= throw new Exception();
20+
21+
echo $test;
22+
}
23+
24+
function testAlwaysTerminatingLhs(): void
25+
{
26+
alwaysThrows()->prop ??= throw new Exception();
27+
28+
echo 'unreachable';
29+
}
30+
31+
/** @return never */
32+
function alwaysThrows(): never
33+
{
34+
throw new Exception();
35+
}

0 commit comments

Comments
 (0)