Skip to content

Commit 12db8d4

Browse files
committed
Fix property type after unset
1 parent 61c9177 commit 12db8d4

33 files changed

+365
-4
lines changed

src/Analyser/MutatingScope.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030
use PhpParser\NodeFinder;
3131
use PHPStan\Node\ExecutionEndNode;
3232
use PHPStan\Node\Expr\AlwaysRememberedExpr;
33+
use PHPStan\Node\Expr\ExistingArrayDimFetch;
3334
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
3435
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
3536
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
3637
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
3738
use PHPStan\Node\Expr\PropertyInitializationExpr;
39+
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
3840
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
3941
use PHPStan\Node\Expr\TypeExpr;
4042
use PHPStan\Node\Expr\UnsetOffsetExpr;
@@ -640,6 +642,9 @@ public function getType(Expr $node): Type
640642
if ($node instanceof GetOffsetValueTypeExpr) {
641643
return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim()));
642644
}
645+
if ($node instanceof ExistingArrayDimFetch) {
646+
return $this->getType(new Expr\ArrayDimFetch($node->getVar(), $node->getDim()));
647+
}
643648
if ($node instanceof UnsetOffsetExpr) {
644649
return $this->getType($node->getVar())->unsetOffset($this->getType($node->getDim()));
645650
}
@@ -649,6 +654,12 @@ public function getType(Expr $node): Type
649654
$this->getType($node->getValue()),
650655
);
651656
}
657+
if ($node instanceof SetExistingOffsetValueTypeExpr) {
658+
return $this->getType($node->getVar())->setExistingOffsetValueType(
659+
$this->getType($node->getDim()),
660+
$this->getType($node->getValue()),
661+
);
662+
}
652663
if ($node instanceof TypeExpr) {
653664
return $node->getExprType();
654665
}

