Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
59ca979
Fix phpstan/phpstan#14333: Setting an array key doesn't update a refe…
VincentLanglet Mar 20, 2026
85e52c5
Add test for nested array reference tracking limitation
phpstan-bot Mar 20, 2026
40bab7a
Skip implicit-key by-ref tracking when index is uncertain due to non-…
phpstan-bot Mar 20, 2026
1791c8b
Simplify
VincentLanglet Mar 20, 2026
9ba6411
Fix
VincentLanglet Mar 20, 2026
2407895
Implement nested array reference tracking
phpstan-bot Mar 20, 2026
5aee21a
Simplify
VincentLanglet Mar 20, 2026
f6a987e
Add comment explaining why implicitIndex is set to null for non-const…
phpstan-bot Mar 20, 2026
f1f6eb7
Move intertwined ref preservation from assignVariable to invalidateEx…
phpstan-bot Mar 20, 2026
88ea889
Move intertwined ref preservation condition into shouldInvalidateExpr…
phpstan-bot Mar 20, 2026
3200c9f
Simplify
VincentLanglet Mar 20, 2026
c5824eb
Rename and simplify isDimFetchPathReachable to absorb nested check
phpstan-bot Mar 20, 2026
85b5b5f
Add failing test
VincentLanglet Mar 20, 2026
94a3bd9
Fix lint
VincentLanglet Mar 20, 2026
f0ba82a
Prevent circular back-propagation of intertwined refs during variable…
phpstan-bot Mar 20, 2026
01977ee
Add test for multi-scalar key values in array by-ref tracking
phpstan-bot Mar 21, 2026
1bd3083
Use toArrayKey() for proper array key coercion and add tests for mult…
phpstan-bot Mar 21, 2026
1eb9521
Add tests
VincentLanglet Mar 21, 2026
e382c23
Use toArrayKey() for proper array key coercion and add tests for mult…
phpstan-bot Mar 21, 2026
8f7ba26
Remove dim expression coercion, fix HasOffsetValueType key coercion i…
phpstan-bot Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
use PHPStan\Type\ConstantTypeHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticTypeFactory;
Expand All @@ -69,6 +70,7 @@
use function array_slice;
use function count;
use function in_array;
use function is_int;
use function is_string;

