fix(ts): index only the JS/TS interop family for receiver typing

A corpus-wide type index let a same-named class in another language stand in for
the receiver's type, which hid from the parking branch that nothing in the JS/TS
family declares it. SFC script blocks are in the family, so a class exported from
a .vue file still answers — here and at merge time.

(cherry picked from commit 078c106e62)
This commit is contained in:
xiongjianxu authored and safishamsi committed 2026-10-13 11:15:14 +01:00
1 parent 60c2f16ea3
commit bf13b63ca4
3 files changed
+43 -7

No files matched your search

+4 -2
View File
@@ -46,8 +46,10 @@ _LANG_SUFFIXES: dict[str, frozenset[str]] = {
"swift": frozenset({".swift"}),
"php": frozenset({".php", ".phtml", ".php3", ".php4", ".php5", ".php7", ".phps"}),
# TS and JS share one key: a TS class legitimately answers a JS call site, so
# splitting them would drop every TS<->JS cross-repo call.
"typescript": frozenset({".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"}),
# splitting them would drop every TS<->JS cross-repo call. An SFC script block is
# TS/JS too, so a class exported from `.vue` answers as well.
"typescript": frozenset({".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
".vue", ".svelte", ".astro"}),
}
# A declaration owns its members through a `method` edge, except in C++, where an
+9 -5
View File
@@ -4976,8 +4976,8 @@ def _resolve_python_member_calls(
_emit_call(caller, children[0], rc)
# Every suffix the TS/JS extractors claim. The member-call resolver both activates on
# and parks by this set, so a pure-ESM (`.mjs`) repo is not silently left out.
# Every suffix the TS/JS extractors claim, so a pure-ESM (`.mjs`) repo still activates
# the member-call resolver below.
_JS_TS_SUFFIXES = (".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs")
@@ -5022,11 +5022,15 @@ def _resolve_typescript_member_calls(
contained = {e.get("target") for e in all_edges if e.get("relation") == "contains"}
# Only the JS/TS interop family declares a type this receiver can hold (SFC script
# blocks included). A same-named class in another language must neither answer nor
# hide from the parking branch that nothing local declares the name.
type_def_nids: dict[str, list[str]] = {}
node_by_id: dict[str, dict] = {}
for n in all_nodes:
node_by_id[n.get("id")] = n
if n.get("source_file") and n.get("id") in contained and _is_type_like_definition(n):
if (_lang_family(n.get("source_file")) == "jsts"
and n.get("id") in contained and _is_type_like_definition(n)):
type_def_nids.setdefault(_key(n.get("label", "")), []).append(n["id"])
method_index: dict[tuple[str, str], str] = {}
@@ -5093,8 +5097,8 @@ def _resolve_typescript_member_calls(
if not type_defs:
# Table-typed only: an uppercase receiver also matches namespace aliases and
# default imports, and node_modules is unscanned, so those are npm names.
# The suffix stands in for the `lang` tag TS raw_calls do not carry.
if not type_qualified and str(rc.get("source_file", "")).endswith(_JS_TS_SUFFIXES):
# The caller's family stands in for the `lang` tag TS raw_calls do not carry.
if not type_qualified and _lang_family(rc.get("source_file")) == "jsts":
_park_unresolved_member_call(
node_by_id.get(caller), callee, type_name, "typescript", rc,
)
+30
View File
@@ -350,3 +350,33 @@ def test_a_type_the_origin_gate_rejected_is_not_parked(tmp_path):
"}\n"),
})
assert _parked(r) == []
def test_a_class_from_another_language_neither_answers_nor_blocks_parking(tmp_path):
# The declaration index is corpus-wide, so a same-named PHP class used to make the
# receiver look locally declared and the call was dropped instead of parked.
_, r = _calls(tmp_path, {
"app.ts": ("import { Greeter } from 'greeter-pkg';\n"
"export class App {\n"
" constructor(private greeter: Greeter) {}\n"
" run(): void { this.greeter.greet(); }\n"
"}\n"),
"Greeter.php": "<?php\nclass Greeter {\n public function greet(): void {}\n}\n",
})
assert _parked(r) == [{"callee": "greet", "receiver_type": "Greeter",
"lang": "typescript", "line": "L4"}]
def test_a_class_declared_in_an_sfc_script_block_still_answers(tmp_path):
# An SFC script block is TS, so its declarations belong to the same interop family
# as the caller's and must keep resolving.
calls, _ = _calls(tmp_path, {
"app.ts": ("import { Greeter } from './Greeter.vue';\n"
"export class App {\n"
" constructor(private greeter: Greeter) {}\n"
" run(): void { this.greeter.greet(); }\n"
"}\n"),
"Greeter.vue": ("<script lang=\"ts\">\nexport class Greeter {\n"
" greet(): void {}\n}\n</script>\n"),
})
assert any("run" in str(s) and "greet" in str(t) for s, t in calls), calls