fix(ts): keep a public call's edge when foo and #foo share one node

The extractor gives a class's `foo()` and `#foo()` the same node id, labeled after
whichever is declared first. Skipping a `c.foo()` match whose label starts with `#`
therefore dropped a real edge when the private member came first (hono's
`c.newResponse()`, whose public arrow property has no node of its own). A call
written `c.foo()` can only mean a public member, so keep the edge to that node.
Only the first commit of #4233 shipped in 0.9.81; this is its second one.

Covered for both declaration orders. vs 0.9.84: hono +3 edges, nothing removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
This commit is contained in:
rohit-jsfreakyandClaude Opus 5.5 authored and safishamsi committed 2026-10-12 13:01:47 +01:00
1 parent afd4fa55c6
commit ad88f5bc70
2 files changed
+15 -18

No files matched your search

-6
View File
@@ -4669,12 +4669,6 @@ def _resolve_typescript_member_calls(
# (a `references` edge to the type node) was another fabrication
# vector; skip instead, matching the C# resolver.
continue
# _key() drops the `#`, so `c.newResponse()` also keys onto a private
# `#newResponse()`. A `#name` is only reachable as `#name`; never bind
# across that difference.
method_name = str(node_by_id.get(method_nid, {}).get("label", "")).lstrip(".")
if method_name.startswith("#") != str(callee).startswith("#"):
continue
if method_nid == caller or (caller, method_nid) in existing_pairs:
continue
existing_pairs.add((caller, method_nid))
+15 -12
View File
@@ -255,15 +255,18 @@ def test_same_named_type_the_import_cannot_decide_emits_no_edge(tmp_path):
assert not _hits(calls, "neither")
def test_a_public_call_never_binds_to_a_private_hash_method(tmp_path):
# `_key()` folds `#newResponse` and `newResponse` together; a `#name` is only
# reachable as `#name`, so `c.newResponse()` must not land on it.
calls, _ = _calls(tmp_path, {
"ctx.ts": ("export class Ctx {\n"
" #newResponse(): number { return 1; }\n"
" build(): number { return this.#newResponse(); }\n"
"}\n"),
"use.ts": ('import { Ctx } from "./ctx";\n'
"export function make(c: Ctx): number { return c.newResponse(); }\n"),
})
assert not _hits(calls, "make", "newResponse")
def test_public_call_reaches_the_member_node_in_either_declaration_order(tmp_path):
# `foo()` and `#foo()` share one node (ids drop `#`), labeled after whichever is
# declared first. `c.foo()` can only mean the public one, so it must keep its
# edge to that node in both orders.
orders = (
" foo(): number { return 1; }\n #foo(): number { return 2; }\n",
" #foo(): number { return 2; }\n foo(): number { return 1; }\n",
)
for i, members in enumerate(orders):
calls, _ = _calls(tmp_path / f"order{i}", {
"ctx.ts": "export class Ctx {\n" + members + "}\n",
"use.ts": ('import { Ctx } from "./ctx";\n'
"export function make(c: Ctx): number { return c.foo(); }\n"),
})
assert any("make" in str(s) and "foo" in str(t) for s, t in calls), (i, calls)