diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 58c962b05a0..514914e23ea 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1085,19 +1085,29 @@ private function resolveType(string $exprString, Expr $node): Type public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool { // mirrored in PHPStan\Rules\IssetCheck - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $this->hasVariableType($expr->name); + if ($expr instanceof Node\Expr\Variable) { + $variableScopes = VariableNameResolver::resolveNamesWithScopes($this, $expr); + if ($variableScopes === null) { + return null; + } + + $hasVariable = TrinaryLogic::lazyExtremeIdentity( + $variableScopes, + static fn (array $variableScope): TrinaryLogic => $variableScope[1]->hasVariableType($variableScope[0]), + ); if ($hasVariable->maybe()) { return null; } if ($result === null) { if ($hasVariable->yes()) { - if ($expr->name === '_SESSION') { - return null; + foreach ($variableScopes as [$variableName]) { + if ($variableName === '_SESSION') { + return null; + } } - return $typeCallback($this->getVariableType($expr->name)); + return $typeCallback($this->getType($expr)); } return false; @@ -1209,8 +1219,16 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n private function issetCheckUndefined(Expr $expr): ?bool { - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $this->hasVariableType($expr->name); + if ($expr instanceof Node\Expr\Variable) { + $variableScopes = VariableNameResolver::resolveNamesWithScopes($this, $expr); + if ($variableScopes === null) { + return null; + } + + $hasVariable = TrinaryLogic::lazyExtremeIdentity( + $variableScopes, + static fn (array $variableScope): TrinaryLogic => $variableScope[1]->hasVariableType($variableScope[0]), + ); if (!$hasVariable->no()) { return null; } diff --git a/src/Analyser/VariableNameResolver.php b/src/Analyser/VariableNameResolver.php new file mode 100644 index 00000000000..128439bc0c8 --- /dev/null +++ b/src/Analyser/VariableNameResolver.php @@ -0,0 +1,47 @@ +|null + */ + public static function resolveNamesWithScopes(Scope $scope, Variable $variable): ?array + { + if (is_string($variable->name)) { + return [[$variable->name, $scope]]; + } + + $namesWithScopes = []; + foreach ($scope->getType($variable->name)->getConstantStrings() as $constantString) { + $name = $constantString->getValue(); + $namesWithScopes[] = [ + $name, + $scope->filterByTruthyValue(new Identical($variable->name, new String_($name))), + ]; + } + + if ($namesWithScopes === []) { + return null; + } + + return $namesWithScopes; + } + +} diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index b874dff3183..f7485d02949 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -2,6 +2,7 @@ namespace PHPStan\Command; +use DateTimeImmutable; use OndraM\CiDetector\CiDetector; use Override; use PHPStan\Analyser\InternalError; @@ -35,6 +36,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Throwable; @@ -63,6 +65,7 @@ use function stream_get_contents; use function strlen; use function substr; +use function time; use const PATHINFO_BASENAME; use const PATHINFO_EXTENSION; @@ -140,6 +143,34 @@ protected function initialize(InputInterface $input, OutputInterface $output): v #[Override] protected function execute(InputInterface $input, OutputInterface $output): int { + if ($output instanceof ConsoleOutputInterface) { + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln(''); + $errorOutput->writeln("⚠️ You're running an old version of PHPStan.️"); + $errorOutput->writeln(''); + $errorOutput->writeln('The last release in the 1.12.x series with new features'); + + $lastRelease = new DateTimeImmutable('2025-07-17 00:00:00'); + $daysSince = (time() - $lastRelease->getTimestamp()) / 60 / 60 / 24; + $errorOutput->writeln('and bugfixes was released on July 17th 2025,'); + $errorOutput->writeln(sprintf('that\'s %d days ago.', (int) $daysSince)); + $errorOutput->writeln(''); + + $errorOutput->writeln('Since then more than 65 new PHPStan versions were released'); + $errorOutput->writeln('with hundreds of new features, bugfixes, and other'); + $errorOutput->writeln('quality of life improvements.'); + $errorOutput->writeln(''); + + $errorOutput->writeln("To learn about what you're missing out on, check out"); + $errorOutput->writeln('this blog with articles about the latest major releases:'); + $errorOutput->writeln('https://phpstan.org/blog'); + $errorOutput->writeln(''); + + $errorOutput->writeln('Upgrade today to PHPStan 2.2 or newer by using'); + $errorOutput->writeln('"phpstan/phpstan": "^2.2" in your composer.json.'); + $errorOutput->writeln(''); + } + $paths = $input->getArgument('paths'); $memoryLimit = $input->getOption('memory-limit'); $autoloadFile = $input->getOption('autoload-file'); diff --git a/src/Rules/IssetCheck.php b/src/Rules/IssetCheck.php index 35195e7ea52..ff0f50ebfe7 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; +use PHPStan\Analyser\VariableNameResolver; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\PropertyInitializationExpr; @@ -13,7 +14,6 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function is_string; use function sprintf; use function str_starts_with; @@ -42,36 +42,23 @@ public function __construct( public function check(Expr $expr, Scope $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error = null): ?IdentifierRuleError { // mirrored in PHPStan\Analyser\MutatingScope::issetCheck() - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $scope->hasVariableType($expr->name); - if ($hasVariable->maybe()) { + if ($expr instanceof Node\Expr\Variable) { + $variableScopes = VariableNameResolver::resolveNamesWithScopes($scope, $expr); + if ($variableScopes === null) { return null; } - if ($error === null) { - if ($hasVariable->yes()) { - if ($expr->name === '_SESSION') { - return null; - } - - $type = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr) : $scope->getScopeNativeType($expr); - if (!$type instanceof NeverType) { - return $this->generateError( - $type, - sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), - $typeMessageCallback, - $identifier, - 'variable', - ); - } + $variableErrors = []; + foreach ($variableScopes as [$variableName, $variableScope]) { + $variableError = $this->checkVariable($expr, $variableName, $variableScope, $operatorDescription, $identifier, $typeMessageCallback, $error); + if ($variableError === null) { + return null; } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) - ->identifier(sprintf('%s.variable', $identifier)) - ->build(); + $variableErrors[] = $variableError; } - return $error; + return $variableErrors[0]; } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { $type = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr->var) @@ -275,20 +262,74 @@ static function (Type $type) use ($typeMessageCallback): ?string { /** * @param ErrorIdentifier $identifier + * @param callable(Type): ?string $typeMessageCallback */ - private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError + private function checkVariable( + Expr\Variable $expr, + string $variableName, + Scope $scope, + string $operatorDescription, + string $identifier, + callable $typeMessageCallback, + ?IdentifierRuleError $error, + ): ?IdentifierRuleError { - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $scope->hasVariableType($expr->name); - if (!$hasVariable->no()) { - return null; + $hasVariable = $scope->hasVariableType($variableName); + if ($hasVariable->maybe()) { + return null; + } + + if ($error === null) { + if ($hasVariable->yes()) { + if ($variableName === '_SESSION') { + return null; + } + + $type = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr) : $scope->getScopeNativeType($expr); + if (!$type instanceof NeverType) { + return $this->generateError( + $type, + sprintf('Variable $%s %s always exists and', $variableName, $operatorDescription), + $typeMessageCallback, + $identifier, + 'variable', + ); + } } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $variableName, $operatorDescription)) ->identifier(sprintf('%s.variable', $identifier)) ->build(); } + return $error; + } + + /** + * @param ErrorIdentifier $identifier + */ + private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError + { + if ($expr instanceof Node\Expr\Variable) { + $variableScopes = VariableNameResolver::resolveNamesWithScopes($scope, $expr); + if ($variableScopes === null) { + return null; + } + + $variableErrors = []; + foreach ($variableScopes as [$variableName, $variableScope]) { + if (!$variableScope->hasVariableType($variableName)->no()) { + return null; + } + + $variableErrors[] = RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $variableName, $operatorDescription)) + ->identifier(sprintf('%s.variable', $identifier)) + ->build(); + } + + return $variableErrors[0]; + } + if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { $type = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr->var) : $scope->getScopeNativeType($expr->var); $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr->dim) : $scope->getScopeNativeType($expr->dim); diff --git a/src/Rules/Properties/PropertyDescriptor.php b/src/Rules/Properties/PropertyDescriptor.php index d9d6e10c995..972fadfd7d2 100644 --- a/src/Rules/Properties/PropertyDescriptor.php +++ b/src/Rules/Properties/PropertyDescriptor.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -17,7 +17,7 @@ final class PropertyDescriptor /** * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ - public function describeProperty(PropertyReflection $property, Scope $scope, $propertyFetch): string + public function describeProperty(ExtendedPropertyReflection $property, Scope $scope, $propertyFetch): string { if ($propertyFetch instanceof Node\Expr\PropertyFetch) { $fetchedOnType = $scope->getType($propertyFetch->var); @@ -31,13 +31,13 @@ public function describeProperty(PropertyReflection $property, Scope $scope, $pr $classDescription = $property->getDeclaringClass()->getDisplayName(); } - /** @var Node\Identifier $name */ - $name = $propertyFetch->name; + // the fetch name node is not usable for dynamic accesses like $foo->{$name} + $name = $property->getName(); if (!$property->isStatic()) { - return sprintf('Property %s::$%s', $classDescription, $name->name); + return sprintf('Property %s::$%s', $classDescription, $name); } - return sprintf('Static property %s::$%s', $classDescription, $name->name); + return sprintf('Static property %s::$%s', $classDescription, $name); } } diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index b25682687b0..483e6a59caf 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -103,17 +103,22 @@ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?F return null; } - if (!$propertyFetch->name instanceof Node\Identifier) { - return null; - } - if ($propertyFetch->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); } else { $propertyHolderType = $scope->getType($propertyFetch->class); } - return $this->findStaticPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + if ($propertyFetch->name instanceof Node\Identifier) { + return $this->findStaticPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + } + + $nameTypeConstantStrings = $scope->getType($propertyFetch->name)->getConstantStrings(); + if (count($nameTypeConstantStrings) === 1) { + return $this->findStaticPropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); + } + + return null; } private function findInstancePropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index 8fbb1e5d0b0..44fdf14728d 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -3,10 +3,9 @@ namespace PHPStan\Rules\Variables; use PhpParser\Node; -use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; +use PHPStan\Analyser\VariableNameResolver; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IdentifierRuleError; @@ -14,7 +13,6 @@ use PHPStan\Rules\RuleErrorBuilder; use function array_merge; use function in_array; -use function is_string; use function sprintf; /** @@ -40,23 +38,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $errors = []; - if (is_string($node->name)) { - $variableNameScopes = [$node->name => $scope]; - } else { - $nameType = $scope->getType($node->name); - $variableNameScopes = []; - foreach ($nameType->getConstantStrings() as $constantString) { - $name = $constantString->getValue(); - $variableNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); - } + $namesWithScopes = VariableNameResolver::resolveNamesWithScopes($scope, $node); + if ($namesWithScopes === null) { + return []; } - foreach ($variableNameScopes as $name => $variableScope) { + $errors = []; + foreach ($namesWithScopes as [$name, $variableScope]) { $errors = array_merge($errors, $this->processSingleVariable( $variableScope, $node, - (string) $name, // @phpstan-ignore cast.useless + $name, )); } diff --git a/src/Rules/Variables/UnsetRule.php b/src/Rules/Variables/UnsetRule.php index d2893e17fe3..a0e1fe3ef6c 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Analyser\VariableNameResolver; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; use PHPStan\Rules\IdentifierRuleError; @@ -11,7 +12,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; -use function is_string; use function sprintf; /** @@ -39,10 +39,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($functionArguments as $argument) { - if ( - $argument instanceof Node\Expr\PropertyFetch - && $argument->name instanceof Node\Identifier - ) { + if ($argument instanceof Node\Expr\PropertyFetch) { $foundPropertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($argument, $scope); if ($foundPropertyReflection === null) { continue; @@ -113,11 +110,19 @@ public function processNode(Node $node, Scope $scope): array private function canBeUnset(Node $node, Scope $scope): ?IdentifierRuleError { - if ($node instanceof Node\Expr\Variable && is_string($node->name)) { - $hasVariable = $scope->hasVariableType($node->name); - if ($hasVariable->no()) { + if ($node instanceof Node\Expr\Variable) { + $namesWithScopes = VariableNameResolver::resolveNamesWithScopes($scope, $node); + if ($namesWithScopes === null) { + return null; + } + + foreach ($namesWithScopes as [$name, $variableScope]) { + if (!$variableScope->hasVariableType($name)->no()) { + continue; + } + return RuleErrorBuilder::message( - sprintf('Call to function unset() contains undefined variable $%s.', $node->name), + sprintf('Call to function unset() contains undefined variable $%s.', $name), ) ->line($node->getStartLine()) ->identifier('unset.variable') diff --git a/tests/PHPStan/Analyser/nsrt/variable-variables-isset.php b/tests/PHPStan/Analyser/nsrt/variable-variables-isset.php new file mode 100644 index 00000000000..734997bf337 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/variable-variables-isset.php @@ -0,0 +1,56 @@ +treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/empty-variable-variables.php'], [ + [ + 'Variable $undefinedVariable in empty() is never defined.', + 13, + ], + [ + 'Variable $nonFalsy in empty() always exists and is not falsy.', + 20, + ], + ]); + } + public function testBug970(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 7e83c53117b..a759f39ddd3 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -198,6 +198,29 @@ public function testRuleWithoutTreatPhpDocTypesAsCertain(): void ]); } + public function testVariableVariables(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/isset-variable-variables.php'], [ + [ + 'Variable $undefinedVariable in isset() is never defined.', + 26, + ], + [ + 'Variable $notNullable in isset() always exists and is not nullable.', + 33, + ], + [ + 'Property IssetVariableVariables\Foo::$notNullable (int) in isset() is not nullable.', + 48, + ], + [ + 'Static property IssetVariableVariables\Foo::$staticNotNullable (int) in isset() is not nullable.', + 57, + ], + ]); + } + public function testNativePropertyTypes(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index fc6c577822d..4c88bc05d39 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -460,6 +460,49 @@ public function testBug14459Hooked(): void ]); } + public function testBug15014(): void + { + $this->analyse([__DIR__ . '/data/bug-15014.php'], []); + } + + public function testVariableVariables(): void + { + $this->analyse([__DIR__ . '/data/null-coalesce-variable-variables.php'], [ + [ + 'Variable $undefinedVariable on left side of ?? is never defined.', + 27, + ], + [ + 'Variable $undefinedVariable on left side of ?? is never defined.', + 33, + ], + [ + 'Variable $notNullable on left side of ?? always exists and is not nullable.', + 40, + ], + [ + 'Variable $notNullable on left side of ?? always exists and is not nullable.', + 48, + ], + [ + 'Coalesce operator ?? is unnecessary because the left side is always set and the right side is null.', + 63, + ], + [ + 'Property NullCoalesceVariableVariables\Foo::$notNullable (int) on left side of ?? is not nullable.', + 69, + ], + [ + 'Static property NullCoalesceVariableVariables\Foo::$staticNotNullable (int) on left side of ?? is not nullable.', + 78, + ], + [ + 'Variable $undefinedArray on left side of ?? is never defined.', + 87, + ], + ]); + } + public function testBug4337(): void { $this->analyse([__DIR__ . '/data/bug-4337.php'], [ diff --git a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index 3bef8ab9005..c3a334d27db 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -56,6 +56,29 @@ public function testUnsetRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testVariableVariables(): void + { + $errors = [ + [ + 'Call to function unset() contains undefined variable $undefinedVariable.', + 27, + ], + [ + 'Cannot unset readonly UnsetVariableVariables\Foo::$readOnly property.', + 40, + ], + ]; + if (PHP_VERSION_ID >= 80400) { + $errors[] = [ + 'Cannot unset property UnsetVariableVariables\Foo::$regular because it might have hooks in a subclass.', + 43, + ]; + } + + $this->analyse([__DIR__ . '/data/unset-variable-variables.php'], $errors); + } + public function testBug2752(): void { $this->analyse([__DIR__ . '/data/bug-2752.php'], []); diff --git a/tests/PHPStan/Rules/Variables/data/bug-15014.php b/tests/PHPStan/Rules/Variables/data/bug-15014.php new file mode 100644 index 00000000000..899ad124d8c --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-15014.php @@ -0,0 +1,12 @@ +{$name})); + + $nullableName = 'nullable'; + var_dump(isset($foo->{$nullableName})); +} + +function dynamicStaticPropertyName(): void +{ + $name = 'staticNotNullable'; + var_dump(isset(Foo::${$name})); + + $nullableName = 'staticNullable'; + var_dump(isset(Foo::${$nullableName})); +} diff --git a/tests/PHPStan/Rules/Variables/data/null-coalesce-variable-variables.php b/tests/PHPStan/Rules/Variables/data/null-coalesce-variable-variables.php new file mode 100644 index 00000000000..cffe4c235d6 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/null-coalesce-variable-variables.php @@ -0,0 +1,88 @@ +{$name} ?? null; + + $nullableName = 'nullable'; + echo $foo->{$nullableName} ?? null; +} + +function dynamicStaticPropertyName(): void +{ + $name = 'staticNotNullable'; + echo Foo::${$name} ?? null; + + $nullableName = 'staticNullable'; + echo Foo::${$nullableName} ?? null; +} + +function offsetOnVariableVariable(): void +{ + $name = 'undefinedArray'; + echo ${$name}['foo'] ?? null; +} diff --git a/tests/PHPStan/Rules/Variables/data/unset-variable-variables.php b/tests/PHPStan/Rules/Variables/data/unset-variable-variables.php new file mode 100644 index 00000000000..b616db85cf6 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/unset-variable-variables.php @@ -0,0 +1,44 @@ += 8.1 + +declare(strict_types = 1); + +namespace UnsetVariableVariables; + +class Foo +{ + + public function __construct( + public readonly int $readOnly, + public int $regular, + ) + { + } + +} + +function unknownName(string $name): void +{ + unset(${$name}); +} + +function neverDefined(): void +{ + $name = 'undefinedVariable'; + unset(${$name}); +} + +function alwaysDefined(): void +{ + $defined = 1; + $name = 'defined'; + unset(${$name}); +} + +function dynamicPropertyName(Foo $foo): void +{ + $name = 'readOnly'; + unset($foo->{$name}); + + $regularName = 'regular'; + unset($foo->{$regularName}); +}