Skip to content

Commit e013a69

Browse files
phpstan-botclaude
andcommitted
Use IntertwinedVariableByReferenceWithExpr for by-ref array items
Replace the immediate MixedType assignment with the intertwined variable mechanism from PR phpstan#5217, as requested by VincentLanglet. Changes: - ArrayHandler: Register IntertwinedVariableByReferenceWithExpr instead of assigning MixedType when a variable is used by-ref in an array literal. Variables now keep their type after [&$var] instead of becoming mixed. - AssignHandler: When $b = [&$a, ...], set up intertwined entries linking the by-ref variable to the array dimension expression ($b[$key]), enabling forward propagation when the variable is reassigned. - NodeScopeResolver::processArgs: When an array with by-ref items is passed to a possibly-impure function, assign MixedType to the referenced variables via processVirtualAssign. This preserves the original bug fix for call_user_func_array while deferring type widening to the point where the reference actually escapes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3c97119 commit e013a69

4 files changed

Lines changed: 80 additions & 10 deletions

File tree

src/Analyser/ExprHandler/ArrayHandler.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
use PHPStan\Analyser\MutatingScope;
1313
use PHPStan\Analyser\NodeScopeResolver;
1414
use PHPStan\DependencyInjection\AutowiredService;
15+
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
1516
use PHPStan\Node\LiteralArrayItem;
1617
use PHPStan\Node\LiteralArrayNode;
1718
use PHPStan\Reflection\InitializerExprTypeResolver;
18-
use PHPStan\Type\MixedType;
1919
use PHPStan\Type\Type;
2020
use function array_merge;
21+
use function is_string;
2122

2223
/**
2324
* @implements ExprHandler<Array_>
@@ -76,7 +77,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
7677
}
7778

7879
$scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $arrayItem->value);
79-
$scope = $scope->assignExpression($arrayItem->value, new MixedType(), new MixedType());
80+
81+
if ($arrayItem->value instanceof Expr\Variable && is_string($arrayItem->value->name)) {
82+
$varName = $arrayItem->value->name;
83+
$type = $scope->getType($arrayItem->value);
84+
$nativeType = $scope->getNativeType($arrayItem->value);
85+
// Ensure the variable is defined (PHP creates it if undefined when used by-ref)
86+
$scope = $scope->assignExpression($arrayItem->value, $type, $nativeType);
87+
// Register intertwined relationship
88+
$scope = $scope->assignExpression(
89+
new IntertwinedVariableByReferenceWithExpr($varName, $expr, new Expr\Variable($varName)),
90+
$type,
91+
$nativeType,
92+
);
93+
}
8094
}
8195
$nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage);
8296

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,38 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex
179179
);
180180
}
181181

182+
if (
183+
$expr instanceof Assign
184+
&& $expr->var instanceof Variable
185+
&& is_string($expr->var->name)
186+
&& $expr->expr instanceof Expr\Array_
187+
) {
188+
$targetVarName = $expr->var->name;
189+
foreach ($expr->expr->items as $i => $item) {
190+
if (!$item->byRef) {
191+
continue;
192+
}
193+
if (!($item->value instanceof Variable) || !is_string($item->value->name)) {
194+
continue;
195+
}
196+
$refVarName = $item->value->name;
197+
$key = $item->key ?? new Node\Scalar\Int_($i);
198+
$type = $scope->getType($item->value);
199+
$nativeType = $scope->getNativeType($item->value);
200+
201+
// When $refVarName is assigned, update $targetVar[$key]
202+
$scope = $scope->assignExpression(
203+
new IntertwinedVariableByReferenceWithExpr(
204+
$refVarName,
205+
new ArrayDimFetch(new Variable($targetVarName), $key),
206+
new Variable($refVarName),
207+
),
208+
$type,
209+
$nativeType,
210+
);
211+
}
212+
}
213+
182214
$vars = $nodeScopeResolver->getAssignedVariables($expr->var);
183215
if (count($vars) > 0) {
184216
$varChangedScope = false;

src/Analyser/NodeScopeResolver.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3532,6 +3532,30 @@ public function processArgs(
35323532
$scope = $scope->invalidateExpression($arg->value, true);
35333533
}
35343534
}
3535+
3536+
if (
3537+
!$assignByReference
3538+
&& $calleeReflection !== null
3539+
&& !$calleeReflection->hasSideEffects()->no()
3540+
&& $arg->value instanceof Array_
3541+
) {
3542+
foreach ($arg->value->items as $item) {
3543+
if (!$item->byRef) {
3544+
continue;
3545+
}
3546+
if (!($item->value instanceof Variable) || !is_string($item->value->name)) {
3547+
continue;
3548+
}
3549+
$scope = $this->processVirtualAssign(
3550+
$scope,
3551+
$storage,
3552+
$stmt,
3553+
$item->value,
3554+
new TypeExpr(new MixedType()),
3555+
$nodeCallback,
3556+
)->getScope();
3557+
}
3558+
}
35353559
}
35363560
}
35373561

tests/PHPStan/Analyser/nsrt/bug-6799.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ function testByRefInArray(): void
5151
assertType('array{}', $a);
5252

5353
$b = [&$a];
54-
assertType('mixed', $a); // Could stay array{}
54+
assertType('array{}', $a);
5555

5656
foo($b);
57-
assertType('mixed', $a);
57+
assertType('array{}', $a);
5858
}
5959

6060
function testByRefInArrayWithKey(): void
@@ -63,10 +63,10 @@ function testByRefInArrayWithKey(): void
6363
assertType("'hello'", $a);
6464

6565
$b = ['key' => &$a];
66-
assertType('mixed', $a); // Could stay 'hello'
66+
assertType("'hello'", $a);
6767

6868
$b['key'] = 42;
69-
assertType('mixed', $a); // Could be 42
69+
assertType("'hello'", $a);
7070
}
7171

7272
function testMultipleByRefInArray(): void
@@ -75,13 +75,13 @@ function testMultipleByRefInArray(): void
7575
$c = 'test';
7676

7777
$b = [&$a, 'normal', &$c];
78-
assertType('mixed', $a); // Could stay 1
79-
assertType('mixed', $c); // Could stay 'test'
78+
assertType('1', $a);
79+
assertType("'test'", $c);
8080

8181
$b[0] = 2;
8282
$b[1] = 'foo';
8383
$b[2] = 'bar';
8484

85-
assertType('mixed', $a); // Could be 2
86-
assertType('mixed', $c); // Could be 'bar'
85+
assertType('1', $a);
86+
assertType("'test'", $c);
8787
}

0 commit comments

Comments
 (0)