Resolve forward_static_call() with the caller's late static binding#6051
Open
zonuexe wants to merge 3 commits into
Open
Resolve forward_static_call() with the caller's late static binding#6051zonuexe wants to merge 3 commits into
zonuexe wants to merge 3 commits into
Conversation
…tic binding forward_static_call() and forward_static_call_array() are forwarding calls: they keep the caller's late static binding, unlike call_user_func() which resets static:: to the named class. Both previously fell through to the function map and returned mixed. Normalize the callable into an inner call like call_user_func() does; when it names a class and a method, resolve it as a static call on resolveTypeByName()'s result without the static-type reset, so the named class yields the caller-bound ancestor type when it is an ancestor and a plain object type otherwise — matching the runtime rule that the binding is only forwarded within the caller's own ancestry. The synthesized call is resolved directly instead of through MutatingScope::getType(), because its printed form would collide with a real, non-forwarding static call in the expression-type cache.
Root -> Middle -> Leaf (final), each with its own which() implementation carrying a distinct return type, mirroring the runtime behavior: - naming an ancestor calls the named class's implementation (overrides in the caller or below it do not re-dispatch) while the binding is forwarded, - the forwarded binding is the caller's static, collapsing to the class itself in a final class, - naming a descendant resolves to the named class's object type, which covers both runtime outcomes (binding forwarded when the runtime static is a subclass of the named class, reset to the named class otherwise), - an instance method has an active class scope to forward as well.
…able Extend CallUserFuncRule to forward_static_call() and forward_static_call_array(), reusing their argument normalization, so missing or mismatched arguments of the forwarded callable are reported like for call_user_func(). The synthesized static call's name nodes carry the original call's position attributes so the errors point at the call site. Nonexistent methods are already covered by the generic callable parameter check.
824a1e5 to
9169eae
Compare
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.
Closes phpstan/phpstan#14958
forward_static_call()/forward_static_call_array()are forwarding calls: they propagate the caller's late static binding, unlikecall_user_func()which resets it to the named class. Both previously fell through to the function map and returnedmixed, and their arguments were not checked against the callable.Type inference
The callable is normalized into an inner call the same way
call_user_func()does. When it names a class and a method ([Base::class, 'create'],'Base::create'), the synthesized static call is resolved onScope::resolveTypeByName()'s result without the static-type reset thatStaticCallHandlerapplies to explicit class names — so the named class yields the caller-bound ancestor type when it is an ancestor and a plain object type otherwise, matching the runtime rule that the binding is only forwarded within the caller's own ancestry (verified on PHP 8.5, including descendant and unrelated classes). Other callables (closures, …) resolve as before.The multi-level-hierarchy semantics are covered by tests in a separate commit: naming an ancestor calls the named class's implementation (overrides below do not re-dispatch) while the binding is forwarded, the binding collapses in a final class, instance methods forward as well, and generics resolve through the passed arguments.
One implementation note: the synthesized static call is resolved directly instead of through
MutatingScope::getType(), because its printed form would collide with a real, non-forwarding static call on the same class in the expression-type cache.Rule
CallUserFuncRuleis extended toforward_static_call*, reusing the same normalization, so missing or mismatched arguments of the forwarded callable are reported like forcall_user_func(). (A callable that is not callable at all — including a nonexistent method — was already reported by the generic parameter check.) The synthesized call's name nodes carry the original call's position attributes so the errors point at the call site.