From aeadbe28e0b0a05a3a48723ac310cf6c0d852711 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 3 Feb 2024 15:06:06 +0100 Subject: [PATCH] Verify property type after unset --- src/Analyser/MutatingScope.php | 4 ++ src/Analyser/NodeScopeResolver.php | 28 ++++++++++++- src/Node/Expr/UnsetOffsetExpr.php | 39 +++++++++++++++++++ src/Node/Printer/Printer.php | 6 +++ .../TypesAssignedToPropertiesRuleTest.php | 22 +++++++++++ .../data/property-type-after-unset.php | 24 ++++++++++++ 6 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/Node/Expr/UnsetOffsetExpr.php create mode 100644 tests/PHPStan/Rules/Properties/data/property-type-after-unset.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 2c56f2c1b6..a83ee949d8 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -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; @@ -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, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5edb9dd7bd..4708876b7f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -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; @@ -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; diff --git a/src/Node/Expr/UnsetOffsetExpr.php b/src/Node/Expr/UnsetOffsetExpr.php new file mode 100644 index 0000000000..55c81eef92 --- /dev/null +++ b/src/Node/Expr/UnsetOffsetExpr.php @@ -0,0 +1,39 @@ +var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_UnsetOffsetExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 5812d16927..9183a4c886 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -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; @@ -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())); diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 7374c31b0f..2429f670ed 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -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) does not accept array.', + 19, + 'array might be empty.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$listProp (list) does not accept array, int>.', + 20, + 'array, int> might not be a list.', + ], + [ + 'Property PropertyTypeAfterUnset\Foo::$nestedListProp (array>) does not accept non-empty-array, int>>.', + 21, + 'array, int> might not be a list.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php new file mode 100644 index 0000000000..954fe213e8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php @@ -0,0 +1,24 @@ + */ + private $nonEmpty; + + /** @var list */ + private $listProp; + + /** @var array> */ + private $nestedListProp; + + public function doFoo(int $i, int $j) + { + unset($this->nonEmpty[$i]); + unset($this->listProp[$i]); + unset($this->nestedListProp[$i][$j]); + } + +}