diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 76c40b1ae3..10b1f135eb 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -36,6 +36,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; @@ -150,6 +151,34 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex true, ); $scope = $result->getScope(); + + if ( + $expr instanceof AssignRef + && $expr->var instanceof Variable + && is_string($expr->var->name) + && $expr->expr instanceof Variable + && is_string($expr->expr->name) + ) { + $varName = $expr->var->name; + $refName = $expr->expr->name; + $type = $scope->getType($expr->var); + $nativeType = $scope->getNativeType($expr->var); + + // When $varName is assigned, update $refName + $scope = $scope->assignExpression( + new IntertwinedVariableByReferenceWithExpr($varName, new Variable($refName), new Variable($varName)), + $type, + $nativeType, + ); + + // When $refName is assigned, update $varName + $scope = $scope->assignExpression( + new IntertwinedVariableByReferenceWithExpr($refName, new Variable($varName), new Variable($refName)), + $type, + $nativeType, + ); + } + $vars = $nodeScopeResolver->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index e60242af43..957fd5d0ad 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2565,7 +2565,10 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions); } - public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty): self + /** + * @param list $intertwinedPropagatedFrom + */ + public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, array $intertwinedPropagatedFrom = []): self { $node = new Variable($variableName); $scope = $this->assignExpression($node, $type, $nativeType); @@ -2594,11 +2597,16 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp && is_string($expressionType->getExpr()->getExpr()->name) && !$has->no() ) { + $targetVarName = $expressionType->getExpr()->getExpr()->name; + if (in_array($targetVarName, $intertwinedPropagatedFrom, true)) { + continue; + } $scope = $scope->assignVariable( - $expressionType->getExpr()->getExpr()->name, + $targetVarName, $scope->getType($expressionType->getExpr()->getAssignedExpr()), $scope->getNativeType($expressionType->getExpr()->getAssignedExpr()), $has, + array_merge($intertwinedPropagatedFrom, [$variableName]), ); } else { $scope = $scope->assignExpression( @@ -2800,6 +2808,12 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require foreach ($expressionTypes as $exprString => $exprTypeHolder) { $exprExpr = $exprTypeHolder->getExpr(); + if ( + $exprExpr instanceof IntertwinedVariableByReferenceWithExpr + && $exprExpr->isSimpleVariableReference() + ) { + continue; + } if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, $requireMoreCharacters, $invalidatingClass)) { continue; } diff --git a/src/Node/Expr/IntertwinedVariableByReferenceWithExpr.php b/src/Node/Expr/IntertwinedVariableByReferenceWithExpr.php index 2b4358a4a6..75c0f7f0c2 100644 --- a/src/Node/Expr/IntertwinedVariableByReferenceWithExpr.php +++ b/src/Node/Expr/IntertwinedVariableByReferenceWithExpr.php @@ -29,6 +29,14 @@ public function getAssignedExpr(): Expr return $this->assignedExpr; } + public function isSimpleVariableReference(): bool + { + return $this->expr instanceof \PhpParser\Node\Expr\Variable + && is_string($this->expr->name) + && $this->assignedExpr instanceof \PhpParser\Node\Expr\Variable + && is_string($this->assignedExpr->name); + } + #[Override] public function getType(): string { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14275.php b/tests/PHPStan/Analyser/nsrt/bug-14275.php new file mode 100644 index 0000000000..0f37de401d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14275.php @@ -0,0 +1,36 @@ +analyse([__DIR__ . '/data/bug-2457.php'], []); } + public function testBug8056(): void + { + $this->analyse([__DIR__ . '/data/bug-8056.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-8056.php b/tests/PHPStan/Rules/Arrays/data/bug-8056.php new file mode 100644 index 0000000000..ff28621d83 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8056.php @@ -0,0 +1,11 @@ +