Skip to content

Commit 59ca979

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix phpstan/phpstan#14333: Setting an array key doesn't update a reference
- When creating an array with by-reference items (e.g. $b = ['key' => &$a]), register IntertwinedVariableByReferenceWithExpr entries so that subsequent assignments to array offsets propagate type changes to the referenced variables - Preserve non-variable-to-variable intertwined refs in assignVariable() so they survive the recursive propagation chain without being invalidated - New regression test in tests/PHPStan/Analyser/nsrt/bug-14333.php
1 parent 3c63f68 commit 59ca979

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
use function array_slice;
7070
use function count;
7171
use function in_array;
72+
use function is_int;
7273
use function is_string;
7374

7475
/**
@@ -315,6 +316,57 @@ public function processAssignVar(
315316
foreach ($conditionalExpressions as $exprString => $holders) {
316317
$scope = $scope->addConditionalExpressions($exprString, $holders);
317318
}
319+
320+
if ($assignedExpr instanceof Expr\Array_) {
321+
$implicitIndex = 0;
322+
foreach ($assignedExpr->items as $arrayItem) {
323+
if ($arrayItem->key !== null) {
324+
$keyType = $scope->getType($arrayItem->key);
325+
if ($keyType->isConstantScalarValue()->yes()) {
326+
$keyValues = $keyType->getConstantScalarValues();
327+
if (count($keyValues) === 1) {
328+
$keyValue = $keyValues[0];
329+
if (is_int($keyValue) && $keyValue >= $implicitIndex) {
330+
$implicitIndex = $keyValue + 1;
331+
}
332+
}
333+
}
334+
}
335+
336+
if (!$arrayItem->byRef || !$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) {
337+
if ($arrayItem->key === null) {
338+
$implicitIndex++;
339+
}
340+
continue;
341+
}
342+
343+
$refVarName = $arrayItem->value->name;
344+
if ($arrayItem->key !== null) {
345+
$dimExpr = $arrayItem->key;
346+
} else {
347+
$dimExpr = new Node\Scalar\Int_($implicitIndex);
348+
$implicitIndex++;
349+
}
350+
351+
$dimFetchExpr = new ArrayDimFetch(new Variable($var->name), $dimExpr);
352+
$refType = $scope->getType(new Variable($refVarName));
353+
$refNativeType = $scope->getNativeType(new Variable($refVarName));
354+
355+
// When $varName's array key changes, update $refVarName
356+
$scope = $scope->assignExpression(
357+
new IntertwinedVariableByReferenceWithExpr($var->name, new Variable($refVarName), $dimFetchExpr),
358+
$refType,
359+
$refNativeType,
360+
);
361+
362+
// When $refVarName changes, update $varName's array key
363+
$scope = $scope->assignExpression(
364+
new IntertwinedVariableByReferenceWithExpr($refVarName, $dimFetchExpr, new Variable($refVarName)),
365+
$refType,
366+
$refNativeType,
367+
);
368+
}
369+
}
318370
} else {
319371
$nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
320372
$hasYield = $hasYield || $nameExprResult->hasYield();

src/Analyser/MutatingScope.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,6 +2573,29 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool
25732573
public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, array $intertwinedPropagatedFrom = []): self
25742574
{
25752575
$node = new Variable($variableName);
2576+
2577+
// Collect non-variable-to-variable intertwined refs for this variable before invalidation,
2578+
// as they may be lost during assignExpression and recursive propagation
2579+
$preservedIntertwinedRefs = [];
2580+
$preservedNativeIntertwinedRefs = [];
2581+
foreach ($this->expressionTypes as $exprString => $exprTypeHolder) {
2582+
$exprExpr = $exprTypeHolder->getExpr();
2583+
if (
2584+
!($exprExpr instanceof IntertwinedVariableByReferenceWithExpr)
2585+
|| $exprExpr->getVariableName() !== $variableName
2586+
|| $exprExpr->isVariableToVariableReference()
2587+
) {
2588+
continue;
2589+
}
2590+
2591+
$preservedIntertwinedRefs[$exprString] = $exprTypeHolder;
2592+
if (!array_key_exists($exprString, $this->nativeExpressionTypes)) {
2593+
continue;
2594+
}
2595+
2596+
$preservedNativeIntertwinedRefs[$exprString] = $this->nativeExpressionTypes[$exprString];
2597+
}
2598+
25762599
$scope = $this->assignExpression($node, $type, $nativeType);
25772600
if ($certainty->no()) {
25782601
throw new ShouldNotHappenException();
@@ -2620,6 +2643,22 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
26202643

26212644
}
26222645

2646+
// Re-add intertwined refs that were lost during propagation
2647+
foreach ($preservedIntertwinedRefs as $exprString => $exprTypeHolder) {
2648+
if (array_key_exists($exprString, $scope->expressionTypes)) {
2649+
continue;
2650+
}
2651+
2652+
$scope->expressionTypes[$exprString] = $exprTypeHolder;
2653+
}
2654+
foreach ($preservedNativeIntertwinedRefs as $exprString => $exprTypeHolder) {
2655+
if (array_key_exists($exprString, $scope->nativeExpressionTypes)) {
2656+
continue;
2657+
}
2658+
2659+
$scope->nativeExpressionTypes[$exprString] = $exprTypeHolder;
2660+
}
2661+
26232662
return $scope;
26242663
}
26252664

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14333;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function testByRefInArrayWithKey(): void
8+
{
9+
$a = 'hello';
10+
assertType("'hello'", $a);
11+
12+
$b = ['key' => &$a];
13+
assertType("'hello'", $a);
14+
15+
$b['key'] = 42;
16+
assertType('42', $a);
17+
}
18+
19+
function testMultipleByRefInArray(): void
20+
{
21+
$a = 1;
22+
$c = 'test';
23+
24+
$b = [&$a, 'normal', &$c];
25+
assertType('1', $a);
26+
assertType("'test'", $c);
27+
28+
$b[0] = 2;
29+
$b[1] = 'foo';
30+
$b[2] = 'bar';
31+
32+
assertType('2', $a);
33+
assertType("'bar'", $c);
34+
}

0 commit comments

Comments
 (0)