Skip to content

Commit ee25c7b

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 7fc92ed + 85c4052 commit ee25c7b

File tree

5 files changed

+324
-20
lines changed

5 files changed

+324
-20
lines changed

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
use PHPStan\Type\ConstantTypeHelper;
5656
use PHPStan\Type\ErrorType;
5757
use PHPStan\Type\IntegerRangeType;
58+
use PHPStan\Type\IntegerType;
5859
use PHPStan\Type\MixedType;
5960
use PHPStan\Type\ObjectType;
6061
use PHPStan\Type\StaticTypeFactory;
@@ -69,6 +70,7 @@
6970
use function array_slice;
7071
use function count;
7172
use function in_array;
73+
use function is_int;
7274
use function is_string;
7375

7476
/**
@@ -315,6 +317,10 @@ public function processAssignVar(
315317
foreach ($conditionalExpressions as $exprString => $holders) {
316318
$scope = $scope->addConditionalExpressions($exprString, $holders);
317319
}
320+
321+
if ($assignedExpr instanceof Expr\Array_) {
322+
$scope = $this->processArrayByRefItems($scope, $var->name, $assignedExpr, new Variable($var->name));
323+
}
318324
} else {
319325
$nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
320326
$hasYield = $hasYield || $nameExprResult->hasYield();
@@ -936,6 +942,67 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr
936942
return $scope->hasVariableType($varNode->name)->negate();
937943
}
938944

945+
private function processArrayByRefItems(MutatingScope $scope, string $rootVarName, Expr\Array_ $arrayExpr, Expr $parentExpr): MutatingScope
946+
{
947+
$implicitIndex = 0;
948+
foreach ($arrayExpr->items as $arrayItem) {
949+
if ($arrayItem->key !== null) {
950+
$keyType = $scope->getType($arrayItem->key)->toArrayKey();
951+
952+
if ($implicitIndex !== null) {
953+
$keyValues = $keyType->getConstantScalarValues();
954+
if (count($keyValues) === 1) {
955+
$keyValue = $keyValues[0];
956+
if (is_int($keyValue) && $keyValue >= $implicitIndex) {
957+
$implicitIndex = $keyValue + 1;
958+
}
959+
} elseif (!$keyType->isInteger()->no()) {
960+
// Key could be an integer, but we don't know which one,
961+
// so subsequent implicit indices are unpredictable
962+
$implicitIndex = null;
963+
}
964+
}
965+
966+
$dimExpr = $arrayItem->key;
967+
} elseif ($implicitIndex !== null) {
968+
$dimExpr = new Node\Scalar\Int_($implicitIndex);
969+
$implicitIndex++;
970+
} else {
971+
$dimExpr = new TypeExpr(new IntegerType());
972+
}
973+
974+
if ($arrayItem->value instanceof Expr\Array_) {
975+
$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);
976+
$scope = $this->processArrayByRefItems($scope, $rootVarName, $arrayItem->value, $dimFetchExpr);
977+
}
978+
979+
if (!$arrayItem->byRef || !$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) {
980+
continue;
981+
}
982+
983+
$refVarName = $arrayItem->value->name;
984+
$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);
985+
$refType = $scope->getType(new Variable($refVarName));
986+
$refNativeType = $scope->getNativeType(new Variable($refVarName));
987+
988+
// When $rootVarName's array key changes, update $refVarName
989+
$scope = $scope->assignExpression(
990+
new IntertwinedVariableByReferenceWithExpr($rootVarName, new Variable($refVarName), $dimFetchExpr),
991+
$refType,
992+
$refNativeType,
993+
);
994+
995+
// When $refVarName changes, update $rootVarName's array key
996+
$scope = $scope->assignExpression(
997+
new IntertwinedVariableByReferenceWithExpr($refVarName, $dimFetchExpr, new Variable($refVarName)),
998+
$refType,
999+
$refNativeType,
1000+
);
1001+
}
1002+
1003+
return $scope;
1004+
}
1005+
9391006
/**
9401007
* @param list<ArrayDimFetch> $dimFetchStack
9411008
* @param non-empty-list<array{Type|null, ArrayDimFetch}> $offsetTypes

src/Analyser/MutatingScope.php

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2584,7 +2584,7 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
25842584
$scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty);
25852585
}
25862586

2587-
foreach ($scope->expressionTypes as $expressionType) {
2587+
foreach ($scope->expressionTypes as $exprString => $expressionType) {
25882588
if (!$expressionType->getExpr() instanceof IntertwinedVariableByReferenceWithExpr) {
25892589
continue;
25902590
}
@@ -2595,6 +2595,16 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
25952595
continue;
25962596
}
25972597

2598+
$assignedExpr = $expressionType->getExpr()->getAssignedExpr();
2599+
if (
2600+
$assignedExpr instanceof Expr\ArrayDimFetch
2601+
&& !$this->isDimFetchPathReachable($scope, $assignedExpr)
2602+
) {
2603+
unset($scope->expressionTypes[$exprString]);
2604+
unset($scope->nativeExpressionTypes[$exprString]);
2605+
continue;
2606+
}
2607+
25982608
$has = $scope->hasExpressionType($expressionType->getExpr()->getExpr());
25992609
if (
26002610
$expressionType->getExpr()->getExpr() instanceof Variable
@@ -2613,18 +2623,41 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp
26132623
array_merge($intertwinedPropagatedFrom, [$variableName]),
26142624
);
26152625
} else {
2626+
$targetRootVar = $this->getIntertwinedRefRootVariableName($expressionType->getExpr()->getExpr());
2627+
if ($targetRootVar !== null && in_array($targetRootVar, $intertwinedPropagatedFrom, true)) {
2628+
continue;
2629+
}
26162630
$scope = $scope->assignExpression(
26172631
$expressionType->getExpr()->getExpr(),
26182632
$scope->getType($expressionType->getExpr()->getAssignedExpr()),
26192633
$scope->getNativeType($expressionType->getExpr()->getAssignedExpr()),
26202634
);
26212635
}
2622-
26232636
}
26242637

26252638
return $scope;
26262639
}
26272640

2641+
private function isDimFetchPathReachable(self $scope, Expr\ArrayDimFetch $dimFetch): bool
2642+
{
2643+
if ($dimFetch->dim === null) {
2644+
return false;
2645+
}
2646+
2647+
if (!$dimFetch->var instanceof Expr\ArrayDimFetch) {
2648+
return true;
2649+
}
2650+
2651+
$varType = $scope->getType($dimFetch->var);
2652+
$dimType = $scope->getType($dimFetch->dim);
2653+
2654+
if (!$varType->hasOffsetValueType($dimType)->yes()) {
2655+
return false;
2656+
}
2657+
2658+
return $this->isDimFetchPathReachable($scope, $dimFetch->var);
2659+
}
2660+
26282661
private function unsetExpression(Expr $expr): self
26292662
{
26302663
$scope = $this;
@@ -2835,12 +2868,6 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
28352868

28362869
foreach ($expressionTypes as $exprString => $exprTypeHolder) {
28372870
$exprExpr = $exprTypeHolder->getExpr();
2838-
if (
2839-
$exprExpr instanceof IntertwinedVariableByReferenceWithExpr
2840-
&& $exprExpr->isVariableToVariableReference()
2841-
) {
2842-
continue;
2843-
}
28442871
if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, $requireMoreCharacters, $invalidatingClass)) {
28452872
continue;
28462873
}
@@ -2908,8 +2935,32 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require
29082935
);
29092936
}
29102937

2938+
private function getIntertwinedRefRootVariableName(Expr $expr): ?string
2939+
{
2940+
if ($expr instanceof Variable && is_string($expr->name)) {
2941+
return $expr->name;
2942+
}
2943+
if ($expr instanceof Expr\ArrayDimFetch) {
2944+
return $this->getIntertwinedRefRootVariableName($expr->var);
2945+
}
2946+
return null;
2947+
}
2948+
29112949
private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, string $exprString, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): bool
29122950
{
2951+
if (
2952+
$expr instanceof IntertwinedVariableByReferenceWithExpr
2953+
&& $exprToInvalidate instanceof Variable
2954+
&& is_string($exprToInvalidate->name)
2955+
&& (
2956+
$expr->getVariableName() === $exprToInvalidate->name
2957+
|| $this->getIntertwinedRefRootVariableName($expr->getExpr()) === $exprToInvalidate->name
2958+
|| $this->getIntertwinedRefRootVariableName($expr->getAssignedExpr()) === $exprToInvalidate->name
2959+
)
2960+
) {
2961+
return false;
2962+
}
2963+
29132964
if ($requireMoreCharacters && $exprStringToInvalidate === $exprString) {
29142965
return false;
29152966
}

src/Node/Expr/IntertwinedVariableByReferenceWithExpr.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
use Override;
66
use PhpParser\Node\Expr;
7-
use PhpParser\Node\Expr\Variable;
87
use PHPStan\Node\VirtualNode;
9-
use function is_string;
108

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

34-
public function isVariableToVariableReference(): bool
35-
{
36-
return $this->expr instanceof Variable
37-
&& is_string($this->expr->name)
38-
&& $this->assignedExpr instanceof Variable
39-
&& is_string($this->assignedExpr->name);
40-
}
41-
4232
#[Override]
4333
public function getType(): string
4434
{

src/Type/Accessory/HasOffsetValueType.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ public function isOffsetAccessLegal(): TrinaryLogic
156156

157157
public function hasOffsetValueType(Type $offsetType): TrinaryLogic
158158
{
159-
if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) {
159+
$arrayKeyType = $offsetType->toArrayKey();
160+
if ($arrayKeyType->isConstantScalarValue()->yes() && $arrayKeyType->equals($this->offsetType)) {
160161
return TrinaryLogic::createYes();
161162
}
162163

@@ -165,7 +166,8 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic
165166

166167
public function getOffsetValueType(Type $offsetType): Type
167168
{
168-
if ($offsetType->isConstantScalarValue()->yes() && $offsetType->equals($this->offsetType)) {
169+
$arrayKeyType = $offsetType->toArrayKey();
170+
if ($arrayKeyType->isConstantScalarValue()->yes() && $arrayKeyType->equals($this->offsetType)) {
169171
return $this->valueType;
170172
}
171173

0 commit comments

Comments
 (0)