fix(resolution): reject unrelated cross-language matches (#1986) (#2032)

* fix(resolution): stop expression-receiver calls from binding to unrelated methods

A method called on an expression receiver reached the resolver as the bare
method name, and exact-match bound it to any project method of that name:
`(await list()).map(...)` in TypeScript onto a UI adapter class's `map`
(0.9), and `v.iter().map(...)` in Rust onto the same TypeScript method
across languages (0.5).

TS/JS extraction (mirrored in the kernel): look through receiver wrappers
that keep the same object — parentheses, `!`, `as`/`satisfies`, `<T>x`,
`await` (including the grammar's `(a && b)!` parse of `a && b!`) — so
`x!.m()` is `x.m` and `(await f()).m()` is `f().m`; any other receiver with
no static type (`(a ?? b).m()`, `f().list.m()`, `arr[0].m()`) emits nothing
instead of the bare name. `this`/`super` (and chains rooted at them),
`window.*` chains and `new C().m()` keep their existing bare form.

Resolution: a bare-named call may cross a language family only through the
C ABI (a C/C++ symbol, or a free function a C/Objective-C/Swift caller
reaches, e.g. a cgo `//export`). A class member of another family, or a
function/class/variable outside that pair, is rejected as the candidate a
strategy commits to (exact-match single/best and fuzzy), never filtered out
of the pool, so no lone survivor is manufactured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(resolution): reject unrelated cross-language matches (#1986)

Name-based calls, constructors, and inheritance could select unrelated foreign-language symbols.
Apply a shared interoperability policy to selected results without manufacturing replacement matches, including JSX/Vue synthesis.
Preserve framework exports and ABI-backed calls, building on PR #1944's TS/Rust expression-receiver fixes.
Validate with both repro scripts, 529 targeted tests, kernel/wasm parity, and clean TypeScript checks.

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-27 13:40:31 +00:00
committed by GitHub
parent 17d09bf506
commit 9649ce1bd6
10 changed files with 575 additions and 86 deletions
+66 -1
View File
@@ -1179,7 +1179,10 @@ impl<'t> Walker<'t> {
.child_by_field_name("object")
.or_else(|| func.child_by_field_name("operand"))
.or_else(|| func.child_by_field_name("argument"))
.or_else(|| func.named_child(0));
.or_else(|| func.named_child(0))
// Look through `(x)`, `x!`, `(x as T)`, `(await f())`
// (mirrors peelTsJsReceiver).
.map(peel_receiver);
// Literal receivers call builtins, never project symbols (#1230).
if let Some(r) = receiver {
if is_literal_receiver(r.kind()) {
@@ -1214,6 +1217,12 @@ impl<'t> Walker<'t> {
// TreeSitterExtractor.extractCall.
let Some(inner) = self.plain_inner_callee(r) else { return };
callee_name = format!("{inner}().{method_name}");
} else if receiver.is_some_and(|r| !keeps_bare_receiver(r, self.src)) {
// An expression receiver with no static type
// (`(a ?? b).map()`, `f().list.map()`): emit nothing
// rather than the bare method name. Mirrors
// TreeSitterExtractor.extractCall.
return;
} else {
callee_name = method_name.to_string();
}
@@ -1508,3 +1517,59 @@ fn collapse_ws(s: &str) -> String {
}
out
}
/// TS/JS wrappers that keep a member call's receiver the same object
/// (mirrors TS_JS_TRANSPARENT_RECEIVER_TYPES in tree-sitter.ts).
fn is_transparent_receiver(kind: &str) -> bool {
matches!(
kind,
"parenthesized_expression"
| "non_null_expression"
| "as_expression"
| "satisfies_expression"
| "type_assertion"
| "await_expression"
)
}
/// Strip transparent wrappers off a receiver (mirrors peelTsJsReceiver),
/// including the grammar's `(a && b)!` parse of `a && b!`.
fn peel_receiver(node: Node<'_>) -> Node<'_> {
let mut cur = node;
while is_transparent_receiver(cur.kind()) {
let mut inner = if cur.kind() == "type_assertion" {
cur.named_child(cur.named_child_count().saturating_sub(1))
} else {
cur.named_child(0)
};
if cur.kind() == "non_null_expression" {
while let Some(b) = inner.filter(|n| n.kind() == "binary_expression") {
inner = b.child_by_field_name("right");
}
}
match inner {
Some(i) => cur = i,
None => break,
}
}
cur
}
/// `this` / `super`, a member chain rooted at either or at `window`, or
/// `new C()` still collapse to the bare method name (mirrors
/// keepsBareTsJsReceiver).
fn keeps_bare_receiver(node: Node<'_>, src: &str) -> bool {
let mut cur = node;
while matches!(cur.kind(), "member_expression" | "subscript_expression") {
match cur.child_by_field_name("object") {
Some(object) => cur = peel_receiver(object),
None => return false,
}
}
match cur.kind() {
"this" | "super" => true,
"identifier" => &src[cur.byte_range()] == "window",
"new_expression" => cur.id() == node.id(),
_ => false,
}
}