From c548c5297cc72d20abe12cb77acfd549ac5a068e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:26:50 +0000 Subject: [PATCH 01/14] Fix wrongly reported empty array when using byRef in array items - Widen variable type to mixed when passed by reference inside an array literal ([&$var, ...]) - This fixes false positives like "Comparison operation > between 0 and 0 is always false" when variables are passed by reference through call_user_func_array - Added regression test in tests/PHPStan/Analyser/nsrt/bug-6799.php - The root cause was that ArrayHandler did not account for byRef array items, so PHPStan retained the initial precise type (e.g. array{}) even after a reference was created Closes https://github.com/phpstan/phpstan/issues/6799 --- src/Analyser/ExprHandler/ArrayHandler.php | 6 ++++ tests/PHPStan/Analyser/nsrt/bug-6799.php | 38 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-6799.php diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index a4d150b0e2..f2a8ef20c0 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -15,6 +15,7 @@ use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_merge; @@ -66,6 +67,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); $scope = $valueResult->getScope(); + if (!$arrayItem->byRef) { + continue; + } + + $scope = $scope->assignExpression($arrayItem->value, new MixedType(), new MixedType()); } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php new file mode 100644 index 0000000000..04f1960848 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -0,0 +1,38 @@ + $value) { + call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); + } + assertType('mixed', $whereFilter); + } + } +} From 5ed6c617b434f6e36bce76f5bddec53c4aed2f07 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 12 Mar 2026 07:21:40 +0100 Subject: [PATCH 02/14] Update bug-6799.php --- tests/PHPStan/Analyser/nsrt/bug-6799.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php index 04f1960848..bbf25372bc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -32,7 +32,7 @@ protected function listingAddWhereFilterAtable(array $filterValues, array &$wher foreach ($filterValues as $type => $value) { call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); } - assertType('mixed', $whereFilter); + assertType('mixed', $whereFilter); // could be string[] } } } From 8fbc9449b94854bcd9afeab57a9835c39d55cd87 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 12 Mar 2026 07:26:22 +0100 Subject: [PATCH 03/14] added regression test --- tests/PHPStan/Analyser/nsrt/bug-6799.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-6799b.php | 45 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-6799b.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php index bbf25372bc..86737c8c58 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -32,7 +32,7 @@ protected function listingAddWhereFilterAtable(array $filterValues, array &$wher foreach ($filterValues as $type => $value) { call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); } - assertType('mixed', $whereFilter); // could be string[] + assertType('mixed', $whereFilter); // could be array } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799b.php b/tests/PHPStan/Analyser/nsrt/bug-6799b.php new file mode 100644 index 0000000000..79f77944b0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6799b.php @@ -0,0 +1,45 @@ + $value) { + $this->listingAddWhereFilterAtableDefault($whereFilter, 'xxxxx', $filters[$type], $value); + } + assertType('array', $whereFilter); // could be string[] + } + } +} From 58e5d140725627118e2cc25064759b5ec96a81de Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 12 Mar 2026 07:26:50 +0100 Subject: [PATCH 04/14] Update bug-6799b.php --- tests/PHPStan/Analyser/nsrt/bug-6799b.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799b.php b/tests/PHPStan/Analyser/nsrt/bug-6799b.php index 79f77944b0..057409e7ef 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799b.php @@ -39,7 +39,7 @@ protected function listingAddWhereFilterAtable(array $filterValues, array &$wher foreach ($filterValues as $type => $value) { $this->listingAddWhereFilterAtableDefault($whereFilter, 'xxxxx', $filters[$type], $value); } - assertType('array', $whereFilter); // could be string[] + assertType('array', $whereFilter); } } } From 2cb5e98b9ec59d7e8adeaf5cea0fd82143ce6d04 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 12 Mar 2026 16:36:51 +0000 Subject: [PATCH 05/14] Add comprehensive tests for byRef in array literals Address review feedback by adding tests for standalone `$b = [&$a]` patterns (not just call_user_func_array), including by-ref with keys and multiple by-ref items in a single array. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-6799.php | 38 +++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php index 86737c8c58..dd338281cc 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -32,7 +32,43 @@ protected function listingAddWhereFilterAtable(array $filterValues, array &$wher foreach ($filterValues as $type => $value) { call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); } - assertType('mixed', $whereFilter); // could be array + assertType('mixed', $whereFilter); } } } + +/** + * @param mixed $foo + */ +function foo($foo): void {} + +function testByRefInArray(): void +{ + $a = []; + assertType('array{}', $a); + + $b = [&$a]; + assertType('mixed', $a); + + foo($b); + assertType('mixed', $a); +} + +function testByRefInArrayWithKey(): void +{ + $a = 'hello'; + assertType("'hello'", $a); + + $b = ['key' => &$a]; + assertType('mixed', $a); +} + +function testMultipleByRefInArray(): void +{ + $a = 1; + $c = 'test'; + + $b = [&$a, 'normal', &$c]; + assertType('mixed', $a); + assertType('mixed', $c); +} From d285cf54b16d265e9331369181ef2db42993f925 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 13 Mar 2026 07:16:49 +0100 Subject: [PATCH 06/14] Create bug-6799c.php --- tests/PHPStan/Analyser/nsrt/bug-6799c.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-6799c.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799c.php b/tests/PHPStan/Analyser/nsrt/bug-6799c.php new file mode 100644 index 0000000000..cca6361076 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6799c.php @@ -0,0 +1,18 @@ + Date: Fri, 13 Mar 2026 07:20:18 +0100 Subject: [PATCH 07/14] Update DefinedVariableRuleTest.php --- .../Rules/Variables/DefinedVariableRuleTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index a05ca24efd..bc28c4d187 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1514,4 +1514,19 @@ public function testBug14117(): void ]); } + public function testBug6799c(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6799c.php'], [ + [ + 'Variable $x might not be defined.', + 9 + ] + ]); + } + } From 037a1c6deb93521ef6e9fd93492c0771375db30e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 13 Mar 2026 07:21:36 +0100 Subject: [PATCH 08/14] Update DefinedVariableRuleTest.php --- tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index bc28c4d187..7805c9948b 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1524,8 +1524,8 @@ public function testBug6799c(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6799c.php'], [ [ 'Variable $x might not be defined.', - 9 - ] + 9, + ], ]); } From 9697ad029abf986ddd2f734b49f1d90ca5face31 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 13 Mar 2026 07:30:41 +0100 Subject: [PATCH 09/14] fix tests --- tests/PHPStan/Analyser/nsrt/bug-6799.php | 3 +++ tests/PHPStan/Analyser/nsrt/bug-6799b.php | 2 ++ .../NumberComparisonOperatorsConstantConditionRuleTest.php | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php index dd338281cc..0bf4792054 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -33,6 +33,9 @@ protected function listingAddWhereFilterAtable(array $filterValues, array &$wher call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); } assertType('mixed', $whereFilter); + if (count($whereFilter) > 0) { + $where[] = "(" . implode(" AND ", $whereFilter) . ")"; + } } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799b.php b/tests/PHPStan/Analyser/nsrt/bug-6799b.php index 057409e7ef..1aa9c0a116 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799b.php @@ -1,5 +1,7 @@ analyse([__DIR__ . '/../../Analyser/nsrt/bug-6799.php'], []); + } + } From 47445ba9b1de2825e3e8ab843b7300972b24a66a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 13 Mar 2026 07:39:27 +0100 Subject: [PATCH 10/14] Update bug-6799b.php --- tests/PHPStan/Analyser/nsrt/bug-6799b.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799b.php b/tests/PHPStan/Analyser/nsrt/bug-6799b.php index 1aa9c0a116..f0af35d829 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799b.php @@ -1,6 +1,6 @@ Date: Fri, 13 Mar 2026 09:16:44 +0100 Subject: [PATCH 11/14] Add some comments --- tests/PHPStan/Analyser/nsrt/bug-6799.php | 18 ++++++++++++++---- tests/PHPStan/Analyser/nsrt/bug-6799c.php | 9 +++++---- .../Variables/DefinedVariableRuleTest.php | 7 +------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php index 0bf4792054..99938ec9ac 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -51,7 +51,7 @@ function testByRefInArray(): void assertType('array{}', $a); $b = [&$a]; - assertType('mixed', $a); + assertType('mixed', $a); // Could stay array{} foo($b); assertType('mixed', $a); @@ -63,7 +63,10 @@ function testByRefInArrayWithKey(): void assertType("'hello'", $a); $b = ['key' => &$a]; - assertType('mixed', $a); + assertType('mixed', $a); // Could stay 'hello' + + $b['key'] = 42; + assertType('mixed', $a); // Could be 42 } function testMultipleByRefInArray(): void @@ -72,6 +75,13 @@ function testMultipleByRefInArray(): void $c = 'test'; $b = [&$a, 'normal', &$c]; - assertType('mixed', $a); - assertType('mixed', $c); + assertType('mixed', $a); // Could stay 1 + assertType('mixed', $c); // Could stay 'test' + + $b[0] = 2; + $b[1] = 'foo'; + $b[2] = 'bar'; + + assertType('mixed', $a); // Could be 2 + assertType('mixed', $c); // Could be 'bar' } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799c.php b/tests/PHPStan/Analyser/nsrt/bug-6799c.php index cca6361076..72eddc8f79 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799c.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799c.php @@ -6,13 +6,14 @@ // https://3v4l.org/g5UjS +$x = null; +assertType('null', $x); $a = [&$x]; +assertType('mixed', $x); // Could stay null + function doFoo(array &$arr) { $arr[0] = 'string'; } -var_dump($x); -assertType('mixed', $x); doFoo($a); -var_dump($x); -assertType('mixed', $x); // could be string +assertType('mixed', $x); diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 7805c9948b..8e2c00264b 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1521,12 +1521,7 @@ public function testBug6799c(): void $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6799c.php'], [ - [ - 'Variable $x might not be defined.', - 9, - ], - ]); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6799c.php'], []); } } From 3c97119b6e582a4724505468c45d6f3242c60c3f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 13 Mar 2026 08:28:17 +0000 Subject: [PATCH 12/14] Fix undefined variable false positive for by-reference array items Restore bug-6799c.php test to use undefined variable in [&$x] (the previous $x = null assignment defeated the test purpose). Allow undefined expressions in by-reference array items using the same set/unset pattern used for function call by-reference arguments. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/ArrayHandler.php | 5 +++++ tests/PHPStan/Analyser/nsrt/bug-6799c.php | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index f2a8ef20c0..cb8f352838 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -61,6 +61,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $keyResult->getScope(); } + if ($arrayItem->byRef) { + $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($scope, $arrayItem->value); + } + $valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); @@ -71,6 +75,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex continue; } + $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $arrayItem->value); $scope = $scope->assignExpression($arrayItem->value, new MixedType(), new MixedType()); } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799c.php b/tests/PHPStan/Analyser/nsrt/bug-6799c.php index 72eddc8f79..43e34ecb25 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799c.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799c.php @@ -6,10 +6,8 @@ // https://3v4l.org/g5UjS -$x = null; -assertType('null', $x); $a = [&$x]; -assertType('mixed', $x); // Could stay null +assertType('mixed', $x); function doFoo(array &$arr) { $arr[0] = 'string'; From e013a690eaa456d9e6cbfaa28e6dd0a5c377d7b8 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 20 Mar 2026 09:06:36 +0000 Subject: [PATCH 13/14] Use IntertwinedVariableByReferenceWithExpr for by-ref array items Replace the immediate MixedType assignment with the intertwined variable mechanism from PR #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 --- src/Analyser/ExprHandler/ArrayHandler.php | 18 ++++++++++-- src/Analyser/ExprHandler/AssignHandler.php | 32 ++++++++++++++++++++++ src/Analyser/NodeScopeResolver.php | 24 ++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-6799.php | 16 +++++------ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index cb8f352838..35b634bcc1 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -12,12 +12,13 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_merge; +use function is_string; /** * @implements ExprHandler @@ -76,7 +77,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $arrayItem->value); - $scope = $scope->assignExpression($arrayItem->value, new MixedType(), new MixedType()); + + if ($arrayItem->value instanceof Expr\Variable && is_string($arrayItem->value->name)) { + $varName = $arrayItem->value->name; + $type = $scope->getType($arrayItem->value); + $nativeType = $scope->getNativeType($arrayItem->value); + // Ensure the variable is defined (PHP creates it if undefined when used by-ref) + $scope = $scope->assignExpression($arrayItem->value, $type, $nativeType); + // Register intertwined relationship + $scope = $scope->assignExpression( + new IntertwinedVariableByReferenceWithExpr($varName, $expr, new Expr\Variable($varName)), + $type, + $nativeType, + ); + } } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 10b1f135eb..ddcba3e078 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -179,6 +179,38 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex ); } + if ( + $expr instanceof Assign + && $expr->var instanceof Variable + && is_string($expr->var->name) + && $expr->expr instanceof Expr\Array_ + ) { + $targetVarName = $expr->var->name; + foreach ($expr->expr->items as $i => $item) { + if (!$item->byRef) { + continue; + } + if (!($item->value instanceof Variable) || !is_string($item->value->name)) { + continue; + } + $refVarName = $item->value->name; + $key = $item->key ?? new Node\Scalar\Int_($i); + $type = $scope->getType($item->value); + $nativeType = $scope->getNativeType($item->value); + + // When $refVarName is assigned, update $targetVar[$key] + $scope = $scope->assignExpression( + new IntertwinedVariableByReferenceWithExpr( + $refVarName, + new ArrayDimFetch(new Variable($targetVarName), $key), + new Variable($refVarName), + ), + $type, + $nativeType, + ); + } + } + $vars = $nodeScopeResolver->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8935ade8e2..aca02eefde 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3532,6 +3532,30 @@ public function processArgs( $scope = $scope->invalidateExpression($arg->value, true); } } + + if ( + !$assignByReference + && $calleeReflection !== null + && !$calleeReflection->hasSideEffects()->no() + && $arg->value instanceof Array_ + ) { + foreach ($arg->value->items as $item) { + if (!$item->byRef) { + continue; + } + if (!($item->value instanceof Variable) || !is_string($item->value->name)) { + continue; + } + $scope = $this->processVirtualAssign( + $scope, + $storage, + $stmt, + $item->value, + new TypeExpr(new MixedType()), + $nodeCallback, + )->getScope(); + } + } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php index 99938ec9ac..88f041e16a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -51,10 +51,10 @@ function testByRefInArray(): void assertType('array{}', $a); $b = [&$a]; - assertType('mixed', $a); // Could stay array{} + assertType('array{}', $a); foo($b); - assertType('mixed', $a); + assertType('array{}', $a); } function testByRefInArrayWithKey(): void @@ -63,10 +63,10 @@ function testByRefInArrayWithKey(): void assertType("'hello'", $a); $b = ['key' => &$a]; - assertType('mixed', $a); // Could stay 'hello' + assertType("'hello'", $a); $b['key'] = 42; - assertType('mixed', $a); // Could be 42 + assertType("'hello'", $a); } function testMultipleByRefInArray(): void @@ -75,13 +75,13 @@ function testMultipleByRefInArray(): void $c = 'test'; $b = [&$a, 'normal', &$c]; - assertType('mixed', $a); // Could stay 1 - assertType('mixed', $c); // Could stay 'test' + assertType('1', $a); + assertType("'test'", $c); $b[0] = 2; $b[1] = 'foo'; $b[2] = 'bar'; - assertType('mixed', $a); // Could be 2 - assertType('mixed', $c); // Could be 'bar' + assertType('1', $a); + assertType("'test'", $c); } From bc76dc04f807d5ac6c76539353e74bfff4fbac21 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 20 Mar 2026 10:50:58 +0000 Subject: [PATCH 14/14] Propagate types bidirectionally through by-ref array items - Add reverse intertwined entries in AssignHandler so that assigning to $b[$key] propagates through to the referenced variable $a when $b = [&$a] - Preserve intertwined entries during invalidation when the invalidated variable is the entry's trigger or target variable - Widen referenced variables to mixed when a variable containing references is passed to an impure function - Update test assertions to reflect correct propagation behavior Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/AssignHandler.php | 11 ++++++++ src/Analyser/MutatingScope.php | 16 ++++++++++- src/Analyser/NodeScopeResolver.php | 31 ++++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-6799.php | 8 +++--- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index ddcba3e078..c7f77ed2de 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -208,6 +208,17 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $type, $nativeType, ); + + // When $targetVar is assigned (e.g. $b[0] = 42), update $refVarName + $scope = $scope->assignExpression( + new IntertwinedVariableByReferenceWithExpr( + $targetVarName, + new Variable($refVarName), + new ArrayDimFetch(new Variable($targetVarName), $key), + ), + $type, + $nativeType, + ); } } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 42fe96957d..8023da6e91 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2835,7 +2835,21 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require $exprExpr = $exprTypeHolder->getExpr(); if ( $exprExpr instanceof IntertwinedVariableByReferenceWithExpr - && $exprExpr->isVariableToVariableReference() + && ( + $exprExpr->isVariableToVariableReference() + || ( + $expressionToInvalidate instanceof Variable + && is_string($expressionToInvalidate->name) + && ( + $exprExpr->getVariableName() === $expressionToInvalidate->name + || ( + $exprExpr->getExpr() instanceof Variable + && is_string($exprExpr->getExpr()->name) + && $exprExpr->getExpr()->name === $expressionToInvalidate->name + ) + ) + ) + ) ) { continue; } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index aca02eefde..c77833df12 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -74,6 +74,7 @@ use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\ExistingArrayDimFetch; +use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\Expr\ForeachValueByRefExpr; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; @@ -3556,6 +3557,36 @@ public function processArgs( )->getScope(); } } + + if ( + !$assignByReference + && $calleeReflection !== null + && !$calleeReflection->hasSideEffects()->no() + && $arg->value instanceof Variable + && is_string($arg->value->name) + ) { + $argVarName = $arg->value->name; + foreach ($scope->expressionTypes as $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + if (!$exprExpr instanceof IntertwinedVariableByReferenceWithExpr) { + continue; + } + if ($exprExpr->getVariableName() !== $argVarName) { + continue; + } + if (!($exprExpr->getExpr() instanceof Variable) || !is_string($exprExpr->getExpr()->name)) { + continue; + } + $scope = $this->processVirtualAssign( + $scope, + $storage, + $stmt, + $exprExpr->getExpr(), + new TypeExpr(new MixedType()), + $nodeCallback, + )->getScope(); + } + } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799.php b/tests/PHPStan/Analyser/nsrt/bug-6799.php index 88f041e16a..1f09a9f3fa 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6799.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -54,7 +54,7 @@ function testByRefInArray(): void assertType('array{}', $a); foo($b); - assertType('array{}', $a); + assertType('mixed', $a); } function testByRefInArrayWithKey(): void @@ -66,7 +66,7 @@ function testByRefInArrayWithKey(): void assertType("'hello'", $a); $b['key'] = 42; - assertType("'hello'", $a); + assertType('42', $a); } function testMultipleByRefInArray(): void @@ -82,6 +82,6 @@ function testMultipleByRefInArray(): void $b[1] = 'foo'; $b[2] = 'bar'; - assertType('1', $a); - assertType("'test'", $c); + assertType('2', $a); + assertType("'bar'", $c); }