Skip to content
17 changes: 17 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2080,6 +2081,22 @@ public function enterAnonymousFunctionWithoutReflection(
}
}

if (
$expr instanceof PropertyFetch
|| $expr instanceof MethodCall
|| $expr instanceof Expr\NullsafePropertyFetch
|| $expr instanceof Expr\NullsafeMethodCall
|| $expr instanceof Expr\StaticPropertyFetch
|| $expr instanceof Expr\StaticCall
) {
if (
$closure->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true
&& $closure->getAttribute(NodeScopeResolver::IMMEDIATELY_CALLED_CALLBACK_ATTRIBUTE_NAME) !== true
) {
continue;
}
}

$expressionTypes[$exprString] = $typeHolder;
}

Expand Down
8 changes: 7 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, true> filePath(string) => bool(true) */
private array $analysedFiles = [];

Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

Expand Down
22 changes: 20 additions & 2 deletions tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand All @@ -26,14 +26,32 @@ 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);
};

assertType('PhpParser\Node\Identifier', $call->name);
}
}

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';
Expand Down
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,22 @@ public function testBug8056(): void
$this->analyse([__DIR__ . '/data/bug-8056.php'], []);
}

public function testBug10345(): void
{
$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,
],
]);
}

}
188 changes: 188 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-10345.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug10345;

use function PHPStan\Testing\assertType;

$container = new \stdClass();
$container->items = [];

assertType('stdClass', $container);
assertType('array{}', $container->items);
$func = function() use ($container): int {
assertType('stdClass', $container);
assertType('mixed', $container->items);
foreach ($container->items as $item) {
}
return 1;
};

$container->items[] = '1';

$a = $func();

class Foo {
/** @var list<string> */
public array $items = [];
}

$container2 = new Foo();
$container2->items = [];

assertType('Bug10345\Foo', $container2);
assertType('array{}', $container2->items);
$func2 = function() use ($container2): int {
assertType('Bug10345\Foo', $container2);
assertType('list<string>', $container2->items);
foreach ($container2->items as $item) {}
return 1;
};

$container2->items[] = '1';

$a2 = $func2();

class Bar {
/** @var list<string> */
private array $items = [];

/** @return list<string> */
public function getItems(): array
{
return $this->items;
}

/** @param list<string> $items */
public function setItems(array $items): void
{
$this->items = $items;
}
}

$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<string>', $container3->getItems());
foreach ($container3->getItems() as $item) {}
return 1;
};

$container3->setItems(['foo']);

$a3 = $func3();
}

// Nullsafe property fetch
$container4 = new Foo();
$container4->items = [];

assertType('Bug10345\Foo', $container4);
assertType('array{}', $container4->items);
$func4 = function() use ($container4): int {
assertType('Bug10345\Foo', $container4);
assertType('list<string>', $container4->items);
foreach ($container4?->items as $item) {}
return 1;
};

$container4->items[] = '1';

$a4 = $func4();

// Static property access
class Baz {
/** @var list<string> */
public static array $items = [];

/** @return list<string> */
public static function getItems(): array
{
return self::$items;
}

/** @param list<string> $items */
public static function setItems(array $items): void
{
self::$items = $items;
}
}

Baz::$items = [];

assertType('array{}', Baz::$items);
$func5 = function(): int {
assertType('list<string>', Baz::$items);
foreach (Baz::$items as $item) {}
return 1;
};

Baz::$items[] = '1';

$a5 = $func5();

// Static method call
Baz::setItems([]);
if (Baz::getItems() === []) {
assertType('array{}', Baz::getItems());
$func6 = function(): int {
assertType('list<string>', Baz::getItems());
foreach (Baz::getItems() as $item) {}
return 1;
};

Baz::setItems(['foo']);

$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;
})();
Loading