From 12cbf38b2e709ce885d27a46cc02f4c1b71f367f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:32:04 +0000 Subject: [PATCH 1/8] Fix false positive "Empty array passed to foreach" in closure with object use - Don't carry forward property fetch expression types for dynamic (undeclared) properties on objects captured by value in closures - Objects are references in PHP, so their properties can change between closure definition and invocation - Only affects dynamic properties (e.g. stdClass); declared/native properties still carry forward type narrowings - New regression test in tests/PHPStan/Rules/Arrays/data/bug-10345.php Closes https://github.com/phpstan/phpstan/issues/10345 --- src/Analyser/MutatingScope.php | 26 +++++++++++++++++++ .../Rules/Arrays/DeadForeachRuleTest.php | 5 ++++ tests/PHPStan/Rules/Arrays/data/bug-10345.php | 15 +++++++++++ 3 files changed, 46 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-10345.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4ce247488f..457a80471d 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2080,6 +2080,10 @@ public function enterAnonymousFunctionWithoutReflection( } } + if ($this->shouldNotCarryForwardPropertyFetchInClosure($expr)) { + continue; + } + $expressionTypes[$exprString] = $typeHolder; } @@ -2146,6 +2150,28 @@ public function enterAnonymousFunctionWithoutReflection( ); } + private function shouldNotCarryForwardPropertyFetchInClosure(Expr $expr): bool + { + if (!$expr instanceof PropertyFetch) { + return false; + } + + if (!$expr->name instanceof Identifier) { + return false; + } + + $objectType = $this->getType($expr->var); + $propertyName = $expr->name->name; + + foreach ($objectType->getObjectClassReflections() as $classReflection) { + if ($classReflection->hasNativeProperty($propertyName)) { + return false; + } + } + + return true; + } + private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool { $expr = $typeHolder->getExpr(); diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index 227bcc3e4f..e6665d5f04 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -60,4 +60,9 @@ public function testBug8056(): void $this->analyse([__DIR__ . '/data/bug-8056.php'], []); } + public function testBug10345(): void + { + $this->analyse([__DIR__ . '/data/bug-10345.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10345.php b/tests/PHPStan/Rules/Arrays/data/bug-10345.php new file mode 100644 index 0000000000..e4a6829e58 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-10345.php @@ -0,0 +1,15 @@ +items = []; + +$func = function() use ($container): int { + foreach ($container->items as $item) {} + return 1; +}; + +$container->items[] = '1'; + +$a = $func(); From 5b4340e78a037b07c572cf3580c5879b4581b32c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 09:28:50 +0000 Subject: [PATCH 2/8] Do not carry forward any property fetch types into closure scope Objects are always references in PHP, even when captured by value via `use`. Properties (both dynamic and declared) can be modified between closure definition and invocation, so their types should not be frozen at closure definition time. Simplify shouldNotCarryForwardPropertyFetchInClosure to skip all PropertyFetch expressions, not just dynamic properties. Add test case with declared property class (Foo with list $items). Update specified-types-closure-use.php expectations accordingly. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 19 +------------------ .../nsrt/specified-types-closure-use.php | 4 ++-- tests/PHPStan/Rules/Arrays/data/bug-10345.php | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 457a80471d..b7acfa2ae5 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2152,24 +2152,7 @@ public function enterAnonymousFunctionWithoutReflection( private function shouldNotCarryForwardPropertyFetchInClosure(Expr $expr): bool { - if (!$expr instanceof PropertyFetch) { - return false; - } - - if (!$expr->name instanceof Identifier) { - return false; - } - - $objectType = $this->getType($expr->var); - $propertyName = $expr->name->name; - - foreach ($objectType->getObjectClassReflections() as $classReflection) { - if ($classReflection->hasNativeProperty($propertyName)) { - return false; - } - } - - return true; + return $expr instanceof PropertyFetch; } private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool diff --git a/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php b/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php index f5f8189ded..d61da57b09 100644 --- a/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php +++ b/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php @@ -13,7 +13,7 @@ public function doFoo(MethodCall $call, MethodCall $bar): void { if ($call->name instanceof Identifier && $bar->name instanceof Identifier) { function () use ($call): void { - assertType('PhpParser\Node\Identifier', $call->name); + assertType('PhpParser\Node\Expr|PhpParser\Node\Identifier', $call->name); assertType('mixed', $bar->name); }; @@ -26,7 +26,7 @@ public function doBar(MethodCall $call, MethodCall $bar): void if ($call->name instanceof Identifier && $bar->name instanceof Identifier) { $a = 1; function () use ($call, &$a): void { - assertType('PhpParser\Node\Identifier', $call->name); + assertType('PhpParser\Node\Expr|PhpParser\Node\Identifier', $call->name); assertType('mixed', $bar->name); }; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10345.php b/tests/PHPStan/Rules/Arrays/data/bug-10345.php index e4a6829e58..4260c2fbe3 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-10345.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-10345.php @@ -13,3 +13,20 @@ $container->items[] = '1'; $a = $func(); + +class Foo { + /** @var list */ + public array $items = []; +} + +$container2 = new Foo(); +$container2->items = []; + +$func2 = function() use ($container2): int { + foreach ($container2->items as $item) {} + return 1; +}; + +$container2->items[] = '1'; + +$a2 = $func2(); From e04acb0329f95a1a421758b1229ecd2d9d38075b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 09:39:58 +0000 Subject: [PATCH 3/8] Inline shouldNotCarryForwardPropertyFetchInClosure check The method was a trivial one-liner, so inline the instanceof check directly at the call site and remove the private method. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b7acfa2ae5..95af117f3a 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2080,7 +2080,7 @@ public function enterAnonymousFunctionWithoutReflection( } } - if ($this->shouldNotCarryForwardPropertyFetchInClosure($expr)) { + if ($expr instanceof PropertyFetch) { continue; } @@ -2150,11 +2150,6 @@ public function enterAnonymousFunctionWithoutReflection( ); } - private function shouldNotCarryForwardPropertyFetchInClosure(Expr $expr): bool - { - return $expr instanceof PropertyFetch; - } - private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool { $expr = $typeHolder->getExpr(); From c70d58c1e5703df86df3371423e40db57bfb0326 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 09:50:34 +0000 Subject: [PATCH 4/8] Do not carry forward method call types into closure scope Objects are always references in PHP, even when captured by value via `use`. Method return values can change if object state changes between closure definition and invocation. Skip MethodCall expressions (in addition to PropertyFetch) when entering closure scope. Add regression test for method call narrowing with getter/setter pattern. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 2 +- tests/PHPStan/Rules/Arrays/data/bug-10345.php | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 95af117f3a..d6dac171eb 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2080,7 +2080,7 @@ public function enterAnonymousFunctionWithoutReflection( } } - if ($expr instanceof PropertyFetch) { + if ($expr instanceof PropertyFetch || $expr instanceof MethodCall) { continue; } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10345.php b/tests/PHPStan/Rules/Arrays/data/bug-10345.php index 4260c2fbe3..366e88f29a 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-10345.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-10345.php @@ -30,3 +30,32 @@ class Foo { $container2->items[] = '1'; $a2 = $func2(); + +class Bar { + /** @var list */ + private array $items = []; + + /** @return list */ + public function getItems(): array + { + return $this->items; + } + + /** @param list $items */ + public function setItems(array $items): void + { + $this->items = $items; + } +} + +$container3 = new Bar(); +if ($container3->getItems() === []) { + $func3 = function() use ($container3): int { + foreach ($container3->getItems() as $item) {} + return 1; + }; + + $container3->setItems(['foo']); + + $a3 = $func3(); +} From 70177a5d9bb376fc47a7f81bbcb4df00e16653ea Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 10:10:28 +0000 Subject: [PATCH 5/8] Also exclude nullsafe and static access from closure scope forwarding Handle NullsafePropertyFetch, NullsafeMethodCall, StaticPropertyFetch, and StaticCall in addition to PropertyFetch and MethodCall when entering closure scope, since these can also be modified between closure definition and invocation. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 9 ++- tests/PHPStan/Rules/Arrays/data/bug-10345.php | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index d6dac171eb..c72f28b35a 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2080,7 +2080,14 @@ public function enterAnonymousFunctionWithoutReflection( } } - if ($expr instanceof PropertyFetch || $expr instanceof MethodCall) { + if ( + $expr instanceof PropertyFetch + || $expr instanceof MethodCall + || $expr instanceof Expr\NullsafePropertyFetch + || $expr instanceof Expr\NullsafeMethodCall + || $expr instanceof Expr\StaticPropertyFetch + || $expr instanceof Expr\StaticCall + ) { continue; } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10345.php b/tests/PHPStan/Rules/Arrays/data/bug-10345.php index 366e88f29a..5173bc3f1d 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-10345.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-10345.php @@ -59,3 +59,58 @@ public function setItems(array $items): void $a3 = $func3(); } + +// Nullsafe property fetch +$container4 = new Foo(); +$container4->items = []; + +$func4 = function() use ($container4): int { + foreach ($container4?->items as $item) {} + return 1; +}; + +$container4->items[] = '1'; + +$a4 = $func4(); + +// Static property access +class Baz { + /** @var list */ + public static array $items = []; + + /** @return list */ + public static function getItems(): array + { + return self::$items; + } + + /** @param list $items */ + public static function setItems(array $items): void + { + self::$items = $items; + } +} + +Baz::$items = []; + +$func5 = function(): int { + foreach (Baz::$items as $item) {} + return 1; +}; + +Baz::$items[] = '1'; + +$a5 = $func5(); + +// Static method call +Baz::setItems([]); +if (Baz::getItems() === []) { + $func6 = function(): int { + foreach (Baz::getItems() as $item) {} + return 1; + }; + + Baz::setItems(['foo']); + + $a6 = $func6(); +} From 12b94b6af4344be1c0b07d2ba719af8496039149 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 14 Mar 2026 11:20:31 +0100 Subject: [PATCH 6/8] Fix lint --- tests/PHPStan/Rules/Arrays/data/bug-10345.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10345.php b/tests/PHPStan/Rules/Arrays/data/bug-10345.php index 5173bc3f1d..521c703498 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-10345.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-10345.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 1); namespace Bug10345; From 2610337d3c088f0414b215f4fc7c3815531a9c1f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 25 Mar 2026 19:35:37 +0100 Subject: [PATCH 7/8] Add assertions --- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Rules/Arrays/data/bug-10345.php | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 13ca1bdc2e..507aa650f6 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -262,6 +262,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Properties/data/bug-14012.php'; yield __DIR__ . '/../Rules/Variables/data/bug-14124.php'; yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-10345.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-14308.php'; } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10345.php b/tests/PHPStan/Rules/Arrays/data/bug-10345.php index 521c703498..a66da23e3b 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-10345.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-10345.php @@ -4,11 +4,18 @@ namespace Bug10345; +use function PHPStan\Testing\assertType; + $container = new \stdClass(); $container->items = []; +assertType('stdClass', $container); +assertType('array{}', $container->items); $func = function() use ($container): int { - foreach ($container->items as $item) {} + assertType('stdClass', $container); + assertType('mixed', $container->items); + foreach ($container->items as $item) { + } return 1; }; @@ -24,7 +31,11 @@ class Foo { $container2 = new Foo(); $container2->items = []; +assertType('Bug10345\Foo', $container2); +assertType('array{}', $container2->items); $func2 = function() use ($container2): int { + assertType('Bug10345\Foo', $container2); + assertType('list', $container2->items); foreach ($container2->items as $item) {} return 1; }; @@ -52,7 +63,11 @@ public function setItems(array $items): void $container3 = new Bar(); if ($container3->getItems() === []) { + assertType('Bug10345\Bar', $container3); + assertType('array{}', $container3->getItems()); $func3 = function() use ($container3): int { + assertType('Bug10345\Bar', $container3); + assertType('list', $container3->getItems()); foreach ($container3->getItems() as $item) {} return 1; }; @@ -66,7 +81,11 @@ public function setItems(array $items): void $container4 = new Foo(); $container4->items = []; +assertType('Bug10345\Foo', $container4); +assertType('array{}', $container4->items); $func4 = function() use ($container4): int { + assertType('Bug10345\Foo', $container4); + assertType('list', $container4->items); foreach ($container4?->items as $item) {} return 1; }; @@ -95,7 +114,9 @@ public static function setItems(array $items): void Baz::$items = []; +assertType('array{}', Baz::$items); $func5 = function(): int { + assertType('list', Baz::$items); foreach (Baz::$items as $item) {} return 1; }; @@ -107,7 +128,9 @@ public static function setItems(array $items): void // Static method call Baz::setItems([]); if (Baz::getItems() === []) { + assertType('array{}', Baz::getItems()); $func6 = function(): int { + assertType('list', Baz::getItems()); foreach (Baz::getItems() as $item) {} return 1; }; From a958dd2c90ca0e6db6e7db1f61490941277921ff Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 25 Mar 2026 18:55:44 +0000 Subject: [PATCH 8/8] Preserve property fetch types in immediately invoked closures When a closure is immediately invoked (IIFE or callback passed to functions like array_map), property fetch types should be carried forward into the closure scope since the closure runs before properties can be modified. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/MutatingScope.php | 8 +++- src/Analyser/NodeScopeResolver.php | 8 +++- .../nsrt/specified-types-closure-use.php | 18 +++++++ .../Rules/Arrays/DeadForeachRuleTest.php | 15 +++++- tests/PHPStan/Rules/Arrays/data/bug-10345.php | 47 +++++++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c72f28b35a..5131ab39ec 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -36,6 +36,7 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\VirtualNode; use PHPStan\Parser\ArrayMapArgVisitor; +use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; use PHPStan\Php\PhpVersionFactory; @@ -2088,7 +2089,12 @@ public function enterAnonymousFunctionWithoutReflection( || $expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\StaticCall ) { - continue; + if ( + $closure->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true + && $closure->getAttribute(NodeScopeResolver::IMMEDIATELY_CALLED_CALLBACK_ATTRIBUTE_NAME) !== true + ) { + continue; + } } $expressionTypes[$exprString] = $typeHolder; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ca1daa6f34..787499e4db 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -187,6 +187,8 @@ class NodeScopeResolver private const LOOP_SCOPE_ITERATIONS = 3; private const GENERALIZE_AFTER_ITERATION = 1; + public const IMMEDIATELY_CALLED_CALLBACK_ATTRIBUTE_NAME = 'isImmediatelyCalledCallback'; + /** @var array filePath(string) => bool(true) */ private array $analysedFiles = []; @@ -3332,8 +3334,12 @@ public function processArgs( } $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); + $callImmediately = $this->callCallbackImmediately($parameter, $parameterType, $calleeReflection); + if ($callImmediately) { + $arg->value->setAttribute(self::IMMEDIATELY_CALLED_CALLBACK_ATTRIBUTE_NAME, true); + } $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null); - if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { + if ($callImmediately) { $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())); $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $closureResult->isAlwaysTerminating(); diff --git a/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php b/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php index d61da57b09..1457c1d0ee 100644 --- a/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php +++ b/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php @@ -34,6 +34,24 @@ function () use ($call, &$a): void { } } + public function doImmediatelyInvoked(MethodCall $call): void + { + if ($call->name instanceof Identifier) { + array_map(function () use ($call): void { + assertType('PhpParser\Node\Identifier', $call->name); + }, [1]); + } + } + + public function doIife(MethodCall $call): void + { + if ($call->name instanceof Identifier) { + (function () use ($call): void { + assertType('PhpParser\Node\Identifier', $call->name); + })(); + } + } + public function doBaz(array $arr, string $key): void { $arr[$key] = 'test'; diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index e6665d5f04..8052a81ddb 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -62,7 +62,20 @@ public function testBug8056(): void public function testBug10345(): void { - $this->analyse([__DIR__ . '/data/bug-10345.php'], []); + $this->analyse([__DIR__ . '/data/bug-10345.php'], [ + [ + 'Empty array passed to foreach.', + 153, + ], + [ + 'Empty array passed to foreach.', + 170, + ], + [ + 'Empty array passed to foreach.', + 185, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-10345.php b/tests/PHPStan/Rules/Arrays/data/bug-10345.php index a66da23e3b..181eeee7c2 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-10345.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-10345.php @@ -139,3 +139,50 @@ public static function setItems(array $items): void $a6 = $func6(); } + +// Immediately invoked closure should keep the type +$container7 = new \stdClass(); +$container7->items = []; + +assertType('stdClass', $container7); +assertType('array{}', $container7->items); +$result7 = array_map( + function() use ($container7): int { + assertType('stdClass', $container7); + assertType('array{}', $container7->items); + foreach ($container7->items as $item) { + } + return 1; + }, + [1, 2, 3], +); + +// Immediately invoked closure with declared property should also keep the type +$container8 = new Foo(); +$container8->items = []; + +assertType('Bug10345\Foo', $container8); +assertType('array{}', $container8->items); +$result8 = array_map( + function() use ($container8): int { + assertType('Bug10345\Foo', $container8); + assertType('array{}', $container8->items); + foreach ($container8->items as $item) {} + return 1; + }, + [1, 2, 3], +); + +// IIFE should keep the type +$container9 = new \stdClass(); +$container9->items = []; + +assertType('stdClass', $container9); +assertType('array{}', $container9->items); +$result9 = (function() use ($container9): int { + assertType('stdClass', $container9); + assertType('array{}', $container9->items); + foreach ($container9->items as $item) { + } + return 1; +})();