diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index a4d150b0e2..35b634bcc1 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -12,11 +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\Type; use function array_merge; +use function is_string; /** * @implements ExprHandler @@ -60,12 +62,35 @@ 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()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); $scope = $valueResult->getScope(); + if (!$arrayItem->byRef) { + continue; + } + + $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $arrayItem->value); + + 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..c7f77ed2de 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -179,6 +179,49 @@ 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, + ); + + // 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, + ); + } + } + $vars = $nodeScopeResolver->getAssignedVariables($expr->var); if (count($vars) > 0) { $varChangedScope = false; 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 8935ade8e2..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; @@ -3532,6 +3533,60 @@ 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(); + } + } + + 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 new file mode 100644 index 0000000000..1f09a9f3fa --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6799.php @@ -0,0 +1,87 @@ + $value) { + call_user_func_array(array($this, 'listingAddWhereFilterAtableDefault'), array(&$whereFilter, 'xxxx', $filters[$type], $value)); + } + assertType('mixed', $whereFilter); + if (count($whereFilter) > 0) { + $where[] = "(" . implode(" AND ", $whereFilter) . ")"; + } + } + } +} + +/** + * @param mixed $foo + */ +function foo($foo): void {} + +function testByRefInArray(): void +{ + $a = []; + assertType('array{}', $a); + + $b = [&$a]; + assertType('array{}', $a); + + foo($b); + assertType('mixed', $a); +} + +function testByRefInArrayWithKey(): void +{ + $a = 'hello'; + assertType("'hello'", $a); + + $b = ['key' => &$a]; + assertType("'hello'", $a); + + $b['key'] = 42; + assertType('42', $a); +} + +function testMultipleByRefInArray(): void +{ + $a = 1; + $c = 'test'; + + $b = [&$a, 'normal', &$c]; + assertType('1', $a); + assertType("'test'", $c); + + $b[0] = 2; + $b[1] = 'foo'; + $b[2] = 'bar'; + + assertType('2', $a); + assertType("'bar'", $c); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799b.php b/tests/PHPStan/Analyser/nsrt/bug-6799b.php new file mode 100644 index 0000000000..f0af35d829 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6799b.php @@ -0,0 +1,47 @@ + $value) { + $this->listingAddWhereFilterAtableDefault($whereFilter, 'xxxxx', $filters[$type], $value); + } + assertType('array', $whereFilter); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6799c.php b/tests/PHPStan/Analyser/nsrt/bug-6799c.php new file mode 100644 index 0000000000..43e34ecb25 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-6799c.php @@ -0,0 +1,17 @@ +analyse([__DIR__ . '/../../Analyser/nsrt/bug-6799.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index a05ca24efd..8e2c00264b 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1514,4 +1514,14 @@ 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'], []); + } + }