/**
Expand Down Expand Up @@ -315,6 +317,10 @@ public function processAssignVar(
foreach ($conditionalExpressions as $exprString => $holders) {
$scope = $scope->addConditionalExpressions($exprString, $holders);
}

if ($assignedExpr instanceof Expr\Array_) {
$scope = $this->processArrayByRefItems($scope, $var->name, $assignedExpr, new Variable($var->name));
}
} else {
$nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
$hasYield = $hasYield || $nameExprResult->hasYield();
Expand Down Expand Up @@ -936,6 +942,67 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr
return $scope->hasVariableType($varNode->name)->negate();
}

private function processArrayByRefItems(MutatingScope $scope, string $rootVarName, Expr\Array_ $arrayExpr, Expr $parentExpr): MutatingScope
{
$implicitIndex = 0;
foreach ($arrayExpr->items as $arrayItem) {
if ($arrayItem->key !== null) {
$keyType = $scope->getType($arrayItem->key)->toArrayKey();

if ($implicitIndex !== null) {
$keyValues = $keyType->getConstantScalarValues();
if (count($keyValues) === 1) {
$keyValue = $keyValues[0];
if (is_int($keyValue) && $keyValue >= $implicitIndex) {
$implicitIndex = $keyValue + 1;
}
} elseif (!$keyType->isInteger()->no()) {
// Key could be an integer, but we don't know which one,
// so subsequent implicit indices are unpredictable
$implicitIndex = null;
}
}

$dimExpr = $arrayItem->key;
} elseif ($implicitIndex !== null) {
$dimExpr = new Node\Scalar\Int_($implicitIndex);
$implicitIndex++;
} else {
$dimExpr = new TypeExpr(new IntegerType());
}

if ($arrayItem->value instanceof Expr\Array_) {
$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);
$scope = $this->processArrayByRefItems($scope, $rootVarName, $arrayItem->value, $dimFetchExpr);
}

if (!$arrayItem->byRef || !$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) {
continue;
}

$refVarName = $arrayItem->value->name;
$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);
$refType = $scope->getType(new Variable($refVarName));
$refNativeType = $scope->getNativeType(new Variable($refVarName));

// When $rootVarName's array key changes, update $refVarName
$scope = $scope->assignExpression(
new IntertwinedVariableByReferenceWithExpr($rootVarName, new Variable($refVarName), $dimFetchExpr),
$refType,
$refNativeType,
);

// When $refVarName changes, update $rootVarName's array key
$scope = $scope->assignExpression(
new IntertwinedVariableByReferenceWithExpr($refVarName, $dimFetchExpr, new Variable($refVarName)),
$refType,
$refNativeType,
);
}

return $scope;
}

/**
* @param list<ArrayDimFetch> $dimFetchStack
* @param list<array{Type|null, ArrayDimFetch}> $offsetTypes
Expand Down
67 changes: 59 additions & 8 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2582,7 +2582,7 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
$scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty);
}

foreach ($scope->expressionTypes as $expressionType) {
foreach ($scope->expressionTypes as $exprString => $expressionType) {
if (!$expressionType->getExpr() instanceof IntertwinedVariableByReferenceWithExpr) {
continue;
}
Expand All @@ -2593,6 +2593,16 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
continue;
}

$assignedExpr = $expressionType->getExpr()->getAssignedExpr();
if (
$assignedExpr instanceof Expr\ArrayDimFetch
&& !$this->isDimFetchPathReachable($scope, $assignedExpr)
) {
unset($scope->expressionTypes[$exprString]);
unset($scope->nativeExpressionTypes[$exprString]);
continue;
}

$has = $scope->hasExpressionType($expressionType->getExpr()->getExpr());
if (
$expressionType->getExpr()->getExpr() instanceof Variable
Expand All @@ -2611,18 +2621,41 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
array_merge($intertwinedPropagatedFrom, [$variableName]),
);
} else {
$targetRootVar = $this->getIntertwinedRefRootVariableName($expressionType->getExpr()->getExpr());
if ($targetRootVar !== null && in_array($targetRootVar, $intertwinedPropagatedFrom, true)) {
continue;
}
$scope = $scope->assignExpression(
$expressionType->getExpr()->getExpr(),
$scope->getType($expressionType->getExpr()->getAssignedExpr()),
$scope->getNativeType($expressionType->getExpr()->getAssignedExpr()),
);
}

}

return $scope;
}

private function isDimFetchPathReachable(self $scope, Expr\ArrayDimFetch $dimFetch): bool
{
if ($dimFetch->dim === null) {
return false;
}

if (!$dimFetch->var instanceof Expr\ArrayDimFetch) {
return true;
}

$varType = $scope->getType($dimFetch->var);
$dimType = $scope->getType($dimFetch->dim);

if (!$varType->hasOffsetValueType($dimType)->yes()) {
return false;
}

return $this->isDimFetchPathReachable($scope, $dimFetch->var);
}

private function unsetExpression(Expr $expr): self
{
$scope = $this;
Expand Down Expand Up @@ -2833,12 +2866,6 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require

foreach ($expressionTypes as $exprString => $exprTypeHolder) {
$exprExpr = $exprTypeHolder->getExpr();
if (
$exprExpr instanceof IntertwinedVariableByReferenceWithExpr
&& $exprExpr->isVariableToVariableReference()
) {
continue;
}
if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, $requireMoreCharacters, $invalidatingClass)) {
continue;
}
Expand Down Expand Up @@ -2906,8 +2933,32 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
);
}

private function getIntertwinedRefRootVariableName(Expr $expr): ?string
{
if ($expr instanceof Variable && is_string($expr->name)) {
return $expr->name;
}
if ($expr instanceof Expr\ArrayDimFetch) {
return $this->getIntertwinedRefRootVariableName($expr->var);
}
return null;
}

private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, string $exprString, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): bool
{
if (
$expr instanceof IntertwinedVariableByReferenceWithExpr
&& $exprToInvalidate instanceof Variable
&& is_string($exprToInvalidate->name)
&& (
$expr->getVariableName() === $exprToInvalidate->name
|| $this->getIntertwinedRefRootVariableName($expr->getExpr()) === $exprToInvalidate->name
|| $this->getIntertwinedRefRootVariableName($expr->getAssignedExpr()) === $exprToInvalidate->name
)
) {
return false;
}

if ($requireMoreCharacters && $exprStringToInvalidate === $exprString) {
return false;
}
Expand Down
10 changes: 0 additions & 10 deletions src/Node/Expr/IntertwinedVariableByReferenceWithExpr.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

use Override;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use PHPStan\Node\VirtualNode;
use function is_string;

final class IntertwinedVariableByReferenceWithExpr extends Expr implements VirtualNode
{
Expand All @@ -31,14 +29,6 @@ public function getAssignedExpr(): Expr
return $this->assignedExpr;
}

public function isVariableToVariableReference(): bool
{
return $this->expr instanceof Variable
&& is_string($this->expr->name)
&& $this->assignedExpr instanceof Variable
&& is_string($this->assignedExpr->name);
}

#[Override]
public function getType(): string
{
Expand Down
4 changes: 2 additions & 2 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public function isOffsetAccessLegal(): TrinaryLogic

public function hasOffsetValueType(Type $offsetType): TrinaryLogic
{
if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) {
if ($offsetType->isConstantScalarValue()->yes() && $offsetType->toArrayKey()->equals($this->offsetType)) {
return TrinaryLogic::createYes();
}

Expand All @@ -165,7 +165,7 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic

public function getOffsetValueType(Type $offsetType): Type
{
if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) {
if ($offsetType->isConstantScalarValue()->yes() && $offsetType->toArrayKey()->equals($this->offsetType)) {
return $this->valueType;
}

Expand Down
Loading
Loading