mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 16:39:43 +00:00
fix(php): route ALL scoped calls through the PHP resolver, not just uppercase receivers
engine.py's defer guard only deferred a scoped_call_expression into raw_calls
when member_receiver[:1].isupper() (or is_this_field_call/_csharp_defer), so
self::, static::, parent::, $var::, and lowercase-first class receivers fell
through to the pre-existing per-file bare-name path instead of reaching
_resolve_php_member_calls. That path is last-write-wins across the whole file
and can mis-bind a same-named method on an unrelated class.
Tag scoped_call_expression with is_scoped_call and add it to the defer
condition unconditionally, so every `::` call is deferred to raw_calls and
resolved by the dedicated resolver regardless of receiver casing or sigil.
Also tighten the self::/static:: and $var:: fixtures with a same-named decoy
method on another class, so they actually prove enclosing-class scoping
and no-fabrication instead of passing on a bare-name coincidence.
Addresses review feedback on PR #3874.
(cherry picked from commit 801c2a2dfd)
This commit is contained in:
1 parent
b0fd76a61c
commit
c7bc5fe4c0
2 files changed
+40
-8
No files matched your search
@@ -7689,6 +7689,7 @@ def _extract_generic(
|
||||
# self): kept apart from member_receiver, which feeds the
|
||||
# receiver-typed resolvers and raw_calls.
|
||||
self_receiver: str | None = None
|
||||
is_scoped_call: bool = False
|
||||
swift_receiver: str | None = None
|
||||
member_receiver: str | None = None
|
||||
kotlin_qualified_prefix: str | None = None
|
||||
@@ -8014,6 +8015,7 @@ def _extract_generic(
|
||||
# qualified-call branch here, so a dedicated PHP resolver can
|
||||
# bind it to the actual method on that class.
|
||||
is_member_call = True
|
||||
is_scoped_call = True
|
||||
name_node = node.child_by_field_name("name")
|
||||
if name_node:
|
||||
callee_name = _read_text(name_node, source)
|
||||
@@ -8264,7 +8266,7 @@ def _extract_generic(
|
||||
and is_member_call
|
||||
and not lua_self_qualified
|
||||
)
|
||||
if _python_defer or _java_defer or (_builtin_member_call and not php_builtin_self_call) or _lua_member_defer or (
|
||||
if _python_defer or _java_defer or (_builtin_member_call and not php_builtin_self_call) or _lua_member_defer or is_scoped_call or (
|
||||
is_member_call
|
||||
and member_receiver
|
||||
and (
|
||||
|
||||
@@ -48,16 +48,40 @@ def test_same_namespace_static_call_is_extracted_not_inferred(tmp_path):
|
||||
|
||||
|
||||
def test_self_and_static_scope_resolve_to_enclosing_class(tmp_path):
|
||||
calls, _ = _extract(tmp_path, {"Widget.php": (
|
||||
"<?php\nnamespace App;\nclass Widget {\n"
|
||||
" public static function build() { return self::helper(); }\n"
|
||||
" public static function render() { return static::helper(); }\n"
|
||||
" public static function helper() { return 1; }\n}\n")})
|
||||
"""A same-named decoy method on an unrelated class proves `self::`/
|
||||
`static::` bind to the ENCLOSING class, not to any node sharing the bare
|
||||
method name (review finding on PR #3874: without a decoy, a bare-name
|
||||
coincidence would pass this test even if self/static were never scoped)."""
|
||||
calls, _ = _extract(tmp_path, {
|
||||
"Widget.php": (
|
||||
"<?php\nnamespace App;\nclass Widget {\n"
|
||||
" public static function build() { return self::helper(); }\n"
|
||||
" public static function render() { return static::helper(); }\n"
|
||||
" public static function helper() { return 1; }\n}\n"),
|
||||
"Decoy.php": (
|
||||
"<?php\nnamespace App;\nclass Decoy {\n"
|
||||
" public static function helper() { return 2; }\n}\n"),
|
||||
})
|
||||
assert (".build()", ".helper()") in calls
|
||||
assert (".render()", ".helper()") in calls
|
||||
assert calls[(".build()", ".helper()")]["confidence"] == "EXTRACTED"
|
||||
|
||||
|
||||
def test_self_scope_does_not_resolve_to_other_class(tmp_path):
|
||||
"""`self::helper()` must NOT bind to another class's `helper()` even when
|
||||
the enclosing class has no method of that name - `self::` names the
|
||||
enclosing class, not a bare method-name match anywhere in the corpus."""
|
||||
calls, _ = _extract(tmp_path, {
|
||||
"A.php": (
|
||||
"<?php\nnamespace App;\nclass A {\n"
|
||||
" public function build() { return self::helper(); }\n}\n"),
|
||||
"B.php": (
|
||||
"<?php\nnamespace App;\nclass B {\n"
|
||||
" public function helper() { return 1; }\n}\n"),
|
||||
})
|
||||
assert not any(s == ".build()" for s, _t in calls)
|
||||
|
||||
|
||||
def test_parent_scope_resolves_through_inheritance(tmp_path):
|
||||
calls, _ = _extract(tmp_path, {
|
||||
"Base.php": (
|
||||
@@ -72,10 +96,16 @@ def test_parent_scope_resolves_through_inheritance(tmp_path):
|
||||
|
||||
def test_dynamic_scope_produces_no_phantom_edge(tmp_path):
|
||||
"""`$cls::method()` has no declared-type table to resolve against - it
|
||||
must stay unresolved rather than guess and mint a wrong edge."""
|
||||
must stay unresolved rather than guess and mint a wrong edge. A same-file,
|
||||
same-named decoy method proves the resolver actually ran and declined to
|
||||
bind, rather than the guard never being exercised (review finding on PR
|
||||
#3874: without the decoy, the old bare-name path would silently produce
|
||||
the same "no edge to a DIFFERENT-labeled node" result for the wrong
|
||||
reason)."""
|
||||
calls, r = _extract(tmp_path, {"Caller.php": (
|
||||
"<?php\nnamespace App;\nclass Caller {\n"
|
||||
" public function run($cls) { return $cls::helper(); }\n}\n")})
|
||||
" public function run($cls) { return $cls::helper(); }\n"
|
||||
" public function helper() { return 1; }\n}\n")})
|
||||
assert not any(s == ".run()" for s, _t in calls)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user