diff --git a/src/Rules/Functions/UnsetRule.php b/src/Rules/Functions/UnsetRule.php new file mode 100644 index 0000000000..cbaef6539b --- /dev/null +++ b/src/Rules/Functions/UnsetRule.php @@ -0,0 +1,69 @@ + + */ +class UnsetRule implements \PHPStan\Rules\Rule +{ + + public function getNodeType(): string + { + return Node\Stmt\Unset_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $functionArguments = $node->vars; + $messages = []; + + foreach ($functionArguments as $argument) { + $message = $this->canBeUnset($argument, $scope); + + if (!$message) { + continue; + } + + $messages[] = $message; + } + + return $messages; + } + + private function canBeUnset(Node $node, Scope $scope): ?string + { + if ($node instanceof Node\Expr\Variable && is_string($node->name)) { + $scopeHasVariable = $scope->hasVariableType($node->name); + + if ($scopeHasVariable->no()) { + return RuleErrorBuilder::message( + sprintf('Call to function unset() contains undefined variable $%s.', $node->name) + )->line($node->getLine())->build()->getMessage(); + } + } elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { + $type = $scope->getType($node->var); + $dimType = $scope->getType($node->dim); + + $isOffsetAccessible = !$type->isOffsetAccessible()->no() && $type->getIterableKeyType()->isSuperTypeOf($dimType)->no(); + + if ($isOffsetAccessible || $type->hasOffsetValueType($dimType)->no()) { + return RuleErrorBuilder::message( + sprintf( + 'Cannot unset offset %s on %s.', + $dimType->describe(VerbosityLevel::value()), + $type->describe(VerbosityLevel::value()) + ) + )->line($node->getLine())->build()->getMessage(); + } + } + + return null; + } + +} diff --git a/tests/PHPStan/Rules/Functions/UnsetRuleTest.php b/tests/PHPStan/Rules/Functions/UnsetRuleTest.php new file mode 100644 index 0000000000..407ca72a59 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/UnsetRuleTest.php @@ -0,0 +1,47 @@ + + */ +class UnsetRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + return new UnsetRule(); + } + + public function testUnsetRule(): void + { + require_once __DIR__ . '/data/unset.php'; + $this->analyse([__DIR__ . '/data/unset.php'], [ + [ + 'Call to function unset() contains undefined variable $notSetVariable.', + 6, + ], + [ + 'Cannot unset offset \'a\' on 3.', + 10, + ], + [ + 'Cannot unset offset \'b\' on 1.', + 14, + ], + [ + 'Cannot unset offset \'c\' on 1.', + 18, + ], + [ + 'Cannot unset offset \'b\' on 1.', + 18, + ], + [ + 'Cannot unset offset \'string\' on iterable.', + 31, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/unset.php b/tests/PHPStan/Rules/Functions/data/unset.php new file mode 100644 index 0000000000..501555b1c0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/unset.php @@ -0,0 +1,32 @@ + 1]; + + unset($singleDimArray['a']['b']); + + $multiDimArray = ['a' => ['b' => 1]]; + + unset($multiDimArray['a']['b']['c'], $scalar, $singleDimArray['a']['b']); + +} + +/** @param iterable $iterable */ +function unsetOnMaybeIterable(iterable $iterable) +{ + unset($iterable['string']); +} + +/** @param iterable $iterable */ +function unsetOnYesIterable(iterable $iterable) +{ + unset($iterable['string']); +}