diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 31af0b9c77..6208008ee9 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -29,6 +29,7 @@ use PHPStan\Type\Type; use function array_merge; use function get_class; +use function is_string; use function sprintf; /** @@ -68,6 +69,13 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $scope = $scope->filterByFalseyValue( new BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), ); + + if ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) { + $context = $context->enterRightSideAssign( + $expr->var->name, + $expr->expr, + ); + } } $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-13810.php b/tests/PHPStan/Analyser/nsrt/bug-13810.php new file mode 100644 index 0000000000..c1377cba0e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13810.php @@ -0,0 +1,68 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug13810; + +use function PHPStan\Testing\assertType; + +function doFoo(): void +{ + static $isSupported; + assertType('mixed', $isSupported); + $isSupported ??= function (mixed $arg) use (&$isSupported): bool { + assertType('Closure(mixed): bool', $isSupported); + return $isSupported($arg); + }; + + assertType('mixed~null', $isSupported); + $isSupported('foo'); +} + +function doBar($isSupported): void +{ + assertType('mixed', $isSupported); + $isSupported ??= function (mixed $arg) use (&$isSupported): bool { + assertType('Closure(mixed): bool', $isSupported); + return $isSupported($arg); + }; + + assertType('mixed~null', $isSupported); + $isSupported('foo'); +} + +function doFooBar(): void +{ + $isSupported = null; + assertType('null', $isSupported); + $isSupported ??= function (mixed $arg) use (&$isSupported): bool { + assertType('Closure(mixed): bool', $isSupported); + return $isSupported($arg); + }; + + assertType('Closure(mixed): bool', $isSupported); + $isSupported('foo'); +} + +class HelloWorld +{ + public function setValue(mixed $value): void + { + /** @var ?callable $isSupported */ + static $isSupported = null; + $isSupported ??= function(mixed $arg) use (&$isSupported): bool { + if (is_array($arg)) { + foreach($arg as $value) { + if (!$isSupported($value)) { + return false; + } + } + return true; + } + return is_string($arg); + }; + if (!$isSupported($value)) { + throw new InvalidArgumentException('only strings/string arrays are supported'); + } + } +} diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index ed1047d020..aec252f89b 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -429,4 +429,9 @@ public function testMaybeNotCallable(): void $this->analyse([__DIR__ . '/data/maybe-not-callable.php'], $errors); } + public function testBug13810(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13810.php'], []); + } + }