Repository navigation
Look up object class names in nullable and mixed unions and class-strings in restricted-usage rules - #7006
Conversation
…ings 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<Foo>.
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 <noreply@anthropic.com>
| $referencedClasses = TypeCombinator::intersect( | ||
| TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType(), | ||
| new ObjectWithoutClassType(), | ||
| )->getObjectClassNames(); |
There was a problem hiding this comment.
can this be simplified to $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames() ?
alternatively we need a test-case which shows it needs to stay like that.
same applies to other similar changes in this PR.
There was a problem hiding this comment.
I made your simplification in all four static-access rules: RestrictedStaticMethodUsageRule, RestrictedStaticMethodCallableUsageRule, RestrictedStaticPropertyUsageRule and RestrictedClassConstantUsageRule. Each now uses:
$referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames();The intersection with object did nothing in these rules. They get their type from RuleLevelHelper::findTypeToCheck(), which already drops union members that don't have the member being accessed. I checked this outside the test suite with S|int, S|false and class-string<S>|int: the old and new versions report exactly the same errors.
The four instance-access rules (method call, property fetch, method callable, string cast) still need the intersection, so I left them unchanged. There the type comes straight from $scope->getType() with no filtering. Without the intersection the code would be the original $type->getObjectClassNames(), which returns an empty list for Foo|null, and that empty list is the bug itself. The existing testBug15681 cases for those rules (?Request, Request|false, Request|int, and the getCurrentRequest()->doFoo() chain) already show this: they fail without the intersection.
The change is committed as 2e262ac and not pushed. make tests passes (23,546 tests, 108 skipped), and make phpstan and make cs are clean.
RuleLevelHelper already filters out union members without the accessed member, so intersecting with object is unnecessary there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| } | ||
|
|
||
| $referencedClasses = $classTypeResult->getReferencedClasses(); | ||
| $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); |
There was a problem hiding this comment.
can this be simplified to $referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getReferencedClasses();?
There was a problem hiding this comment.
I didn't change any code, and I didn't make a new commit or rerun the tests for this round. In the comment on RestrictedStaticMethodCallableUsageRule.php, staabm took back the suggestion to use TypeCombinator::removeNull(...)->getReferencedClasses() ("no, I was wrong"), and has since approved the PR. The code is the same as at commit 2e262ac, which the last round's make tests, make phpstan and make cs passed on.
The suggestion wouldn't have worked anyway. getReferencedClasses() returns every class mentioned anywhere in a type, including generic type arguments, so it's not the same as the class you access a member on. It also doesn't resolve class-string<Foo> to Foo the way getObjectTypeOrClassStringObjectType() does, so $className::doFoo() with a class-string<StaticRequest> would stop being checked.
All four static-access rules still use:
$referencedClasses = TypeCombinator::removeNull($classTypeResult->getType())->getObjectTypeOrClassStringObjectType()->getObjectClassNames();There was a problem hiding this comment.
I kept getObjectTypeOrClassStringObjectType()->getObjectClassNames() and didn't switch to getReferencedClasses(). As you suggested earlier, I added a test case that shows why the current code needs to stay.
Why getReferencedClasses() doesn't work here: it doesn't only return the class whose member is being accessed. It returns every class mentioned anywhere in the type, including generic type arguments. GenericObjectType::getReferencedClasses() adds the referenced classes of each type argument, and GenericClassStringType passes the call through to its inner type. For GenericStaticRequest<StaticRequest>|null, it returns both GenericStaticRequest and StaticRequest, so the rules would also check StaticRequest::doFoo(), a class that isn't being called.
New test case: I added testGeneric() to tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php. It does static method calls, static callables, static property reads and class constant reads on:
GenericStaticRequest<StaticRequest>|nullclass-string<GenericStaticRequest<StaticRequest>>
StaticRequest also has a doFoo() method, a $foo property and a FOO constant, which makes the difference visible. I added the expected errors to the four static-rule tests.
I ran it both ways:
- With your suggested change: each new line is reported twice, once for
GenericStaticRequestand once forStaticRequest. InRestrictedStaticMethodUsageRuleTest, lines 115 and 120 each showed twoCannot call doFooerrors. - With the current code: each line is reported once, which is correct.
make tests passes (23,546 tests, 108 skipped), and make phpstan and make cs are clean. The test is committed as 4914ca9 and not pushed.
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 <noreply@anthropic.com>
|
👍 |
|
I noticed a few things whil working on this, i ddin push before you fixed it xD See references pr |
phpunit and drupal: phpstan/phpstan-src#7006 now runs restricted-usage checks on nullable receivers, so calls on ?Driver and \JsonSchema\Validator|null are reported. symfony: doctrine/persistence 4.3.x-dev deprecated ClassMetadataFactory::hasMetadataFor() (doctrine/persistence#533). Amp-Thread-ID: https://ampcode.com/threads/T-01a19770-5331-74b2-963f-e324ae0f7635
Summary
Restricted-usage extensions (deprecations,
@internal, etc.) were not called for members accessed on a nullable object type. For example,$requestStack->getCurrentRequest()->get()wheregetCurrentRequest()returns?Requestwas not reported.$nullable?->get()was reported. The same gap existed for other non-object union members (Foo|false,Foo|int), for first-class callables and string casts, and for static access, class constants and static properties on?Fooand onclass-string<Foo>.Changes
src/Rules/RestrictedUsage/RestrictedUsageObjectClassNames.php:get(Type): returnsgetObjectClassNames(). If that list is empty and the type may be an object, it intersects the type withobjectand reads the class names from the result.getForStaticAccess(Type): removesnull, appliesgetObjectTypeOrClassStringObjectType()so class-strings resolve to their object type, then callsget().get():RestrictedMethodUsageRule,RestrictedPropertyUsageRule,RestrictedMethodCallableUsageRule,RestrictedUsageOfDeprecatedStringCastRule.::) usesgetForStaticAccess()on the type found byRuleLevelHelper:RestrictedStaticMethodUsageRule,RestrictedStaticMethodCallableUsageRule,RestrictedStaticPropertyUsageRule,RestrictedClassConstantUsageRule.Root cause
UnionType::getObjectClassNames()returns an empty list as soon as one member is not an object, soRequest|nullproduced no class names and the extensions were never called. The static rules usedFoundTypeResult::getReferencedClasses(), which comes from the samegetObjectClassNames()call. That list is also empty forclass-string<Foo>, so$className::deprecatedMethod()was never reported either.Test
tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.phphas atestBug15681case in each of the eight restricted-usage rule tests. It covers:?Foo, onFoo|false,Foo|Other|nullandFoo|int, and on a?Foo-returning method call(string)casts?Foo,class-string<Foo>andclass-string<Foo>|nullAll eight tests fail without the fix.
Fixes phpstan/phpstan#15681