Skip to content

Commit 5955b7a

Browse files
phpstan-botclaude
andcommitted
Fix: Only invalidate expressions for immediately-invoked callables, not later-invoked ones
The deferredInvalidateExpressions were being collected unconditionally for all closures, regardless of whether the callable was marked with @param-immediately-invoked-callable or @param-later-invoked-callable. This caused property types to be incorrectly invalidated even for later-invoked callables where the callback hasn't executed yet. Gate deferredInvalidateExpressions behind callCallbackImmediately() and update the bug-11417 test to assert that the later-invoked-callable case correctly reports "If condition is always true" instead of just a comment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9fab5c0 commit 5955b7a

3 files changed

Lines changed: 12 additions & 4 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3370,7 +3370,9 @@ public function processArgs(
33703370
$scope = $scope->restoreThis($restoreThisScope);
33713371
}
33723372

3373-
$deferredInvalidateExpressions[] = [$invalidateExpressions, $uses];
3373+
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3374+
$deferredInvalidateExpressions[] = [$invalidateExpressions, $uses];
3375+
}
33743376
} elseif ($arg->value instanceof Expr\ArrowFunction) {
33753377
if (
33763378
$closureBindScope === null
@@ -3418,8 +3420,8 @@ public function processArgs(
34183420
if ($exprType->isCallable()->yes()) {
34193421
$acceptors = $exprType->getCallableParametersAcceptors($scope);
34203422
if (count($acceptors) === 1) {
3421-
$deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()];
34223423
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3424+
$deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()];
34233425
$callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints());
34243426
if (!$this->implicitThrows) {
34253427
$callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit()));

tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,13 @@ public function testBug8926(): void
186186
public function testBug11417(): void
187187
{
188188
$this->treatPhpDocTypesAsCertain = true;
189-
$this->analyse([__DIR__ . '/data/bug-11417.php'], []);
189+
$this->analyse([__DIR__ . '/data/bug-11417.php'], [
190+
[
191+
'If condition is always true.',
192+
66,
193+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
194+
],
195+
]);
190196
}
191197

192198
public function testBug10903(): void

tests/PHPStan/Rules/Comparison/data/bug-11417.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function getConn(): string
6363
$this->conn = "conn";
6464
});
6565

66-
if (is_null($this->conn)) { // should be always true - later-invoked callable doesn't invalidate
66+
if (is_null($this->conn)) {
6767
throw new \Exception("conn failed");
6868
}
6969

0 commit comments

Comments
 (0)