From d41f614c3b5b265f873afe866c19fedb6a611a88 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Sun, 8 Nov 2026 17:40:36 +0000 Subject: [PATCH] Call restricted usage extensions for members accessed on nullable types Restricted usage rules took class names from getObjectClassNames(), which returns nothing for unions with non-object members like Foo|null. Strip the non-object parts first so extensions are still called once per class. Amp-Thread-ID: https://ampcode.com/threads/T-01a1bc39-a252-73e6-a583-cfe70f1d1c88 --- .../RestrictedClassConstantUsageRule.php | 4 +- .../RestrictedMethodCallableUsageRule.php | 4 +- .../RestrictedMethodUsageRule.php | 4 +- .../RestrictedPropertyUsageRule.php | 4 +- ...estrictedStaticMethodCallableUsageRule.php | 4 +- .../RestrictedStaticMethodUsageRule.php | 4 +- .../RestrictedStaticPropertyUsageRule.php | 4 +- ...trictedUsageOfDeprecatedStringCastRule.php | 4 +- .../RestrictedClassConstantUsageRuleTest.php | 12 ++++ .../RestrictedMethodCallableUsageRuleTest.php | 11 ++++ .../RestrictedMethodUsageRuleTest.php | 28 +++++++++ .../RestrictedPropertyUsageRuleTest.php | 28 +++++++++ ...ictedStaticMethodCallableUsageRuleTest.php | 11 ++++ .../RestrictedStaticMethodUsageRuleTest.php | 11 ++++ .../RestrictedStaticPropertyUsageRuleTest.php | 12 ++++ ...tedUsageOfDeprecatedStringCastRuleTest.php | 4 ++ .../data/restricted-nullable.php | 63 +++++++++++++++++++ .../data/restricted-to-string.php | 5 ++ 18 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Rules/RestrictedUsage/data/restricted-nullable.php diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php index 43505b34532..594e3f92ae1 100644 --- a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php @@ -14,7 +14,9 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -71,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = TypeCombinator::intersect($classTypeResult->getType(), new ObjectWithoutClassType())->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php index f983f281f4c..2cfbf92c4d7 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php @@ -12,6 +12,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -52,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->getName()->name; $methodCalledOnType = $scope->getType($node->getVar()); - $referencedClasses = $methodCalledOnType->getObjectClassNames(); + $referencedClasses = TypeCombinator::intersect($methodCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php index 7a869fddef4..1f04eb0bbeb 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php @@ -12,6 +12,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -52,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->name->name; $methodCalledOnType = $scope->getType($node->var); - $referencedClasses = $methodCalledOnType->getObjectClassNames(); + $referencedClasses = TypeCombinator::intersect($methodCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php index 1d3dc8a126a..b86084ed174 100644 --- a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php @@ -11,6 +11,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -51,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array $propertyName = $node->name->name; $propertyCalledOnType = $scope->getType($node->var); - $referencedClasses = $propertyCalledOnType->getObjectClassNames(); + $referencedClasses = TypeCombinator::intersect($propertyCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php index cc5b2b46f28..a7d7567221e 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php @@ -15,7 +15,9 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -72,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = TypeCombinator::intersect($classTypeResult->getType(), new ObjectWithoutClassType())->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php index 7028d8e68c5..a7e059286f5 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php @@ -14,7 +14,9 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -71,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = TypeCombinator::intersect($classTypeResult->getType(), new ObjectWithoutClassType())->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php index da220231c7c..33ac27c9491 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -14,7 +14,9 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -71,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = TypeCombinator::intersect($classTypeResult->getType(), new ObjectWithoutClassType())->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php index 4641a2f19d6..1cb985bd801 100644 --- a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php +++ b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php @@ -11,6 +11,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -43,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array } $exprType = $scope->getType($node->expr); - $referencedClasses = $exprType->getObjectClassNames(); + $referencedClasses = TypeCombinator::intersect($exprType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php index 460af0a1fd8..7a9bf5a14d3 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -41,6 +42,17 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testNullable(): void + { + $this->analyse([__DIR__ . '/data/restricted-nullable.php'], [ + [ + 'Cannot access FOO', + 53, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php index 331116731e8..fbc36525662 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php @@ -31,6 +31,17 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testNullable(): void + { + $this->analyse([__DIR__ . '/data/restricted-nullable.php'], [ + [ + 'Cannot call doFoo', + 50, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php index d3c6fa093cd..603d13a0ed7 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule as TRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -29,6 +30,33 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testNullable(): void + { + $this->analyse([__DIR__ . '/data/restricted-nullable.php'], [ + [ + 'Cannot call doFoo', + 48, + ], + [ + 'Cannot call doFoo', + 49, + ], + [ + 'Cannot call doFoo', + 57, + ], + [ + 'Cannot call doFoo', + 57, + ], + [ + 'Cannot call doFoo', + 59, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php index f011b645f9c..61f785eca8d 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule as TRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -29,6 +30,33 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testNullable(): void + { + $this->analyse([__DIR__ . '/data/restricted-nullable.php'], [ + [ + 'Cannot access $foo', + 51, + ], + [ + 'Cannot access $foo', + 52, + ], + [ + 'Cannot access $foo', + 58, + ], + [ + 'Cannot access $foo', + 58, + ], + [ + 'Cannot access $foo', + 60, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php index a643faf3034..56ca1ca93c1 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php @@ -55,6 +55,17 @@ public function testBug12951(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testNullable(): void + { + $this->analyse([__DIR__ . '/data/restricted-nullable.php'], [ + [ + 'Cannot call doFoo', + 55, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php index 11c4d674504..9df782aa223 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php @@ -54,6 +54,17 @@ public function testBug12951(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testNullable(): void + { + $this->analyse([__DIR__ . '/data/restricted-nullable.php'], [ + [ + 'Cannot call doFoo', + 54, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php index 9d8fa01f6d7..2e3f8f888b7 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule as TRule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -52,6 +53,17 @@ public function testBug12951(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testNullable(): void + { + $this->analyse([__DIR__ . '/data/restricted-nullable.php'], [ + [ + 'Cannot access $foo', + 56, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php index d6ed16a496b..1542a4d44c5 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php @@ -26,6 +26,10 @@ public function testRule(): void 'Cannot call __toString', 11, ], + [ + 'Cannot call __toString', + 26, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/restricted-nullable.php b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-nullable.php new file mode 100644 index 00000000000..763ac1216cf --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-nullable.php @@ -0,0 +1,63 @@ += 8.1 + +namespace RestrictedUsageNullable; + +class Foo +{ + + public const FOO = 1; + + public $foo; + + public function doFoo(): void + { + + } + +} + +class FooStatic +{ + + public static $foo; + + public static function doFoo(): void + { + + } + +} + +class Bar +{ + + public $foo; + + public function doFoo(): void + { + + } + +} + +class Test +{ + + public function doTest(?Foo $foo, ?FooStatic $fooStatic, Foo|Bar|null $fooOrBar, Foo|false $fooOrFalse): void + { + $foo->doFoo(); + $foo?->doFoo(); + $foo->doFoo(...); + $foo->foo; + $foo?->foo; + $foo::FOO; + $fooStatic::doFoo(); + $fooStatic::doFoo(...); + $fooStatic::$foo; + $fooOrBar->doFoo(); + $fooOrBar->foo; + $fooOrFalse->doFoo(); + $fooOrFalse->foo; + } + +} diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/restricted-to-string.php b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-to-string.php index 6742e3116f6..f0d6eb7f88b 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/data/restricted-to-string.php +++ b/tests/PHPStan/Rules/RestrictedUsage/data/restricted-to-string.php @@ -21,6 +21,11 @@ public function doTest2(FooWithoutToString $f) (string) $f; } + public function doTest3(?FooToString $f) + { + (string) $f; + } + } class FooWithoutToString