Skip to content

Commit aeadbe2

Browse files
committed
Verify property type after unset
1 parent f6cab89 commit aeadbe2

File tree

6 files changed

+121
-2
lines changed

6 files changed

+121
-2
lines changed

src/Analyser/MutatingScope.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use PHPStan\Node\Expr\PropertyInitializationExpr;
3838
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
3939
use PHPStan\Node\Expr\TypeExpr;
40+
use PHPStan\Node\Expr\UnsetOffsetExpr;
4041
use PHPStan\Node\IssetExpr;
4142
use PHPStan\Node\Printer\ExprPrinter;
4243
use PHPStan\Parser\ArrayMapArgVisitor;
@@ -639,6 +640,9 @@ public function getType(Expr $node): Type
639640
if ($node instanceof GetOffsetValueTypeExpr) {
640641
return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim()));
641642
}
643+
if ($node instanceof UnsetOffsetExpr) {
644+
return $this->getType($node->getVar())->unsetOffset($this->getType($node->getDim()));
645+
}
642646
if ($node instanceof SetOffsetValueTypeExpr) {
643647
return $this->getType($node->getVar())->setOffsetValueType(
644648
$node->getDim() !== null ? $this->getType($node->getDim()) : null,

src/Analyser/NodeScopeResolver.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
7979
use PHPStan\Node\Expr\PropertyInitializationExpr;
8080
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
81+
use PHPStan\Node\Expr\UnsetOffsetExpr;
8182
use PHPStan\Node\FinallyExitPointsNode;
8283
use PHPStan\Node\FunctionCallableNode;
8384
use PHPStan\Node\FunctionReturnStatementsNode;
@@ -1510,9 +1511,32 @@ private function processStmtNode(
15101511
$throwPoints = [];
15111512
foreach ($stmt->vars as $var) {
15121513
$scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
1513-
$scope = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope();
1514+
$exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep());
1515+
$scope = $exprResult->getScope();
15141516
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
1515-
$scope = $scope->unsetExpression($var);
1517+
$hasYield = $hasYield || $exprResult->hasYield();
1518+
$throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
1519+
if ($var instanceof ArrayDimFetch && $var->dim !== null) {
1520+
$scope = $this->processAssignVar(
1521+
$scope,
1522+
$stmt,
1523+
$var->var,
1524+
new UnsetOffsetExpr($var->var, $var->dim),
1525+
static function (Node $node, Scope $scope) use ($nodeCallback): void {
1526+
if (!$node instanceof PropertyAssignNode) {
1527+
return;
1528+
}
1529+
1530+
$nodeCallback($node, $scope);
1531+
},
1532+
ExpressionContext::createDeep(),
1533+
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []),
1534+
false,
1535+
)->getScope();
1536+
} else {
1537+
$scope = $scope->invalidateExpression($var);
1538+
}
1539+
15161540
}
15171541
} elseif ($stmt instanceof Node\Stmt\Use_) {
15181542
$hasYield = false;

src/Node/Expr/UnsetOffsetExpr.php

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 UnsetOffsetExpr 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_UnsetOffsetExpr';
29+
}
30+
31+
/**
32+
* @return string[]
33+
*/
34+
public function getSubNodeNames(): array
35+
{
36+
return [];
37+
}
38+
39+
}

src/Node/Printer/Printer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Node\Expr\PropertyInitializationExpr;
1212
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
1313
use PHPStan\Node\Expr\TypeExpr;
14+
use PHPStan\Node\Expr\UnsetOffsetExpr;
1415
use PHPStan\Node\IssetExpr;
1516
use PHPStan\Type\VerbosityLevel;
1617
use function sprintf;
@@ -33,6 +34,11 @@ protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $
3334
return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()));
3435
}
3536

37+
protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore
38+
{
39+
return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()));
40+
}
41+
3642
protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore
3743
{
3844
return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr()));

tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,4 +586,26 @@ public function testBug7087(): void
586586
$this->analyse([__DIR__ . '/data/bug-7087.php'], []);
587587
}
588588

589+
public function testUnset(): void
590+
{
591+
$this->checkExplicitMixed = true;
592+
$this->analyse([__DIR__ . '/data/property-type-after-unset.php'], [
593+
[
594+
'Property PropertyTypeAfterUnset\Foo::$nonEmpty (non-empty-array<int>) does not accept array<int>.',
595+
19,
596+
'array<int> might be empty.',
597+
],
598+
[
599+
'Property PropertyTypeAfterUnset\Foo::$listProp (list<int>) does not accept array<int<0, max>, int>.',
600+
20,
601+
'array<int<0, max>, int> might not be a list.',
602+
],
603+
[
604+
'Property PropertyTypeAfterUnset\Foo::$nestedListProp (array<list<int>>) does not accept non-empty-array<array<int<0, max>, int>>.',
605+
21,
606+
'array<int<0, max>, int> might not be a list.',
607+
],
608+
]);
609+
}
610+
589611
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace PropertyTypeAfterUnset;
4+
5+
class Foo
6+
{
7+
8+
/** @var non-empty-array<int> */
9+
private $nonEmpty;
10+
11+
/** @var list<int> */
12+
private $listProp;
13+
14+
/** @var array<list<int>> */
15+
private $nestedListProp;
16+
17+
public function doFoo(int $i, int $j)
18+
{
19+
unset($this->nonEmpty[$i]);
20+
unset($this->listProp[$i]);
21+
unset($this->nestedListProp[$i][$j]);
22+
}
23+
24+
}

0 commit comments

Comments
 (0)