Skip to content

Commit 2e7e5ef

Browse files
phpstan-botclaude
andcommitted
Use callCallbackImmediately() instead of hardcoded ArrayMapArgVisitor check
Instead of hardcoding ArrayMapArgVisitor in MutatingScope to determine if a closure is immediately invoked, set the ImmediatelyInvokedClosureVisitor attribute in NodeScopeResolver based on callCallbackImmediately(). This relies on PHPStan's existing mechanism for determining immediate vs later invocation, which respects @param-immediately-invoked-callable and @param-later-invoked-callable PHPDoc tags, and defaults to immediately invoked for function calls and later invoked for method calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1a738cd commit 2e7e5ef

5 files changed

Lines changed: 77 additions & 3 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2155,7 +2155,6 @@ public function enterAnonymousFunctionWithoutReflection(
21552155

21562156
if (
21572157
$closure->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true
2158-
&& $closure->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) === null
21592158
&& (
21602159
$expr instanceof PropertyFetch
21612160
|| $expr instanceof MethodCall

src/Analyser/NodeScopeResolver.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3406,9 +3406,14 @@ public function processArgs(
34063406
}
34073407
}
34083408

3409+
$callCallbackImmediately = $this->callCallbackImmediately($parameter, $parameterType, $calleeReflection);
3410+
if ($callCallbackImmediately) {
3411+
$arg->value->setAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME, true);
3412+
}
3413+
34093414
$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
34103415
$closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null);
3411-
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3416+
if ($callCallbackImmediately) {
34123417
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints()));
34133418
$impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
34143419
$isAlwaysTerminating = $isAlwaysTerminating || $closureResult->isAlwaysTerminating();
@@ -3464,9 +3469,14 @@ public function processArgs(
34643469
}
34653470
}
34663471

3472+
$callCallbackImmediately = $this->callCallbackImmediately($parameter, $parameterType, $calleeReflection);
3473+
if ($callCallbackImmediately) {
3474+
$arg->value->setAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME, true);
3475+
}
3476+
34673477
$this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
34683478
$arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null);
3469-
if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3479+
if ($callCallbackImmediately) {
34703480
$throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints()));
34713481
$impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
34723482
$isAlwaysTerminating = $isAlwaysTerminating || $arrowFunctionResult->isAlwaysTerminating();

tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,31 @@ public function doArrayMap(MethodCall $call): void
8989
}
9090
}
9191

92+
public function doGenericFunctionCall(MethodCall $call): void
93+
{
94+
if ($call->name instanceof Identifier) {
95+
// Generic function with callable parameter - immediately invoked by default
96+
usort([1], function () use ($call): int {
97+
assertType('PhpParser\Node\Identifier', $call->name);
98+
return 0;
99+
});
100+
}
101+
}
102+
103+
public function doLaterInvokedCallable(MethodCall $call): void
104+
{
105+
if ($call->name instanceof Identifier) {
106+
// @param-later-invoked-callable - property types should NOT be carried forward
107+
laterInvoke(function () use ($call): void {
108+
assertType('PhpParser\Node\Expr|PhpParser\Node\Identifier', $call->name);
109+
});
110+
}
111+
}
112+
113+
}
114+
115+
/**
116+
* @param-later-invoked-callable $callback
117+
*/
118+
function laterInvoke(callable $callback): void {
92119
}

tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ public function testBug10345(): void
6666
'Empty array passed to foreach.',
6767
134,
6868
],
69+
[
70+
'Empty array passed to foreach.',
71+
152,
72+
],
6973
]);
7074
}
7175

tests/PHPStan/Rules/Arrays/data/bug-10345.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,37 @@ public static function setItems(array $items): void
134134
foreach ($container8->items as $item) {}
135135
return 1;
136136
}, [1]);
137+
138+
// Generic function with callable parameter - immediately invoked by default
139+
/**
140+
* @template T
141+
* @param callable(): T $callback
142+
* @return T
143+
*/
144+
function invoke(callable $callback): mixed {
145+
return $callback();
146+
}
147+
148+
$container9 = new \stdClass();
149+
$container9->items = [];
150+
151+
$result9 = invoke(function() use ($container9): int {
152+
foreach ($container9->items as $item) {} // should report error - immediately invoked
153+
return 1;
154+
});
155+
156+
// Function with @param-later-invoked-callable - should NOT report error
157+
/**
158+
* @param-later-invoked-callable $callback
159+
*/
160+
function invokeLater(callable $callback): void {
161+
// stores callback for later
162+
}
163+
164+
$container10 = new \stdClass();
165+
$container10->items = [];
166+
167+
invokeLater(function() use ($container10): int {
168+
foreach ($container10->items as $item) {} // should NOT report error - later invoked
169+
return 1;
170+
});

0 commit comments

Comments
 (0)