Skip to content
29 changes: 29 additions & 0 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
21 changes: 19 additions & 2 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2635,7 +2635,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<string> $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);
Expand Down Expand Up @@ -2664,11 +2667,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(
Expand Down Expand Up @@ -2870,6 +2878,15 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require

foreach ($expressionTypes as $exprString => $exprTypeHolder) {
$exprExpr = $exprTypeHolder->getExpr();
if (
$exprExpr instanceof IntertwinedVariableByReferenceWithExpr
&& $exprExpr->getExpr() instanceof Variable
&& is_string($exprExpr->getExpr()->name)
&& $exprExpr->getAssignedExpr() instanceof Variable
&& is_string($exprExpr->getAssignedExpr()->name)
) {
continue;
}
if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, $requireMoreCharacters, $invalidatingClass)) {
continue;
}
Expand Down
36 changes: 36 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14275.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace Bug14275;

use function PHPStan\Testing\assertType;

// Basic reference: modifying $b should update $a
$a = [];
$b = &$a;

$b[0] = 1;
assertType('array{1}', $a);
assertType('array{1}', $b);

// Reference with scalar reassignment
$c = 1;
$d = &$c;
$d = 2;
assertType('2', $c);
assertType('2', $d);

// Reference with different type reassignment
$e = 'hello';
$f = &$e;
$f = 42;
assertType('42', $e);
assertType('42', $f);

// Subsequent assignments should continue propagating
$e = 22;
assertType('22', $e);
assertType('22', $f);

$f = 33;
assertType('33', $e);
assertType('33', $f);
16 changes: 16 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-8056.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace Bug8056;

use function PHPStan\Testing\assertType;

$array = [];
$tmp = &$array;
$tmp[] = 'foo';

assertType("array{'foo'}", $array);
assertType("array{'foo'}", $tmp);

foreach ($array as $i) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug 8056 was about a false positive with the error "Empty array passed to foreach.".

we should have a rule-test which verifies this false positive error no longer is emitted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added a rule test (testBug8056) in DeadForeachRuleTest with a test data file that verifies the "Empty array passed to foreach" false positive is no longer emitted when iterating over an array that was modified through a reference variable. The test passes successfully.

assertType("'foo'", $i);
}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ public function testBug2457(): void
$this->analyse([__DIR__ . '/data/bug-2457.php'], []);
}

public function testBug8056(): void
{
$this->analyse([__DIR__ . '/data/bug-8056.php'], []);
}

}
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-8056.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types = 1);

namespace Bug8056Rule;

$array = [];
$tmp = &$array;
$tmp[] = 'foo';

foreach ($array as $i) {

}
Loading