fix(ts): don't guess between members the normalized key can't tell apart

Review on #4479: after the exact-name lookup, the `_key` fallback in
_resolve_typescript_member_calls still matched loosely. With #4475 splitting
close/#close, updateHash/_updateHash and set/$set into separate nodes, one
normalized key can now map to two members, and method_index kept whichever
was registered last. So c._close() bound to #close, c.UpdateHash() to
_updateHash, and c.a() to $a, none of which is the called name. Track keys
that more than one member normalizes to and skip the fallback for them (no
edge rather than a guess). Exact hits, and loose hits with a single
candidate, are unchanged.

Co-Authored-By: Grok Bot
(cherry picked from commit a1ce057757)
This commit is contained in:
Mpasha17 authored and safishamsi committed 2026-10-16 17:11:41 +01:00
1 parent 70a600196f
commit c4589a0c1b
2 files changed
+35 -2

No files matched your search

+7 -2
View File
@@ -5048,6 +5048,8 @@ def _resolve_typescript_member_calls(
# `updateHash` / `_updateHash` are two nodes of one class, so the exact
# name is tried first and `_key` stays the fallback.
method_exact: dict[tuple[str, str], str] = {}
# Keys more than one member normalizes to: the fallback can't tell them apart.
method_key_ambiguous: set[tuple[str, str]] = set()
class_of_method: dict[str, str] = {}
for e in all_edges:
if e.get("relation") != "method":
@@ -5055,7 +5057,10 @@ def _resolve_typescript_member_calls(
src, tgt = e.get("source"), e.get("target")
tnode = node_by_id.get(tgt)
if tnode is not None:
method_index[(src, _key(tnode.get("label", "")))] = tgt
key = (src, _key(tnode.get("label", "")))
if method_index.get(key, tgt) != tgt:
method_key_ambiguous.add(key)
method_index[key] = tgt
method_exact[(src, str(tnode.get("label", "")).removesuffix("()").removeprefix("."))] = tgt
if isinstance(src, str) and isinstance(tgt, str):
class_of_method[tgt] = src
@@ -5188,7 +5193,7 @@ def _resolve_typescript_member_calls(
type_nid = visible[0]
method_nid = method_exact.get((type_nid, str(callee)))
exact = method_nid is not None
if not exact:
if not exact and (type_nid, _key(callee)) not in method_key_ambiguous:
method_nid = method_index.get((type_nid, _key(callee)))
if not method_nid:
# Receiver typed, but the type has no such method. The old fallback
+28
View File
@@ -99,6 +99,34 @@ def test_cross_file_ts_typed_receiver_hits_each_member(tmp_path):
assert ("b()", ".$set()") in calls and ("b()", ".set()") not in calls
@pytest.mark.parametrize("members,call", [
(" close(): void {}\n #close(): void {}\n", "c._close()"),
(" updateHash(): void {}\n _updateHash(): void {}\n", "c.$updateHash()"),
(" updateHash(): void {}\n _updateHash(): void {}\n", "c.UpdateHash()"),
(" set(v: number): void {}\n $set(v: number): void {}\n", "c._set(1)"),
(" _a(): void {}\n $a(): void {}\n", "c.a()"),
])
def test_cross_file_call_matching_no_member_exactly_is_not_guessed(tmp_path, members, call):
# Review on #4479: neither member has the called name, and the loose key fits
# both, so picking one would be a guess (it was whichever registered last).
r = _extract(tmp_path, {
"ctx.ts": "export class Ctx {\n" + members + "}\n",
"use.ts": ('import { Ctx } from "./ctx";\n'
"export function go(c: Ctx) { " + call + "; }\n"),
})
assert not [t for s, t in _calls(r) if s == "go()"], _calls(r)
def test_cross_file_loose_match_with_a_single_candidate_is_unchanged(tmp_path):
# One member per key still resolves loosely, as on v8 (not touched here).
r = _extract(tmp_path, {
"ctx.ts": "export class Ctx {\n _close(): void {}\n}\n",
"use.ts": ('import { Ctx } from "./ctx";\n'
"export function go(c: Ctx) { c.close(); }\n"),
})
assert ("go()", "._close()") in _calls(r)
def test_dedup_build_and_merge_keep_both(tmp_path):
from graphify.build import build, build_merge
from graphify.cluster import cluster