From b091695a2d921381b48228158f514231079e0e15 Mon Sep 17 00:00:00 2001 From: bjorn Date: Mon, 9 Nov 2026 09:40:39 +0100 Subject: [PATCH] Check every union member for restricted usage class names The restricted usage rules (method, property, static, class constant, callable, string cast) missed the class of a union member in several cases: - Static access on Foo|false, Foo|string, Foo|int|null was lost at level 7+, because the whole union was converted at once. - Instance access on callable|Foo, iterable|Foo, Foo|object{a: int} and TObject|Foo found no class name, because the intersection with object left a member without a class. - class-string|false static access was never checked. - RestrictedStaticPropertyUsageRule looked for an instance property instead of a static one when picking the type to check, so $orFalse::$prop was missed. Class names are now collected per union member. Follow-up to https://github.com/phpstan/phpstan-src/pull/7006 (phpstan/phpstan#15681) --- .../RestrictedClassConstantUsageRule.php | 3 +- .../RestrictedMethodCallableUsageRule.php | 4 +- .../RestrictedMethodUsageRule.php | 4 +- .../RestrictedPropertyUsageRule.php | 4 +- ...estrictedStaticMethodCallableUsageRule.php | 3 +- .../RestrictedStaticMethodUsageRule.php | 3 +- .../RestrictedStaticPropertyUsageRule.php | 5 +- .../RestrictedUsageObjectClassNames.php | 59 ++++++++++++++++ ...trictedUsageOfDeprecatedStringCastRule.php | 4 +- .../RestrictedClassConstantUsageRuleTest.php | 16 +++++ .../RestrictedMethodCallableUsageRuleTest.php | 16 +++++ .../RestrictedMethodUsageRuleTest.php | 16 +++++ .../RestrictedPropertyUsageRuleTest.php | 16 +++++ ...ictedStaticMethodCallableUsageRuleTest.php | 16 +++++ .../RestrictedStaticMethodUsageRuleTest.php | 16 +++++ .../RestrictedStaticPropertyUsageRuleTest.php | 16 +++++ ...tedUsageOfDeprecatedStringCastRuleTest.php | 16 +++++ .../Rules/RestrictedUsage/data/bug-15681.php | 70 +++++++++++++++++++ 18 files changed, 266 insertions(+), 21 deletions(-) create mode 100644 src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php diff --git a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php index 7aa7257df68..359a918bca6 100644 --- a/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedClassConstantUsageRule.php @@ -15,7 +15,6 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -72,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::getFromClassExpression($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php index 2cfbf92c4d7..a143a64594d 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodCallableUsageRule.php @@ -12,8 +12,6 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -54,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->getName()->name; $methodCalledOnType = $scope->getType($node->getVar()); - $referencedClasses = TypeCombinator::intersect($methodCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($methodCalledOnType); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php index 1f04eb0bbeb..859a2dc902a 100644 --- a/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedMethodUsageRule.php @@ -12,8 +12,6 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -54,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->name->name; $methodCalledOnType = $scope->getType($node->var); - $referencedClasses = TypeCombinator::intersect($methodCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($methodCalledOnType); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php index b86084ed174..36f2e97e98a 100644 --- a/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedPropertyUsageRule.php @@ -11,8 +11,6 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -53,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array $propertyName = $node->name->name; $propertyCalledOnType = $scope->getType($node->var); - $referencedClasses = TypeCombinator::intersect($propertyCalledOnType, new ObjectWithoutClassType())->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($propertyCalledOnType); $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php index 9ed23eed04b..e137fed31a6 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRule.php @@ -16,7 +16,6 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -73,7 +72,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::getFromClassExpression($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php index 711fc8dcaac..07e340e42b0 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticMethodUsageRule.php @@ -15,7 +15,6 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -72,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::getFromClassExpression($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php index 6046e368fac..ec3cec8eeaa 100644 --- a/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php +++ b/src/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRule.php @@ -15,7 +15,6 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -65,14 +64,14 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->class, '', // We don't care about the error message - static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasInstanceProperty($propertyName)->yes(), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasStaticProperty($propertyName)->yes(), ); if ($classTypeResult->getType() instanceof ErrorType) { return []; } - $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::getFromClassExpression($classTypeResult->getType()); } $errors = []; diff --git a/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php b/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php new file mode 100644 index 00000000000..9d6dc0d254e --- /dev/null +++ b/src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php @@ -0,0 +1,59 @@ + + */ + public static function get(Type $type): array + { + return self::collect($type, false); + } + + /** + * Same as get(), for the class expression of a static access (`$expr::foo()`), + * where a member can also be a class-string. + * + * @return list + */ + public static function getFromClassExpression(Type $type): array + { + return self::collect($type, true); + } + + /** + * @return list + */ + private static function collect(Type $type, bool $acceptClassStrings): array + { + $members = $type instanceof UnionType ? $type->getTypes() : [$type]; + + $classNames = []; + foreach ($members as $member) { + if ($acceptClassStrings) { + $member = $member->getObjectTypeOrClassStringObjectType(); + } + + foreach ($member->getObjectClassNames() as $className) { + $classNames[$className] = $className; + } + } + + return array_values($classNames); + } + +} diff --git a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php index 1cb985bd801..81ca144ddc0 100644 --- a/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php +++ b/src/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRule.php @@ -11,8 +11,6 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\TypeCombinator; /** * @implements Rule @@ -45,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array } $exprType = $scope->getType($node->expr); - $referencedClasses = TypeCombinator::intersect($exprType, new ObjectWithoutClassType())->getObjectClassNames(); + $referencedClasses = RestrictedUsageObjectClassNames::get($exprType); $errors = []; diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php index 9eaa348f0b7..0144bb29b8b 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedClassConstantUsageRuleTest.php @@ -66,6 +66,22 @@ public function testBug15681(): void 'Cannot access FOO', 123, ], + [ + 'Cannot access FOO', + 172, + ], + [ + 'Cannot access FOO', + 177, + ], + [ + 'Cannot access FOO', + 182, + ], + [ + 'Cannot access FOO', + 193, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php index 0540915c05d..7232adde25a 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodCallableUsageRuleTest.php @@ -43,6 +43,22 @@ public function testBug15681(): void 'Cannot call doFoo', 65, ], + [ + 'Cannot call doFoo', + 141, + ], + [ + 'Cannot call doFoo', + 142, + ], + [ + 'Cannot call doFoo', + 143, + ], + [ + 'Cannot call doFoo', + 158, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php index baa5b7226ef..bdf59fc0236 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedMethodUsageRuleTest.php @@ -62,6 +62,22 @@ public function testBug15681(): void 'Cannot call doFoo', 57, ], + [ + 'Cannot call doFoo', + 133, + ], + [ + 'Cannot call doFoo', + 134, + ], + [ + 'Cannot call doFoo', + 135, + ], + [ + 'Cannot call doFoo', + 156, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php index 4bf3c0ad779..e1161a47c4b 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedPropertyUsageRuleTest.php @@ -50,6 +50,22 @@ public function testBug15681(): void 'Cannot access $foo', 62, ], + [ + 'Cannot access $foo', + 137, + ], + [ + 'Cannot access $foo', + 138, + ], + [ + 'Cannot access $foo', + 139, + ], + [ + 'Cannot access $foo', + 157, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php index 6f34206703d..dab5d9ca1be 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodCallableUsageRuleTest.php @@ -79,6 +79,22 @@ public function testBug15681(): void 'Cannot call doFoo', 121, ], + [ + 'Cannot call doFoo', + 170, + ], + [ + 'Cannot call doFoo', + 175, + ], + [ + 'Cannot call doFoo', + 180, + ], + [ + 'Cannot call doFoo', + 191, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php index a63a545ae81..692466e06aa 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticMethodUsageRuleTest.php @@ -78,6 +78,22 @@ public function testBug15681(): void 'Cannot call doFoo', 120, ], + [ + 'Cannot call doFoo', + 169, + ], + [ + 'Cannot call doFoo', + 174, + ], + [ + 'Cannot call doFoo', + 179, + ], + [ + 'Cannot call doFoo', + 190, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php index e5a0b3c745f..c00244e9f96 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedStaticPropertyUsageRuleTest.php @@ -77,6 +77,22 @@ public function testBug15681(): void 'Cannot access $foo', 122, ], + [ + 'Cannot access $foo', + 171, + ], + [ + 'Cannot access $foo', + 176, + ], + [ + 'Cannot access $foo', + 181, + ], + [ + 'Cannot access $foo', + 192, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php index 8fbc13b40c9..0204a7f5cff 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php +++ b/tests/PHPStan/Rules/RestrictedUsage/RestrictedUsageOfDeprecatedStringCastRuleTest.php @@ -42,6 +42,22 @@ public function testBug15681(): void 'Cannot call __toString', 68, ], + [ + 'Cannot call __toString', + 145, + ], + [ + 'Cannot call __toString', + 146, + ], + [ + 'Cannot call __toString', + 147, + ], + [ + 'Cannot call __toString', + 159, + ], ]); } diff --git a/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php b/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php index 87fc0b49126..b877b9be828 100644 --- a/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php +++ b/tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php @@ -122,3 +122,73 @@ function testGeneric(?GenericStaticRequest $generic, string $genericClassName): echo $genericClassName::$foo; echo $genericClassName::FOO; } + +/** + * @param (callable(): void)|Request $callable + * @param iterable|Request $iterable + * @param object{a: int}|Request $objectShape + */ +function testUnionWithClasslessObject($callable, $iterable, $objectShape): void +{ + $callable->doFoo(); + $iterable->doFoo(); + $objectShape->doFoo(); + + echo $callable->foo; + echo $iterable->foo; + echo $objectShape->foo; + + $callable->doFoo(...); + $iterable->doFoo(...); + $objectShape->doFoo(...); + + (string) $callable; + (string) $iterable; + (string) $objectShape; +} + +/** + * @template TObject of object + * @param TObject|Request $templated + */ +function testUnionWithTemplate(object $templated): void +{ + $templated->doFoo(); + echo $templated->foo; + $templated->doFoo(...); + (string) $templated; +} + +/** + * @param StaticRequest|false $orFalse + * @param StaticRequest|string $orString + * @param StaticRequest|int|null $orIntOrNull + */ +function testStaticOnMixedUnion(StaticRequest|false $orFalse, StaticRequest|string $orString, StaticRequest|int|null $orIntOrNull): void +{ + $orFalse::doFoo(); + $orFalse::doFoo(...); + echo $orFalse::$foo; + echo $orFalse::FOO; + + $orString::doFoo(); + $orString::doFoo(...); + echo $orString::$foo; + echo $orString::FOO; + + $orIntOrNull::doFoo(); + $orIntOrNull::doFoo(...); + echo $orIntOrNull::$foo; + echo $orIntOrNull::FOO; +} + +/** + * @param class-string|false $classNameOrFalse + */ +function testClassStringOrFalse(string|false $classNameOrFalse): void +{ + $classNameOrFalse::doFoo(); + $classNameOrFalse::doFoo(...); + echo $classNameOrFalse::$foo; + echo $classNameOrFalse::FOO; +}