diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b9bf1942f6..4c62a3c9ac 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3493,7 +3493,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } } elseif ($expr instanceof Expr\Closure) { $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, null); - $scope = $processClosureResult->getScope(); + $scope = $processClosureResult->applyByRefUseScope($processClosureResult->getScope()); return new ExpressionResult( $scope, @@ -5142,7 +5142,7 @@ private function processClosureNode( array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope, $storage); - return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); + return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating, $closureResultScope, $byRefUses); } /** @@ -5551,6 +5551,8 @@ private function processArgs( $isAlwaysTerminating = false; /** @var list $deferredInvalidateExpressions */ $deferredInvalidateExpressions = []; + /** @var ProcessClosureResult[] $deferredByRefClosureResults */ + $deferredByRefClosureResults = []; foreach ($args as $i => $arg) { $assignByReference = false; $parameter = null; @@ -5662,6 +5664,7 @@ private function processArgs( } $scope = $closureResult->getScope(); + $deferredByRefClosureResults[] = $closureResult; $invalidateExpressions = $closureResult->getInvalidateExpressions(); if ($restoreThisScope !== null) { $nodeFinder = new NodeFinder(); @@ -5753,6 +5756,10 @@ private function processArgs( $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); } + foreach ($deferredByRefClosureResults as $deferredClosureResult) { + $scope = $deferredClosureResult->applyByRefUseScope($scope); + } + if ($parameters !== null) { foreach ($args as $i => $arg) { $assignByReference = false; diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php index 0d90fc1b66..dd6cfa5d26 100644 --- a/src/Analyser/ProcessClosureResult.php +++ b/src/Analyser/ProcessClosureResult.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PhpParser\Node\ClosureUse; use PHPStan\Node\InvalidateExprNode; final class ProcessClosureResult @@ -11,6 +12,7 @@ final class ProcessClosureResult * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints * @param InvalidateExprNode[] $invalidateExpressions + * @param ClosureUse[] $byRefUses */ public function __construct( private MutatingScope $scope, @@ -18,6 +20,8 @@ public function __construct( private array $impurePoints, private array $invalidateExpressions, private bool $isAlwaysTerminating, + private ?MutatingScope $byRefClosureResultScope = null, + private array $byRefUses = [], ) { } @@ -27,6 +31,15 @@ public function getScope(): MutatingScope return $this->scope; } + public function applyByRefUseScope(MutatingScope $scope): MutatingScope + { + if ($this->byRefClosureResultScope === null) { + return $scope; + } + + return $scope->processClosureScope($this->byRefClosureResultScope, null, $this->byRefUses); + } + /** * @return InternalThrowPoint[] */ diff --git a/tests/PHPStan/Rules/Methods/data/discussion-14038.php b/tests/PHPStan/Rules/Methods/data/discussion-14038.php index a0c73e05c2..12de156d37 100644 --- a/tests/PHPStan/Rules/Methods/data/discussion-14038.php +++ b/tests/PHPStan/Rules/Methods/data/discussion-14038.php @@ -30,3 +30,21 @@ function(): void { ); } } + +class ByRefTest +{ + private function foo(callable $c, string $s): void + { + } + + public function testByRef(): void + { + $x = 'hello'; + $this->foo( + function() use (&$x): void { + $x = 42; + }, + $x, + ); + } +}