Skip to content

Commit 1a738cd

Browse files
phpstan-botclaude
andcommitted
Preserve property fetch types in immediately invoked closures
For closures that are immediately invoked (IIFEs and array_map callbacks), property/method fetch type narrowings should still be carried forward since the object's properties cannot change between definition and invocation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 868c3dc commit 1a738cd

4 files changed

Lines changed: 59 additions & 7 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use PHPStan\Node\Printer\ExprPrinter;
4545
use PHPStan\Node\VirtualNode;
4646
use PHPStan\Parser\ArrayMapArgVisitor;
47+
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
4748
use PHPStan\Parser\Parser;
4849
use PHPStan\Php\PhpVersion;
4950
use PHPStan\Php\PhpVersionFactory;
@@ -2153,12 +2154,16 @@ public function enterAnonymousFunctionWithoutReflection(
21532154
}
21542155

21552156
if (
2156-
$expr instanceof PropertyFetch
2157-
|| $expr instanceof MethodCall
2158-
|| $expr instanceof Expr\NullsafePropertyFetch
2159-
|| $expr instanceof Expr\NullsafeMethodCall
2160-
|| $expr instanceof Expr\StaticPropertyFetch
2161-
|| $expr instanceof Expr\StaticCall
2157+
$closure->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true
2158+
&& $closure->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) === null
2159+
&& (
2160+
$expr instanceof PropertyFetch
2161+
|| $expr instanceof MethodCall
2162+
|| $expr instanceof Expr\NullsafePropertyFetch
2163+
|| $expr instanceof Expr\NullsafeMethodCall
2164+
|| $expr instanceof Expr\StaticPropertyFetch
2165+
|| $expr instanceof Expr\StaticCall
2166+
)
21622167
) {
21632168
continue;
21642169
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,24 @@ function ($key) use ($arr): void {
6969
}
7070
}
7171

72+
public function doIife(MethodCall $call): void
73+
{
74+
if ($call->name instanceof Identifier) {
75+
// IIFE - property types should be carried forward
76+
(function () use ($call): void {
77+
assertType('PhpParser\Node\Identifier', $call->name);
78+
})();
79+
}
80+
}
81+
82+
public function doArrayMap(MethodCall $call): void
83+
{
84+
if ($call->name instanceof Identifier) {
85+
// array_map - closure is immediately invoked, property types should be carried forward
86+
array_map(function () use ($call): void {
87+
assertType('PhpParser\Node\Identifier', $call->name);
88+
}, [1]);
89+
}
90+
}
91+
7292
}

tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,16 @@ public function testBug2457(): void
5757

5858
public function testBug10345(): void
5959
{
60-
$this->analyse([__DIR__ . '/data/bug-10345.php'], []);
60+
$this->analyse([__DIR__ . '/data/bug-10345.php'], [
61+
[
62+
'Empty array passed to foreach.',
63+
125,
64+
],
65+
[
66+
'Empty array passed to foreach.',
67+
134,
68+
],
69+
]);
6170
}
6271

6372
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,21 @@ public static function setItems(array $items): void
116116

117117
$a6 = $func6();
118118
}
119+
120+
// Immediately invoked closure (IIFE) - should still detect empty array
121+
$container7 = new \stdClass();
122+
$container7->items = [];
123+
124+
$result7 = (function() use ($container7): int {
125+
foreach ($container7->items as $item) {}
126+
return 1;
127+
})();
128+
129+
// array_map - closure is immediately invoked, should still detect empty array
130+
$container8 = new \stdClass();
131+
$container8->items = [];
132+
133+
$result8 = array_map(function() use ($container8): int {
134+
foreach ($container8->items as $item) {}
135+
return 1;
136+
}, [1]);

0 commit comments

Comments
 (0)