diff --git a/conf/config.neon b/conf/config.neon index 9151a82caf..e307d1380b 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -632,6 +632,11 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\ImmediatelyInvokedClosureVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parallel\ParallelAnalyser arguments: diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 133ca1cfca..e34d1b2621 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -44,6 +44,7 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\PropertyAssignNode; use PHPStan\Parser\ArrayMapArgVisitor; +use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; @@ -4759,6 +4760,7 @@ private function processFinallyScopeVariableTypeHolders( * @param Node\ClosureUse[] $byRefUses */ public function processClosureScope( + Expr\Closure $expr, self $closureScope, ?self $prevScope, array $byRefUses, @@ -4791,7 +4793,9 @@ public function processClosureScope( $prevVariableType = $prevScope->getVariableType($variableName); if (!$variableType->equals($prevVariableType)) { $variableType = TypeCombinator::union($variableType, $prevVariableType); - $variableType = self::generalizeType($variableType, $prevVariableType, 0); + if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) !== true) { + $variableType = self::generalizeType($variableType, $prevVariableType, 0); + } } } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index af56a00f72..2bf8af24c4 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4228,7 +4228,7 @@ private function processClosureNode( } $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); - $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); + $closureScope = $closureScope->processClosureScope($expr, $scope, null, $byRefUses); $closureType = $closureScope->getAnonymousFunctionReflection(); if (!$closureType instanceof ClosureType) { throw new ShouldNotHappenException(); @@ -4298,7 +4298,7 @@ private function processClosureNode( $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); } $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); - $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); + $closureScope = $closureScope->processClosureScope($expr, $intermediaryClosureScope, $prevScope, $byRefUses); if ($closureScope->equals($prevScope)) { break; } @@ -4318,7 +4318,7 @@ private function processClosureNode( array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ProcessClosureResult($scope->processClosureScope($closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult($scope->processClosureScope($expr, $closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); } /** diff --git a/src/Parser/ImmediatelyInvokedClosureVisitor.php b/src/Parser/ImmediatelyInvokedClosureVisitor.php new file mode 100644 index 0000000000..570ba75830 --- /dev/null +++ b/src/Parser/ImmediatelyInvokedClosureVisitor.php @@ -0,0 +1,46 @@ +inFuncCall = false; + return null; + } + + public function enterNode(Node $node): ?Node + { + if ($node instanceof Node\Expr\FuncCall) { + $this->inFuncCall = true; + } + + if ( + $this->inFuncCall + && $node instanceof Node\Expr\Closure + ) { + $node->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + + public function leaveNode(Node $node): ?Node + { + if ($node instanceof Node\Expr\FuncCall) { + $this->inFuncCall = false; + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11561.php b/tests/PHPStan/Analyser/nsrt/bug-11561.php new file mode 100644 index 0000000000..1a01e5f97a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11561.php @@ -0,0 +1,40 @@ += 8.0 + +namespace Bug11561; + +use function PHPStan\Testing\assertType; +use DateTime; + +/** @param array{date: DateTime} $c */ +function main(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + assertType('array{date: DateTime, id: 1}', $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name?: 'ruud'}", $c); +} + + +/** @param array{date: DateTime} $c */ +function main2(mixed $c): void{ + assertType('array{date: DateTime}', $c); + $c['id']=1; + $c['name'] = 'staabm'; + assertType("array{date: DateTime, id: 1, name: 'staabm'}", $c); + + $x = (function() use (&$c) { + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); + $c['name'] = 'ruud'; + assertType("array{date: DateTime, id: 1, name: 'ruud'}", $c); + return 'x'; + })(); + + assertType("array{date: DateTime, id: 1, name: 'ruud'|'staabm'}", $c); +}