From 3c3570506dd00076dd66e6c93b179bcce8490fa3 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:37:58 +0000 Subject: [PATCH 1/4] Look up object class names in nullable and mixed unions and class-strings in restricted-usage rules - Add RestrictedUsageObjectClassNames helper: when Type::getObjectClassNames() is empty for a type that may be an object (e.g. Foo|null, Foo|false, Foo|int), intersect it with `object` first so the class names of the object members are found. - Use it in RestrictedMethodUsageRule, RestrictedPropertyUsageRule, RestrictedMethodCallableUsageRule and RestrictedUsageOfDeprecatedStringCastRule. - For the left side of `::` (RestrictedStaticMethodUsageRule, RestrictedStaticMethodCallableUsageRule, RestrictedStaticPropertyUsageRule, RestrictedClassConstantUsageRule), remove null and resolve class-strings with getObjectTypeOrClassStringObjectType(). Previously these used the FoundTypeResult referenced classes, which were empty for ?Foo as well as for class-string. --- .../RestrictedClassConstantUsageRule.php | 2 +- .../RestrictedMethodCallableUsageRule.php | 2 +- .../RestrictedMethodUsageRule.php | 2 +- .../RestrictedPropertyUsageRule.php | 2 +- ...estrictedStaticMethodCallableUsageRule.php | 2 +- .../RestrictedStaticMethodUsageRule.php | 2 +- .../RestrictedStaticPropertyUsageRule.php | 2 +- .../RestrictedUsageObjectClassNames.php | 38 ++++++++ ...trictedUsageOfDeprecatedStringCastRule.php | 2 +- .../RestrictedClassConstantUsageRuleTest.php | 20 ++++ .../RestrictedMethodCallableUsageRuleTest.php | 15 +++ .../RestrictedMethodUsageRuleTest.php | 36 ++++++++ .../RestrictedPropertyUsageRuleTest.php | 24 +++++ ...ictedStaticMethodCallableUsageRuleTest.php | 19 ++++ .../RestrictedStaticMethodUsageRuleTest.php | 19 ++++ .../RestrictedStaticPropertyUsageRuleTest.php | 20 ++++ ...tedUsageOfDeprecatedStringCastRuleTest.php | 16 ++++ .../Rules/RestrictedUsage/data/bug-15681.php | 91 +++++++++++++++++++ 18 files changed, 306 insertions(+), 8 deletions(-) create mode 100644 src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php create mode 100644 tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php index 43505b34532..ea6796cda3f 100644 --- a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php index f983f281f4c..a143a64594d 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->getName()->name; $methodCalledOnType = $scope->getType($node->getVar()); - $referencedClasses = $methodCalledOnType->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($methodCalledOnType); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php index 7a869fddef4..859a2dc902a 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->name->name; $methodCalledOnType = $scope->getType($node->var); - $referencedClasses = $methodCalledOnType->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($methodCalledOnType); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php index 1d3dc8a126a..36f2e97e98a 100644 --- a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php @@ -51,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array $propertyName = $node->name->name; $propertyCalledOnType = $scope->getType($node->var); - $referencedClasses = $propertyCalledOnType->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($propertyCalledOnType); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php index cc5b2b46f28..86cb36f2d39 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php @@ -72,7 +72,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php index 7028d8e68c5..89e1a936eb9 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php index da220231c7c..191433fa650 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -71,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = $classTypeResult->getReferencedClasses(); + $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php b/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php new file mode 100644 index 00000000000..4d812737c5d --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php @@ -0,0 +1,38 @@ + + */ + public static function get(Type $type): array + { + $classNames = $type->getObjectClassNames(); + if ($classNames !== [] || $type->isObject()->no()) { + return $classNames; + } + + return TypeCombinator::intersect($type, new ObjectWithoutClassType())->getObjectClassNames(); + } + + /** + * For the left side of `::`, which can also be a class-string. + * + * @return list + */ + public static function getForStaticAccess(Type $type): array + { + return self::get(TypeCombinator::removeNull($type)->getObjectTypeOrClassStringObjectType()); + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php index 4641a2f19d6..81ca144ddc0 100644 --- a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php +++ b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php @@ -43,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array } $exprType = $scope->getType($node->expr); - $referencedClasses = $exprType->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($exprType); $errors = []; diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php index 460af0a1fd8..4e3796e12f8 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,25 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot access FOO', + 73, + ], + [ + 'Cannot access FOO', + 85, + ], + [ + 'Cannot access FOO', + 90, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php index 331116731e8..0540915c05d 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php @@ -31,6 +31,21 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot call doFoo', + 64, + ], + [ + 'Cannot call doFoo', + 65, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php index d3c6fa093cd..baa5b7226ef 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,41 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot call doFoo', + 51, + ], + [ + 'Cannot call doFoo', + 52, + ], + [ + 'Cannot call doFoo', + 53, + ], + [ + 'Cannot call doFoo', + 54, + ], + [ + 'Cannot call doFoo', + 55, + ], + [ + 'Cannot call doFoo', + 56, + ], + [ + 'Cannot call doFoo', + 57, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php index f011b645f9c..4bf3c0ad779 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,29 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot access $foo', + 59, + ], + [ + 'Cannot access $foo', + 60, + ], + [ + 'Cannot access $foo', + 61, + ], + [ + 'Cannot access $foo', + 62, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php index a643faf3034..121626a1aa5 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php @@ -55,6 +55,25 @@ public function testBug12951(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot call doFoo', + 71, + ], + [ + 'Cannot call doFoo', + 83, + ], + [ + 'Cannot call doFoo', + 88, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php index 11c4d674504..63287ab7b35 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php @@ -54,6 +54,25 @@ public function testBug12951(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot call doFoo', + 70, + ], + [ + 'Cannot call doFoo', + 82, + ], + [ + 'Cannot call doFoo', + 87, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php index 9d8fa01f6d7..f090546934d 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,25 @@ public function testBug12951(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot access $foo', + 72, + ], + [ + 'Cannot access $foo', + 84, + ], + [ + 'Cannot access $foo', + 89, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php index d6ed16a496b..8fbc13b40c9 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -29,6 +30,21 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15681(): void + { + $this->analyse([__DIR__ . '/data/bug-15681.php'], [ + [ + 'Cannot call __toString', + 67, + ], + [ + 'Cannot call __toString', + 68, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php b/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php new file mode 100644 index 00000000000..e7063511ca2 --- /dev/null +++ b/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php @@ -0,0 +1,91 @@ += 8.1 + +namespace Bug15681RestrictedUsage; + +class Request +{ + + public string $foo = ''; + + public function doFoo(): void + { + } + + public function __toString(): string + { + return ''; + } + +} + +class RequestStack +{ + + public function getCurrentRequest(): ?Request + { + return null; + } + +} + +class Other +{ + +} + +class StaticRequest +{ + + public static string $foo = ''; + + public const FOO = 1; + + public static function doFoo(): void + { + } + +} + +function test(RequestStack $stack, Request $request, ?Request $nullable, Request|false $falsy, Request|Other|null $multi, Request|int $int, ?StaticRequest $static): void +{ + $request->doFoo(); + $nullable?->doFoo(); + $nullable->doFoo(); + $stack->getCurrentRequest()->doFoo(); + $falsy->doFoo(); + $multi->doFoo(); + $int->doFoo(); + + echo $request->foo; + echo $nullable->foo; + echo $stack->getCurrentRequest()->foo; + echo $falsy->foo; + + $nullable->doFoo(...); + $falsy->doFoo(...); + + (string) $nullable; + (string) $falsy; + + $static::doFoo(); + $static::doFoo(...); + echo $static::$foo; + echo $static::FOO; +} + +/** + * @param class-string $className + * @param class-string|null $nullableClassName + */ +function testClassString(string $className, ?string $nullableClassName): void +{ + $className::doFoo(); + $className::doFoo(...); + echo $className::$foo; + echo $className::FOO; + + $nullableClassName::doFoo(); + $nullableClassName::doFoo(...); + echo $nullableClassName::$foo; + echo $nullableClassName::FOO; +} From 823254742aa2c23c18bc7c4ba9fe50a322391f1b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 8 Nov 2026 16:13:04 +0000 Subject: [PATCH 2/4] Inline object class name lookup in restricted-usage rules Drop the RestrictedUsageObjectClassNames helper and use TypeCombinator::intersect() with ObjectWithoutClassType directly, like the rest of the codebase does. Co-Authored-By: Claude Opus 5.5 --- .../RestrictedClassConstantUsageRule.php | 7 +++- .../RestrictedMethodCallableUsageRule.php | 4 +- .../RestrictedMethodUsageRule.php | 4 +- .../RestrictedPropertyUsageRule.php | 4 +- ...estrictedStaticMethodCallableUsageRule.php | 7 +++- .../RestrictedStaticMethodUsageRule.php | 7 +++- .../RestrictedStaticPropertyUsageRule.php | 7 +++- .../RestrictedUsageObjectClassNames.php | 38 ------------------- ...trictedUsageOfDeprecatedStringCastRule.php | 4 +- 9 files changed, 36 insertions(+), 46 deletions(-) delete mode 100644 src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php index ea6796cda3f..1516b0488a1 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,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); + $referencedClasses = TypeCombinator::intersect( + TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), + new ObjectWithoutClassType(), + )->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php index a143a64594d..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 = RestrictedUsageObjectClassNames::get($methodCalledOnType); + $referencedClasses = TypeCombinator::intersect($methodCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php index 859a2dc902a..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 = RestrictedUsageObjectClassNames::get($methodCalledOnType); + $referencedClasses = TypeCombinator::intersect($methodCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php index 36f2e97e98a..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 = RestrictedUsageObjectClassNames::get($propertyCalledOnType); + $referencedClasses = TypeCombinator::intersect($propertyCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php index 86cb36f2d39..b6f9cbaf73f 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,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); + $referencedClasses = TypeCombinator::intersect( + TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), + new ObjectWithoutClassType(), + )->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php index 89e1a936eb9..ed1de567efc 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,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); + $referencedClasses = TypeCombinator::intersect( + TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), + new ObjectWithoutClassType(), + )->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php index 191433fa650..8f513e52297 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,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = RestrictedUsageObjectClassNames::getForStaticAccess($classTypeResult->getType()); + $referencedClasses = TypeCombinator::intersect( + TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), + new ObjectWithoutClassType(), + )->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php b/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php deleted file mode 100644 index 4d812737c5d..00000000000 --- a/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ - public static function get(Type $type): array - { - $classNames = $type->getObjectClassNames(); - if ($classNames !== [] || $type->isObject()->no()) { - return $classNames; - } - - return TypeCombinator::intersect($type, new ObjectWithoutClassType())->getObjectClassNames(); - } - - /** - * For the left side of `::`, which can also be a class-string. - * - * @return list - */ - public static function getForStaticAccess(Type $type): array - { - return self::get(TypeCombinator::removeNull($type)->getObjectTypeOrClassStringObjectType()); - } - -} diff --git a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php index 81ca144ddc0..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 = RestrictedUsageObjectClassNames::get($exprType); + $referencedClasses = TypeCombinator::intersect($exprType, new ObjectWithoutClassType())->getObjectClassNames(); $errors = []; From 2e262accef64e4ba874c5b1bd027b83cb435d1f1 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 8 Nov 2026 16:39:18 +0000 Subject: [PATCH 3/4] Simplify class name lookup in static restricted-usage rules RuleLevelHelper already filters out union members without the accessed member, so intersecting with object is unnecessary there. Co-Authored-By: Claude Opus 5.5 --- .../RestrictedUsage/RestrictedClassConstantUsageRule.php | 6 +----- .../RestrictedStaticMethodCallableUsageRule.php | 6 +----- .../RestrictedUsage/RestrictedStaticMethodUsageRule.php | 6 +----- .../RestrictedUsage/RestrictedStaticPropertyUsageRule.php | 6 +----- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php index 1516b0488a1..7aa7257df68 100644 --- a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php @@ -14,7 +14,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -73,10 +72,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = TypeCombinator::intersect( - TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), - new ObjectWithoutClassType(), - )->getObjectClassNames(); + $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php index b6f9cbaf73f..9ed23eed04b 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php @@ -15,7 +15,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -74,10 +73,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = TypeCombinator::intersect( - TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), - new ObjectWithoutClassType(), - )->getObjectClassNames(); + $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php index ed1de567efc..711fc8dcaac 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php @@ -14,7 +14,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -73,10 +72,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = TypeCombinator::intersect( - TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), - new ObjectWithoutClassType(), - )->getObjectClassNames(); + $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php index 8f513e52297..6046e368fac 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -14,7 +14,6 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -73,10 +72,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = TypeCombinator::intersect( - TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), - new ObjectWithoutClassType(), - )->getObjectClassNames(); + $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); } $errors = []; From 4914ca9983370dd8b32d024726992c74daf62ad2 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 8 Nov 2026 16:53:08 +0000 Subject: [PATCH 4/4] Test generic types in static restricted-usage rules getReferencedClasses() would also return the generic type arguments, so the same member would be checked on unrelated classes. Co-Authored-By: Claude Opus 5.5 --- .../RestrictedClassConstantUsageRuleTest.php | 8 +++++ ...ictedStaticMethodCallableUsageRuleTest.php | 8 +++++ .../RestrictedStaticMethodUsageRuleTest.php | 8 +++++ .../RestrictedStaticPropertyUsageRuleTest.php | 8 +++++ .../Rules/RestrictedUsage/data/bug-15681.php | 33 +++++++++++++++++++ 5 files changed, 65 insertions(+) diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php index 4e3796e12f8..9eaa348f0b7 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php @@ -58,6 +58,14 @@ public function testBug15681(): void 'Cannot access FOO', 90, ], + [ + 'Cannot access FOO', + 118, + ], + [ + 'Cannot access FOO', + 123, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php index 121626a1aa5..6f34206703d 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php @@ -71,6 +71,14 @@ public function testBug15681(): void 'Cannot call doFoo', 88, ], + [ + 'Cannot call doFoo', + 116, + ], + [ + 'Cannot call doFoo', + 121, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php index 63287ab7b35..a63a545ae81 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php @@ -70,6 +70,14 @@ public function testBug15681(): void 'Cannot call doFoo', 87, ], + [ + 'Cannot call doFoo', + 115, + ], + [ + 'Cannot call doFoo', + 120, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php index f090546934d..e5a0b3c745f 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php @@ -69,6 +69,14 @@ public function testBug15681(): void 'Cannot access $foo', 89, ], + [ + 'Cannot access $foo', + 117, + ], + [ + 'Cannot access $foo', + 122, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php b/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php index e7063511ca2..87fc0b49126 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php +++ b/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php @@ -89,3 +89,36 @@ function testClassString(string $className, ?string $nullableClassName): void echo $nullableClassName::$foo; echo $nullableClassName::FOO; } + +/** + * @template T + */ +class GenericStaticRequest +{ + + public static string $foo = ''; + + public const FOO = 1; + + public static function doFoo(): void + { + } + +} + +/** + * @param GenericStaticRequest|null $generic + * @param class-string> $genericClassName + */ +function testGeneric(?GenericStaticRequest $generic, string $genericClassName): void +{ + $generic::doFoo(); + $generic::doFoo(...); + echo $generic::$foo; + echo $generic::FOO; + + $genericClassName::doFoo(); + $genericClassName::doFoo(...); + echo $genericClassName::$foo; + echo $genericClassName::FOO; +}