Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a2dc7f2
Fix phpstan/phpstan#14333: Setting an array key doesn't update a refe…
VincentLanglet Mar 20, 2026
cee3ee4
Add test for nested array reference tracking limitation
phpstan-bot Mar 20, 2026
452ef48
Skip implicit-key by-ref tracking when index is uncertain due to non-…
phpstan-bot Mar 20, 2026
5f2bb40
Simplify
VincentLanglet Mar 20, 2026
ba41a09
Fix
VincentLanglet Mar 20, 2026
9b8aef9
Implement nested array reference tracking
phpstan-bot Mar 20, 2026
408e173
Simplify
VincentLanglet Mar 20, 2026
d3c6ba1
Add comment explaining why implicitIndex is set to null for non-const…
phpstan-bot Mar 20, 2026
94ce927
Move intertwined ref preservation from assignVariable to invalidateEx…
phpstan-bot Mar 20, 2026
2418eaa
Move intertwined ref preservation condition into shouldInvalidateExpr…
phpstan-bot Mar 20, 2026
138d9dd
Simplify
VincentLanglet Mar 20, 2026
0d5dc30
Rename and simplify isDimFetchPathReachable to absorb nested check
phpstan-bot Mar 20, 2026
da620de
Add failing test
VincentLanglet Mar 20, 2026
5837156
Fix lint
VincentLanglet Mar 20, 2026
0222fd8
Prevent circular back-propagation of intertwined refs during variable…
phpstan-bot Mar 20, 2026
e7d6282
Add test for multi-scalar key values in array by-ref tracking
phpstan-bot Mar 21, 2026
2c00eda
Use toArrayKey() for proper array key coercion and add tests for mult…
phpstan-bot Mar 21, 2026
0b7b5e1
Add tests
VincentLanglet Mar 21, 2026
5a99dab
Use toArrayKey() for proper array key coercion and add tests for mult…
phpstan-bot Mar 21, 2026
c621e9e
Remove dim expression coercion, fix HasOffsetValueType key coercion i…
phpstan-bot Mar 21, 2026
eda0fc1
Check isConstantScalarValue() on coerced array key type in HasOffsetV…
phpstan-bot Mar 23, 2026
08498ff
Apply toArrayKey() before isConstantScalarValue() check in HasOffsetV…
phpstan-bot Mar 23, 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 @@
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 @@
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()) {

Check warning on line 959 in src/Analyser/ExprHandler/AssignHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if (is_int($keyValue) && $keyValue >= $implicitIndex) { $implicitIndex = $keyValue + 1; } - } elseif (!$keyType->isInteger()->no()) { + } elseif ($keyType->isInteger()->yes()) { // Key could be an integer, but we don't know which one, // so subsequent implicit indices are unpredictable $implicitIndex = null;

Check warning on line 959 in src/Analyser/ExprHandler/AssignHandler.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if (is_int($keyValue) && $keyValue >= $implicitIndex) { $implicitIndex = $keyValue + 1; } - } elseif (!$keyType->isInteger()->no()) { + } elseif ($keyType->isInteger()->yes()) { // Key could be an integer, but we don't know which one, // so subsequent implicit indices are unpredictable $implicitIndex = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the mutation suggestests this might need/miss a test with either integer-range and/or union of integers

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests also confirmed passing (11,631 tests, 78,867 assertions). Everything is done and pushed.

// 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 non-empty-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 @@
$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 @@
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 @@
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()) {

Check warning on line 2652 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $varType = $scope->getType($dimFetch->var); $dimType = $scope->getType($dimFetch->dim); - if (!$varType->hasOffsetValueType($dimType)->yes()) { + if ($varType->hasOffsetValueType($dimType)->no()) { return false; }

Check warning on line 2652 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $varType = $scope->getType($dimFetch->var); $dimType = $scope->getType($dimFetch->dim); - if (!$varType->hasOffsetValueType($dimType)->yes()) { + if ($varType->hasOffsetValueType($dimType)->no()) { return false; }
return false;
}

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

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

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 @@
);
}

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
6 changes: 4 additions & 2 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@

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

Check warning on line 160 in src/Type/Accessory/HasOffsetValueType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic { $arrayKeyType = $offsetType->toArrayKey(); - if ($arrayKeyType->isConstantScalarValue()->yes() && $arrayKeyType->equals($this->offsetType)) { + if (!$arrayKeyType->isConstantScalarValue()->no() && $arrayKeyType->equals($this->offsetType)) { return TrinaryLogic::createYes(); }

Check warning on line 160 in src/Type/Accessory/HasOffsetValueType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic { $arrayKeyType = $offsetType->toArrayKey(); - if ($arrayKeyType->isConstantScalarValue()->yes() && $arrayKeyType->equals($this->offsetType)) { + if (!$arrayKeyType->isConstantScalarValue()->no() && $arrayKeyType->equals($this->offsetType)) { return TrinaryLogic::createYes(); }
return TrinaryLogic::createYes();
}

Expand All @@ -165,7 +166,8 @@

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

Check warning on line 170 in src/Type/Accessory/HasOffsetValueType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function getOffsetValueType(Type $offsetType): Type { $arrayKeyType = $offsetType->toArrayKey(); - if ($arrayKeyType->isConstantScalarValue()->yes() && $arrayKeyType->equals($this->offsetType)) { + if (!$arrayKeyType->isConstantScalarValue()->no() && $arrayKeyType->equals($this->offsetType)) { return $this->valueType; }

Check warning on line 170 in src/Type/Accessory/HasOffsetValueType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ public function getOffsetValueType(Type $offsetType): Type { $arrayKeyType = $offsetType->toArrayKey(); - if ($arrayKeyType->isConstantScalarValue()->yes() && $arrayKeyType->equals($this->offsetType)) { + if (!$arrayKeyType->isConstantScalarValue()->no() && $arrayKeyType->equals($this->offsetType)) { return $this->valueType; }
return $this->valueType;
}

Expand Down
Loading
Loading