Skip to content

Commit

Permalink
Verify property type after unset
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 3, 2024
1 parent f6cab89 commit aeadbe2
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 2 deletions.
4 changes: 4 additions & 0 deletions src/Analyser/MutatingScope.php
Expand Up @@ -37,6 +37,7 @@
use PHPStan\Node\Expr\PropertyInitializationExpr;
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Node\Expr\UnsetOffsetExpr;
use PHPStan\Node\IssetExpr;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Parser\ArrayMapArgVisitor;
Expand Down Expand Up @@ -639,6 +640,9 @@ public function getType(Expr $node): Type
if ($node instanceof GetOffsetValueTypeExpr) {
return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim()));
}
if ($node instanceof UnsetOffsetExpr) {
return $this->getType($node->getVar())->unsetOffset($this->getType($node->getDim()));
}
if ($node instanceof SetOffsetValueTypeExpr) {
return $this->getType($node->getVar())->setOffsetValueType(
$node->getDim() !== null ? $this->getType($node->getDim()) : null,
Expand Down
28 changes: 26 additions & 2 deletions src/Analyser/NodeScopeResolver.php
Expand Up @@ -78,6 +78,7 @@
use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
use PHPStan\Node\Expr\PropertyInitializationExpr;
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
use PHPStan\Node\Expr\UnsetOffsetExpr;
use PHPStan\Node\FinallyExitPointsNode;
use PHPStan\Node\FunctionCallableNode;
use PHPStan\Node\FunctionReturnStatementsNode;
Expand Down Expand Up @@ -1510,9 +1511,32 @@ private function processStmtNode(
$throwPoints = [];
foreach ($stmt->vars as $var) {
$scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
$scope = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope();
$exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep());
$scope = $exprResult->getScope();
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
$scope = $scope->unsetExpression($var);
$hasYield = $hasYield || $exprResult->hasYield();
$throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
if ($var instanceof ArrayDimFetch && $var->dim !== null) {
$scope = $this->processAssignVar(
$scope,
$stmt,
$var->var,
new UnsetOffsetExpr($var->var, $var->dim),
static function (Node $node, Scope $scope) use ($nodeCallback): void {
if (!$node instanceof PropertyAssignNode) {
return;
}

$nodeCallback($node, $scope);
},
ExpressionContext::createDeep(),
static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []),
false,
)->getScope();
} else {
$scope = $scope->invalidateExpression($var);
}

}
} elseif ($stmt instanceof Node\Stmt\Use_) {
$hasYield = false;
Expand Down
39 changes: 39 additions & 0 deletions src/Node/Expr/UnsetOffsetExpr.php
@@ -0,0 +1,39 @@
<?php declare(strict_types = 1);

namespace PHPStan\Node\Expr;

use PhpParser\Node\Expr;
use PHPStan\Node\VirtualNode;

class UnsetOffsetExpr extends Expr implements VirtualNode
{

public function __construct(private Expr $var, private Expr $dim)
{
parent::__construct([]);
}

public function getVar(): Expr
{
return $this->var;
}

public function getDim(): Expr
{
return $this->dim;
}

public function getType(): string
{
return 'PHPStan_Node_UnsetOffsetExpr';
}

/**
* @return string[]
*/
public function getSubNodeNames(): array
{
return [];
}

}
6 changes: 6 additions & 0 deletions src/Node/Printer/Printer.php
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Node\Expr\PropertyInitializationExpr;
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Node\Expr\UnsetOffsetExpr;
use PHPStan\Node\IssetExpr;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
Expand All @@ -33,6 +34,11 @@ protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $
return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()));
}

protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore
{
return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim()));
}

protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore
{
return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr()));
Expand Down
Expand Up @@ -586,4 +586,26 @@ public function testBug7087(): void
$this->analyse([__DIR__ . '/data/bug-7087.php'], []);
}

public function testUnset(): void
{
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/data/property-type-after-unset.php'], [
[
'Property PropertyTypeAfterUnset\Foo::$nonEmpty (non-empty-array<int>) does not accept array<int>.',
19,
'array<int> might be empty.',
],
[
'Property PropertyTypeAfterUnset\Foo::$listProp (list<int>) does not accept array<int<0, max>, int>.',
20,
'array<int<0, max>, int> might not be a list.',
],
[
'Property PropertyTypeAfterUnset\Foo::$nestedListProp (array<list<int>>) does not accept non-empty-array<array<int<0, max>, int>>.',
21,
'array<int<0, max>, int> might not be a list.',
],
]);
}

}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Properties/data/property-type-after-unset.php
@@ -0,0 +1,24 @@
<?php

namespace PropertyTypeAfterUnset;

class Foo
{

/** @var non-empty-array<int> */
private $nonEmpty;

/** @var list<int> */
private $listProp;

/** @var array<list<int>> */
private $nestedListProp;

public function doFoo(int $i, int $j)
{
unset($this->nonEmpty[$i]);
unset($this->listProp[$i]);
unset($this->nestedListProp[$i][$j]);
}

}

0 comments on commit aeadbe2

Please sign in to comment.