fix(swift): a call through a prefix operator, try or await keeps its callee and receiver (#2686)

tree-sitter-swift binds a prefix operator to the leftmost operand:
`!isReady()` parses as `(!isReady)()`, and `!store.contains(id)` as
`(!store).contains(id)`. `try` and `await` before a chain bind to the part
before its last call: `try! Decoder().decode(d)` parses as
`(try! Decoder()).decode(d)`. Both engines read the callee or receiver node
as written, so the call was named `!isReady`, which never resolves, or it
lost its receiver and name-matched whichever `contains` or `decode` came
first; a type path behind a `!` lost its path too.

Both walkers now strip these wrappers (a prefix operator other than an
implicit member's `.`, `try` and `await`) off a call's callee and receiver.
They read a receiver chain's text from its innermost wrapped operand on, for
the chained-call encoding and the type-path receiver, and look through a
prefix operator for the type a member read references: `!Config.shared.isOn`
references `Config`. An operator applied to a parenthesized value, `!(x)` or
`-(a + b)`, parses as a call of the operator and now emits no call.

swift-wrapped-call-receivers.test.ts checks the edges, and
TortureWrappedCalls.swift pins both engines' refs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 22:31:31 +00:00
committed by GitHub
parent 951d575e62
commit 3308cff071
6 changed files with 382 additions and 15 deletions
+71 -4
View File
@@ -128,6 +128,57 @@ fn is_type_path(s: &str) -> bool {
segments >= 2
}
/// isSwiftCallWrapper (tree-sitter.ts): a wrapper that leaves a call's target
/// the same — `try`/`await`, which before a chain bind to the part before its
/// last call (`try! A().b()` is `(try! A()).b()`), or a prefix operator, which
/// binds to the leftmost operand (`!isReady()` is `(!isReady)()`). An implicit
/// member's leading `.` (`.success(x)`) is part of the name.
fn is_call_wrapper(node: Node, src: &str) -> bool {
match node.kind() {
"try_expression" | "await_expression" => true,
"prefix_expression" => !src[node.byte_range()].starts_with('.'),
_ => false,
}
}
/// swiftWrappedOperand: the operand a call wrapper wraps.
fn wrapped_operand<'t>(node: Node<'t>) -> Option<Node<'t>> {
node.child_by_field_name(if node.kind() == "prefix_expression" { "target" } else { "expr" })
}
/// peelSwiftCallWrappers: strip call wrappers off a callee or receiver.
fn peel_call_wrappers<'t>(node: Node<'t>, src: &str) -> Node<'t> {
let mut cur = node;
while is_call_wrapper(cur, src) {
match wrapped_operand(cur) {
Some(inner) => cur = inner,
None => break,
}
}
cur
}
/// swiftUnwrappedText: a receiver's text from its innermost wrapped operand
/// on — the wrappers sit on the chain's left edge, so `!Settings.Defaults`
/// reads `Settings.Defaults` and `try! Foo.make().bar` reads `Foo.make().bar`.
fn unwrapped_text<'s>(node: Node, src: &'s str) -> &'s str {
let mut start = node.start_byte();
let mut cur = Some(node);
while let Some(n) = cur {
if is_call_wrapper(n, src) {
cur = wrapped_operand(n);
if let Some(inner) = cur {
start = inner.start_byte();
}
} else if matches!(n.kind(), "navigation_expression" | "call_expression") {
cur = n.named_child(0);
} else {
break;
}
}
&src[start..node.end_byte()]
}
struct Scope {
row: u32,
kind: &'static str,
@@ -1023,6 +1074,13 @@ impl<'t> Walker<'t> {
.child_by_field_name("function")
.or_else(|| node.named_child(0));
let Some(func) = func else { return };
// `-(a + b)`: an operator token applied to a parenthesized value leaves
// the call with no callee node, only its arguments (tree-sitter.ts).
// `!(x)` keeps its `bang` callee: a project may declare `prefix func !`.
if func.kind() == "call_suffix" {
return;
}
let func = peel_call_wrappers(func, self.src);
let mut callee_name = String::new();
if func.kind() == "navigation_expression" {
@@ -1043,11 +1101,14 @@ impl<'t> Walker<'t> {
});
if let Some(property) = property {
let method_name = self.text(property);
// Through a prefix operator, `try` and `await`:
// `(!store).contains(x)`, `(try! A()).b()`.
let receiver = func
.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))
.map(|r| peel_call_wrappers(r, self.src));
if let Some(r) = receiver {
if is_literal_receiver(r.kind()) {
return; // `"lit".upper()` / `5.times()` — nothing
@@ -1066,9 +1127,11 @@ impl<'t> Walker<'t> {
} else if receiver.map(|r| r.kind() == "call_expression").unwrap_or(false) {
// #750 swift re-encode: innerNav = receiver.namedChild(0),
// ws-stripped; capitalized chains only.
// Without a wrapper on its left edge
// (`try! Foo.make().bar().baz()`).
let inner = receiver.unwrap().named_child(0);
let inner_callee =
inner.map(|n| strip_js_ws(self.text(n))).unwrap_or_default();
inner.map(|n| strip_js_ws(unwrapped_text(n, self.src))).unwrap_or_default();
let reencode = inner_callee
.as_bytes()
.first()
@@ -1081,11 +1144,12 @@ impl<'t> Walker<'t> {
};
} else if let Some(path) = receiver
.filter(|r| r.kind() == "navigation_expression")
.map(|r| strip_js_ws(self.text(r)))
.map(|r| strip_js_ws(unwrapped_text(r, self.src)))
.filter(|p| is_type_path(p))
{
// A type path, `API.PackageController.GetRoute.query(on:)`:
// keep it for the resolver (tree-sitter.ts SWIFT_TYPE_PATH_RECEIVER).
// keep it for the resolver (tree-sitter.ts SWIFT_TYPE_PATH_RECEIVER),
// without a prefix operator on its head (`!Settings.Defaults.isOn()`).
callee_name = format!("{path}.{method_name}");
} else {
// self_expression / super_expression / instance nav /
@@ -1138,6 +1202,9 @@ impl<'t> Walker<'t> {
.or_else(|| node.child_by_field_name("scope"))
.or_else(|| node.named_child(0));
let Some(recv) = recv else { return };
// A prefix operator binds to the head: `!Config.shared.isOn` is
// `(!Config).shared.isOn` (tree-sitter.ts).
let recv = peel_call_wrappers(recv, self.src);
if matches!(
recv.kind(),
"identifier" | "type_identifier" | "simple_identifier" | "name" | "scoped_type_identifier"