src/Analyser/NodeScopeResolver.php

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
use PhpParser\Node\Stmt\TryCatch;
5151
use PhpParser\Node\Stmt\Unset_;
5252
use PhpParser\Node\Stmt\While_;
53+
use PhpParser\NodeTraverser;
54+
use PhpParser\NodeVisitor\CloningVisitor;
55+
use PhpParser\NodeVisitorAbstract;
5356
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
5457
use PHPStan\BetterReflection\Reflection\ReflectionEnum;
5558
use PHPStan\BetterReflection\Reflector\Reflector;
@@ -72,11 +75,13 @@
7275
use PHPStan\Node\DoWhileLoopConditionNode;
7376
use PHPStan\Node\ExecutionEndNode;
7477
use PHPStan\Node\Expr\AlwaysRememberedExpr;
78+
use PHPStan\Node\Expr\ExistingArrayDimFetch;
7579
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
7680
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
7781
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
7882
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
7983
use PHPStan\Node\Expr\PropertyInitializationExpr;
84+
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
8085
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
8186
use PHPStan\Node\Expr\UnsetOffsetExpr;
8287
use PHPStan\Node\FinallyExitPointsNode;
@@ -1518,10 +1523,35 @@ private function processStmtNode(
15181523
$hasYield = $hasYield || $exprResult->hasYield();
15191524
$throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
15201525
if ($var instanceof ArrayDimFetch && $var->dim !== null) {
1526+
$cloningTraverser = new NodeTraverser();
1527+
$cloningTraverser->addVisitor(new CloningVisitor());
1528+
1529+
/** @var Expr $clonedVar */
1530+
[$clonedVar] = $cloningTraverser->traverse([$var->var]);
1531+
1532+
$traverser = new NodeTraverser();
1533+
$traverser->addVisitor(new class () extends NodeVisitorAbstract {
1534+
1535+
/**
1536+
* @return ExistingArrayDimFetch|null
1537+
*/
1538+
public function leaveNode(Node $node)
1539+
{
1540+
if (!$node instanceof ArrayDimFetch || $node->dim === null) {
1541+
return null;
1542+
}
1543+
1544+
return new ExistingArrayDimFetch($node->var, $node->dim);
1545+
}
1546+
1547+
});
1548+
1549+
/** @var Expr $clonedVar */
1550+
[$clonedVar] = $traverser->traverse([$clonedVar]);
15211551
$scope = $this->processAssignVar(
15221552
$scope,
15231553
$stmt,
1524-
$var->var,
1554+
$clonedVar,
15251555
new UnsetOffsetExpr($var->var, $var->dim),
15261556
static function (Node $node, Scope $scope) use ($nodeCallback): void {
15271557
if (!$node instanceof PropertyAssignNode) {
@@ -4209,6 +4239,72 @@ static function (): void {
42094239
$hasYield = $hasYield || $result->hasYield();
42104240
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
42114241
}
4242+
} elseif ($var instanceof ExistingArrayDimFetch) {
4243+
$dimFetchStack = [];
4244+
$assignedPropertyExpr = $assignedExpr;
4245+
while ($var instanceof ExistingArrayDimFetch) {
4246+
$varForSetOffsetValue = $var->getVar();
4247+
if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
4248+
$varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
4249+
}
4250+
$assignedPropertyExpr = new SetExistingOffsetValueTypeExpr(
4251+
$varForSetOffsetValue,
4252+
$var->getDim(),
4253+
$assignedPropertyExpr,
4254+
);
4255+
$dimFetchStack[] = $var;
4256+
$var = $var->getVar();
4257+
}
4258+
4259+
$offsetTypes = [];
4260+
$offsetNativeTypes = [];
4261+
foreach (array_reverse($dimFetchStack) as $dimFetch) {
4262+
$dimExpr = $dimFetch->getDim();
4263+
$offsetTypes[] = $scope->getType($dimExpr);
4264+
$offsetNativeTypes[] = $scope->getNativeType($dimExpr);
4265+
}
4266+
4267+
$valueToWrite = $scope->getType($assignedExpr);
4268+
$nativeValueToWrite = $scope->getNativeType($assignedExpr);
4269+
$varType = $scope->getType($var);
4270+
$varNativeType = $scope->getNativeType($var);
4271+
4272+
$offsetValueType = $varType;
4273+
$offsetNativeValueType = $varNativeType;
4274+
$offsetValueTypeStack = [$offsetValueType];
4275+
$offsetValueNativeTypeStack = [$offsetNativeValueType];
4276+
foreach (array_slice($offsetTypes, 0, -1) as $offsetType) {
4277+
$offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
4278+
$offsetValueTypeStack[] = $offsetValueType;
4279+
}
4280+
foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) {
4281+
$offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType);
4282+
$offsetValueNativeTypeStack[] = $offsetNativeValueType;
4283+
}
4284+
4285+
foreach (array_reverse($offsetTypes) as $offsetType) {
4286+
/** @var Type $offsetValueType */
4287+
$offsetValueType = array_pop($offsetValueTypeStack);
4288+
$valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite);
4289+
}
4290+
foreach (array_reverse($offsetNativeTypes) as $offsetNativeType) {
4291+
/** @var Type $offsetNativeValueType */
4292+
$offsetNativeValueType = array_pop($offsetValueNativeTypeStack);
4293+
$nativeValueToWrite = $offsetNativeValueType->setExistingOffsetValueType($offsetNativeType, $nativeValueToWrite);
4294+
}
4295+
4296+
if ($var instanceof Variable && is_string($var->name)) {
4297+
$scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite);
4298+
} else {
4299+
if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
4300+
$nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
4301+
}
4302+
$scope = $scope->assignExpression(
4303+
$var,
4304+
$valueToWrite,
4305+
$nativeValueToWrite,
4306+
);
4307+
}
42124308
}
42134309

