Repository navigation
Conversation
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<int>|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<Foo>|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 phpstan#7006 (phpstan/phpstan#15681)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I made the same PR as the referenced fix, was too late with opening so you fixed it already. But while walking though my work i also notices some extra ways the related issue might appear. Not sure if you want this, if this is overkill, but it seems the bug is still present in some combinations.
Follow-up to #7006 (phpstan/phpstan#15681).
#7006 fixed restricted usage on
?Foo,Foo|falseandclass-string<Foo>. Some cases are still not reported:Foo|false,Foo|stringandFoo|int|nullis reported at levels 2–6 but not at level 7 and above, so a higher level reports fewer errors:class-string<Foo>|falseused statically is missed at every level.findTypeToCheck()callback inRestrictedStaticPropertyUsageRulecheckshasInstanceProperty()and should checkhasStaticProperty(). This now matchesAccessStaticPropertiesCheck.Root cause
UnionType::getObjectClassNames()returns[]as soon as one member has no class names. AfterTypeCombinator::intersect($type, new ObjectWithoutClassType()), members likecallable-object,iterable&object, object shapes and object templates are still in the union, so the whole result is empty.getObjectTypeOrClassStringObjectType()on the whole union turnsfalse/intintoErrorTypeandstringintoobject.Foo|falsetherefore becomesErrorTypeand yields no class names. Below level 7,findTypeToCheck()happens to filter those members out first, which is why the result depends on the level.Fix
A small helper,
RestrictedUsageObjectClassNames, collects class names member by member:get(Type)is used by the 4 instance rules in place of the intersect.getFromClassExpression(Type)is used by the 4 static rules. It appliesgetObjectTypeOrClassStringObjectType()per member. The static rules still go throughRuleLevelHelper::findTypeToCheck(); only how class names are read from its result changes.Why not reuse something existing:
intersect(..., object)isn't enough, for the reason above: it leaves classless object members in the union, so cases 1 and 3 stay unreported.TypeUtils::flattenTypes()expands constant arrays (2^N optional keys). These rules run for every method call and property fetch on arbitrary types, so that's the wrong cost profile.AllowedArrayKeysTypesandPhpDocLineHelper.The loop is one
instanceofcheck plusgetObjectClassNames()per member, so it costs less than the intersect it replaces. On Drupal core with phpstan-deprecation-rules the difference in analysis time was within noise.Behaviour changes
findTypeToCheck()can't filter, such as'Foo'|falseorclass-string<Foo>|false, is now reported. The class really is a possible receiver there.checkThisOnlystill skips$expr::….mixed,object,object|Foo(which collapses toobject), non-class strings or plainclass-string.Tests
The new cases are appended to
tests/PHPStan/Rules/RestrictedUsage/data/bug-15681.php, so existing line numbers don't change. Each of the 8testBug15681()methods has new expectations. All 8 fail on 2.3.x without thesrc/changes. The phpstan-deprecation-rules test suite passes unchanged.Open question, not addressed here: at levels 0–1 the static rules don't check
$var::deprecatedStatic()at all, not even for a plainFoo $var. Instance access ($var->deprecated()) is checked at every level. Projects that run deprecation-rules at a low level only to find deprecations will miss these calls. Is that intended?