Skip to content

Commit ae54616

Browse files
authored
Fix phpstan/phpstan#14275: Variable passed by reference are not updated (#5217)
1 parent e2637c3 commit ae54616

7 files changed

Lines changed: 123 additions & 2 deletions

File tree

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use PHPStan\DependencyInjection\AutowiredService;
3737
use PHPStan\Node\Expr\ExistingArrayDimFetch;
3838
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
39+
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
3940
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
4041
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
4142
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
@@ -150,6 +151,34 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex
150151
true,
151152
);
152153
$scope = $result->getScope();
154+
155+
if (
156+
$expr instanceof AssignRef
157+
&& $expr->var instanceof Variable
158+
&& is_string($expr->var->name)
159+
&& $expr->expr instanceof Variable
160+
&& is_string($expr->expr->name)
161+
) {
162+
$varName = $expr->var->name;
163+
$refName = $expr->expr->name;
164+
$type = $scope->getType($expr->var);
165+
$nativeType = $scope->getNativeType($expr->var);
166+
167+
// When $varName is assigned, update $refName
168+
$scope = $scope->assignExpression(
169+
new IntertwinedVariableByReferenceWithExpr($varName, new Variable($refName), new Variable($varName)),
170+
$type,
171+
$nativeType,
172+
);
173+
174+
// When $refName is assigned, update $varName
175+
$scope = $scope->assignExpression(
176+
new IntertwinedVariableByReferenceWithExpr($refName, new Variable($varName), new Variable($refName)),
177+
$type,
178+
$nativeType,
179+
);
180+
}
181+
153182
$vars = $nodeScopeResolver->getAssignedVariables($expr->var);
154183
if (count($vars) > 0) {
155184
$varChangedScope = false;

src/Analyser/MutatingScope.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2567,7 +2567,10 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool
25672567
return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions);
25682568
}
25692569

2570-
public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty): self
2570+
/**
2571+
* @param list<string> $intertwinedPropagatedFrom
2572+
*/
2573+
public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, array $intertwinedPropagatedFrom = []): self
25712574
{
25722575
$node = new Variable($variableName);
25732576
$scope = $this->assignExpression($node, $type, $nativeType);
@@ -2596,11 +2599,16 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
25962599
&& is_string($expressionType->getExpr()->getExpr()->name)
25972600
&& !$has->no()
25982601
) {
2602+
$targetVarName = $expressionType->getExpr()->getExpr()->name;
2603+
if (in_array($targetVarName, $intertwinedPropagatedFrom, true)) {
2604+
continue;
2605+
}
25992606
$scope = $scope->assignVariable(
2600-
$expressionType->getExpr()->getExpr()->name,
2607+
$targetVarName,
26012608
$scope->getType($expressionType->getExpr()->getAssignedExpr()),
26022609
$scope->getNativeType($expressionType->getExpr()->getAssignedExpr()),
26032610
$has,
2611+
array_merge($intertwinedPropagatedFrom, [$variableName]),
26042612
);
26052613
} else {
26062614
$scope = $scope->assignExpression(
@@ -2825,6 +2833,12 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
28252833

28262834
foreach ($expressionTypes as $exprString => $exprTypeHolder) {
28272835
$exprExpr = $exprTypeHolder->getExpr();
2836+
if (
2837+
$exprExpr instanceof IntertwinedVariableByReferenceWithExpr
2838+
&& $exprExpr->isVariableToVariableReference()
2839+
) {
2840+
continue;
2841+
}
28282842
if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, $requireMoreCharacters, $invalidatingClass)) {
28292843
continue;
28302844
}

src/Node/Expr/IntertwinedVariableByReferenceWithExpr.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use Override;
66
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\Variable;
78
use PHPStan\Node\VirtualNode;
9+
use function is_string;
810

911
final class IntertwinedVariableByReferenceWithExpr extends Expr implements VirtualNode
1012
{
@@ -29,6 +31,14 @@ public function getAssignedExpr(): Expr
2931
return $this->assignedExpr;
3032
}
3133

34+
public function isVariableToVariableReference(): bool
35+
{
36+
return $this->expr instanceof Variable
37+
&& is_string($this->expr->name)
38+
&& $this->assignedExpr instanceof Variable
39+
&& is_string($this->assignedExpr->name);
40+
}
41+
3242
#[Override]
3343
public function getType(): string
3444
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14275;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
// Basic reference: modifying $b should update $a
8+
$a = [];
9+
$b = &$a;
10+
11+
$b[0] = 1;
12+
assertType('array{1}', $a);
13+
assertType('array{1}', $b);
14+
15+
// Reference with scalar reassignment
16+
$c = 1;
17+
$d = &$c;
18+
$d = 2;
19+
assertType('2', $c);
20+
assertType('2', $d);
21+
22+
// Reference with different type reassignment
23+
$e = 'hello';
24+
$f = &$e;
25+
$f = 42;
26+
assertType('42', $e);
27+
assertType('42', $f);
28+
29+
// Subsequent assignments should continue propagating
30+
$e = 22;
31+
assertType('22', $e);
32+
assertType('22', $f);
33+
34+
$f = 33;
35+
assertType('33', $e);
36+
assertType('33', $f);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug8056;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
$array = [];
8+
$tmp = &$array;
9+
$tmp[] = 'foo';
10+
11+
assertType("array{'foo'}", $array);
12+
assertType("array{'foo'}", $tmp);
13+
14+
foreach ($array as $i) {
15+
assertType("'foo'", $i);
16+
}

tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@ public function testBug2457(): void
5555
$this->analyse([__DIR__ . '/data/bug-2457.php'], []);
5656
}
5757

58+
public function testBug8056(): void
59+
{
60+
$this->analyse([__DIR__ . '/data/bug-8056.php'], []);
61+
}
62+
5863
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug8056Rule;
4+
5+
$array = [];
6+
$tmp = &$array;
7+
$tmp[] = 'foo';
8+
9+
foreach ($array as $i) {
10+
11+
}

0 commit comments

Comments
 (0)