42144310
return new ExpressionResult($scope, $hasYield, $throwPoints);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node\Expr;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Node\VirtualNode;
7+
8+
class ExistingArrayDimFetch extends Expr implements VirtualNode
9+
{
10+
11+
public function __construct(private Expr $var, private Expr $dim)
12+
{
13+
parent::__construct([]);
14+
}
15+
16+
public function getVar(): Expr
17+
{
18+
return $this->var;
19+
}
20+
21+
public function getDim(): Expr
22+
{
23+
return $this->dim;
24+
}
25+
26+
public function getType(): string
27+
{
28+
return 'PHPStan_Node_ExistingArrayDimFetch';
29+
}
30+
31+
/**
32+
* @return string[]
33+
*/
34+
public function getSubNodeNames(): array
35+
{
36+
return [];
37+
}
38+
39+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node\Expr;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Node\VirtualNode;
7+
8+
class SetExistingOffsetValueTypeExpr extends Expr implements VirtualNode
9+
{
10+
11+
public function __construct(private Expr $var, private Expr $dim, private Expr $value)
12+
{
13+
parent::__construct([]);
14+
}
15+
16+
public function getVar(): Expr
17+
{
18+
return $this->var;
19+
}
20+
21+
public function getDim(): Expr
22+
{
23+
return $this->dim;
24+
}
25+
26+
public function getValue(): Expr
27+
{
28+
return $this->value;
29+
}
30+
31+
public function getType(): string
32+
{
33+
return 'PHPStan_Node_SetExistingOffsetValueTypeExpr';
34+
}
35+
36+
/**
37+
* @return string[]
38+
*/
39+
public function getSubNodeNames(): array
40+
{
41+
return [];
42+
}
43+
44+
}

src/Node/Printer/Printer.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
use PhpParser\PrettyPrinter\Standard;
66
use PHPStan\Node\Expr\AlwaysRememberedExpr;
7+
use PHPStan\Node\Expr\ExistingArrayDimFetch;
78
use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
89
use PHPStan\Node\Expr\GetIterableValueTypeExpr;
910
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
1011
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
1112
use PHPStan\Node\Expr\PropertyInitializationExpr;
13+
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
1214
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
1315
use PHPStan\Node\Expr\TypeExpr;
1416
use PHPStan\Node\Expr\UnsetOffsetExpr;
@@ -49,6 +51,11 @@ protected function pPHPStan_Node_GetIterableKeyTypeExpr(GetIterableKeyTypeExpr $
4951
return sprintf('__phpstanGetIterableKeyType(%s)', $this->p($expr->getExpr()));
5052
}
5153

54+
protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore
55+
{
56+
return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()));
57+
}
58+
5259
protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore
5360
{
5461
return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch()));
@@ -59,6 +66,11 @@ protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $
5966
return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue()));
6067
}
6168

69+
protected function pPHPStan_Node_SetExistingOffsetValueTypeExpr(SetExistingOffsetValueTypeExpr $expr): string // phpcs:ignore
70+
{
71+
return sprintf('__phpstanSetExistingOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()), $this->p($expr->getValue()));
72+
}
73+
6274
protected function pPHPStan_Node_AlwaysRememberedExpr(AlwaysRememberedExpr $expr): string // phpcs:ignore
6375
{
6476
return sprintf('__phpstanRembered(%s)', $this->p($expr->getExpr()));

src/Type/Accessory/AccessoryArrayListType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
161161
return new ErrorType();
162162
}
163163

164+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
165+
{
166+
if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) {
167+
return $this;
168+
}
169+
170+
return new ErrorType();
171+
}
172+
164173
public function unsetOffset(Type $offsetType): Type
165174
{
166175
if ($this->hasOffsetValueType($offsetType)->no()) {

src/Type/Accessory/AccessoryLiteralStringType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
151151
return $this;
152152
}
153153

154+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
155+
{
156+
return $this;
157+
}
158+
154159
public function unsetOffset(Type $offsetType): Type
155160
{
156161
return new ErrorType();

src/Type/Accessory/AccessoryNonEmptyStringType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
157157
return $this;
158158
}
159159

160+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
161+
{
162+
return $this;
163+
}
164+
160165
public function unsetOffset(Type $offsetType): Type
161166
{
162167
return new ErrorType();

src/Type/Accessory/AccessoryNonFalsyStringType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
154154
return $this;
155155
}
156156

157+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
158+
{
159+
return $this;
160+
}
161+
157162
public function unsetOffset(Type $offsetType): Type
158163
{
159164
return new ErrorType();

src/Type/Accessory/AccessoryNumericStringType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
156156
return $this;
157157
}
158158

159+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
160+
{
161+
return $this;
162+
}
163+
159164
public function unsetOffset(Type $offsetType): Type
160165
{
161166
return new ErrorType();

src/Type/Accessory/HasOffsetType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
157157
return $this;
158158
}
159159

160+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
161+
{
162+
return $this;
163+
}
164+
160165
public function unsetOffset(Type $offsetType): Type
161166
{
162167
if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) {

src/Type/Accessory/HasOffsetValueType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
181181
return new self($offsetType, $valueType);
182182
}
183183

184+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
185+
{
186+
return new self($this->offsetType, $valueType);
187+
}
188+
184189
public function unsetOffset(Type $offsetType): Type
185190
{
186191
if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) {

src/Type/Accessory/NonEmptyArrayType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
154154
return $this;
155155
}
156156

157+
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
158+
{
159+
return $this;
160+
}
161+
157162
public function unsetOffset(Type $offsetType): Type
158163
{
159164
return new ErrorType();

0 commit comments

Comments
 (0)