mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 00:38:18 +08:00
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user