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
+1
View File
@@ -201,6 +201,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In JavaScript and TypeScript projects, a folder named `build` inside `src/` is now indexed for its TypeScript, JSX, Vue, Svelte and Astro files, so a feature kept there, like the `src/build/` pages of luci-go's milo, shows up in `codegraph_explore` and the routes that lazily load its pages link to them. Before, every folder named `build` was skipped as build output. The JavaScript, declaration and other files such a folder holds are still left out, since a build may have written them, and so is every `build` folder outside `src/`. - In JavaScript and TypeScript projects, a folder named `build` inside `src/` is now indexed for its TypeScript, JSX, Vue, Svelte and Astro files, so a feature kept there, like the `src/build/` pages of luci-go's milo, shows up in `codegraph_explore` and the routes that lazily load its pages link to them. Before, every folder named `build` was skipped as build output. The JavaScript, declaration and other files such a folder holds are still left out, since a build may have written them, and so is every `build` folder outside `src/`.
- On Windows, a Swift call written after `try!`, like `try! Message2(data: data)` or `try! Settings.Defaults.load()`, now links to what it calls, as it already did on macOS and Linux. Before, the `!` was read as a logical not, so the call linked to nothing, and a call made through a type lost the type it was made on. Re-index Swift projects on Windows after upgrading. - On Windows, a Swift call written after `try!`, like `try! Message2(data: data)` or `try! Settings.Defaults.load()`, now links to what it calls, as it already did on macOS and Linux. Before, the `!` was read as a logical not, so the call linked to nothing, and a call made through a type lost the type it was made on. Re-index Swift projects on Windows after upgrading.
- In JavaScript and TypeScript, a barrel's `export { A, B as C } from './x'` now links each name it passes on to what `./x` exports, following barrels that pass it on in turn. Before, each name was looked up by itself and could link to a same-named symbol elsewhere in the project, such as another folder's `Provider`, and a name passed on from a package, like `export { debounce } from 'lodash'`, could link to a project function of that name. Re-index JavaScript and TypeScript projects after upgrading. - In JavaScript and TypeScript, a barrel's `export { A, B as C } from './x'` now links each name it passes on to what `./x` exports, following barrels that pass it on in turn. Before, each name was looked up by itself and could link to a same-named symbol elsewhere in the project, such as another folder's `Provider`, and a name passed on from a package, like `export { debounce } from 'lodash'`, could link to a project function of that name. Re-index JavaScript and TypeScript projects after upgrading.
- In Swift, a call written after `!` or another prefix operator, or chained after `try` or `await`, now links to what it calls. `if !isReady()` links to `isReady`, `!store.contains(id)` and `try! JSONDecoder().decode(…)` link to the method of the type they're called on rather than to whichever `contains` or `decode` came first, and `!Settings.Defaults.isOn()` lands on the type it names. Re-index Swift projects after upgrading.
## [1.6.2] - 2026-10-03 ## [1.6.2] - 2026-10-03
@@ -0,0 +1,49 @@
// Calls whose callee or receiver the grammar wraps. tree-sitter-swift binds a
// prefix operator to the leftmost operand, so `!isReady()` parses as
// `(!isReady)()` and `!store.contains(x)` as `(!store).contains(x)`; `try` and
// `await` before a chain bind to its first call, so `try! Decoder().decode(d)`
// parses as `(try! Decoder()).decode(d)`. None of them changes what is called.
// An operator applied to a parenthesized value parses as a call of the
// operator: `!(x)` calls `!` (a project may declare `prefix func !`), and
// `-(x)` has no callee node at all.
import Foundation
let globalFlag = !Config.isEnabled()
final class Screen {
static let ready = !Config.shared.isReady()
let store = Store()
var negated: Bool = !Store.isEmpty()
func update(items: [Item], data: Data, id: Int) async throws -> Bool {
let a = !isReady()
let b = !store.contains(id)
let c = !Config.check()
let d = -abs(id)
let e = !Settings.Defaults.isOn()
let f = !Settings.Defaults.shared.isOn()
let g = !self.isCancelled
let h = !Config.shared.isEnabled
let i = ~Mask.all.rawValue
let j = try! Decoder().decode(Item.self, from: data)
let k = try Parser.make().parse(data)
let l = try! Parser.make().tokens().first()
let m = try await Client.shared.fetch(id).items()
let n = await loader.load().items()
let o = try !isValid(data)
let p: Result<Int, Error> = .success(id)
let q = !(items.isEmpty)
let r = -(id + 1)
let s = -(id)
if !items.contains(where: { $0.id == id }) {
return !store.remove(id)
}
guard !Task.isCancelled, !isReady() else { return false }
_ = (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s)
return !Config.check() && -Offset.zero() < 0
}
func isReady() -> Bool { true }
func isValid(_ d: Data) throws -> Bool { true }
var isCancelled: Bool { false }
}
+51 -1
View File
@@ -18,7 +18,9 @@
* assignment-prune case the swift-nio sweep caught, SWIFT_SPEC fn-refs with * assignment-prune case the swift-nio sweep caught, SWIFT_SPEC fn-refs with
* the label-forward skip and #selector shapes) and its CRLF variant (derived * the label-forward skip and #selector shapes) and its CRLF variant (derived
* in-memory — #1329); TortureTry.swift: calls written after `try!`, `try?` * in-memory — #1329); TortureTry.swift: calls written after `try!`, `try?`
* and `try`, which the Windows kernels' grammar build misparsed. * and `try`, which the Windows kernels' grammar build misparsed;
* TortureWrappedCalls.swift: calls through a prefix operator, `try` or
* `await`, which the grammar wraps around the callee or receiver.
* *
* The full-repo sweep lives in scripts/kernel-parity.mjs (Alamofire/vapor/ * The full-repo sweep lives in scripts/kernel-parity.mjs (Alamofire/vapor/
* swift-nio, --max-deferral 0.3 — swift error incidence is structurally * swift-nio, --max-deferral 0.3 — swift error incidence is structurally
@@ -172,6 +174,54 @@ describe.skipIf(!kernelBuilt)('kernel Swift extraction parity', () => {
expect(refs.filter((r) => tryBangLines.has(r.line) && r.referenceName.includes('!'))).toEqual([]); expect(refs.filter((r) => tryBangLines.has(r.line) && r.referenceName.includes('!'))).toEqual([]);
}); });
// A call the grammar wraps. A prefix operator binds to the leftmost operand
// (`!isReady()` parses as `(!isReady)()`, `!store.contains(id)` as
// `(!store).contains(id)`), and `try`/`await` before a chain to the part
// before its last call (`try! Decoder().decode(d)` is
// `(try! Decoder()).decode(d)`). Both engines read through the wrappers; an
// implicit member (`.success(x)`) keeps its dot. An operator applied to a
// parenthesized value parses as a call of the operator: `!(x)` stays a call
// of `!`, and `-(x)`, which has no callee node, is no call (its argument
// text used to be read as one: `-(id)` called `id`).
it('a call through a prefix operator, try or await keeps its name and receiver, LF and CRLF', () => {
const source = fs.readFileSync(path.join(FIXTURE_DIR, 'TortureWrappedCalls.swift'), 'utf8');
assertParity('fixtures/TortureWrappedCalls.swift', source, 5);
assertParity('fixtures/TortureWrappedCalls.swift (crlf)', source.replace(/(?<!\r)\n/g, '\r\n'), 5);
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
const refs = tryKernelExtract('fixtures/TortureWrappedCalls.swift', source, 'swift')!.unresolvedReferences;
const lines = source.split(/\r?\n/);
// The `kind` refs on the first line containing `text`, by name.
const on = (text: string, kind: 'calls' | 'references'): string[] => {
const i = lines.findIndex((l) => l.includes(text));
expect(i, `fixture line "${text}"`).toBeGreaterThanOrEqual(0);
return refs.filter((r) => r.line === i + 1 && r.referenceKind === kind).map((r) => r.referenceName).sort();
};
expect(on('let a = !isReady()', 'calls')).toEqual(['isReady']);
expect(on('let b = !store.contains(id)', 'calls')).toEqual(['store.contains']);
expect(on('let c = !Config.check()', 'calls')).toEqual(['Config.check']);
expect(on('let d = -abs(id)', 'calls')).toEqual(['abs']);
expect(on('let e = !Settings.Defaults.isOn()', 'calls')).toEqual(['Settings.Defaults.isOn']);
expect(on('let e = !Settings.Defaults.isOn()', 'references')).toEqual(['Settings']);
expect(on('let h = !Config.shared.isEnabled', 'references')).toEqual(['Config']);
expect(on('let i = ~Mask.all.rawValue', 'references')).toEqual(['Mask']);
expect(on('var negated: Bool = !Store.isEmpty()', 'calls')).toEqual(['Store.isEmpty']);
expect(on('let j = try! Decoder()', 'calls')).toEqual(['Decoder', 'Decoder().decode']);
expect(on('let k = try Parser.make()', 'calls')).toEqual(['Parser.make', 'Parser.make().parse']);
expect(on('let l = try! Parser.make().tokens()', 'calls'))
.toEqual(['Parser.make', 'Parser.make().tokens', 'Parser.make().tokens().first']);
expect(on('let o = try !isValid(data)', 'calls')).toEqual(['isValid']);
expect(on('let q = !(items.isEmpty)', 'calls')).toEqual(['!']);
expect(on('let r = -(id + 1)', 'calls')).toEqual([]);
expect(on('let s = -(id)', 'calls')).toEqual([]);
expect(on('return !store.remove(id)', 'calls')).toEqual(['store.remove']);
expect(on('guard !Task.isCancelled', 'calls')).toEqual(['isReady']);
expect(on('guard !Task.isCancelled', 'references')).toEqual(['Task']);
// Unchanged: an implicit member keeps its dot, a lowercase chain stays bare.
expect(on('let p: Result<Int, Error> = .success(id)', 'calls')).toEqual(['.success']);
expect(on('let n = await loader.load().items()', 'calls')).toEqual(['items', 'loader.load']);
});
it('files with parse errors defer to the wasm extractor (recovery is encoding-dependent)', () => { it('files with parse errors defer to the wasm extractor (recovery is encoding-dependent)', () => {
// A NEW-only regression construct (`#if` between enum cases — the swift // A NEW-only regression construct (`#if` between enum cases — the swift
// checklist's grammar-bump delta 5) — errors on the 0.7.3 grammar. // checklist's grammar-bump delta 5) — errors on the 0.7.3 grammar.
@@ -0,0 +1,126 @@
/**
* A Swift call keeps its name and receiver through a prefix operator, `try`
* and `await`.
*
* 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! Reader().read(s)` is `(try! Reader()).read(s)`.
* The call came out named `!isReady`, which links nothing, or lost its
* receiver and name-matched whichever `contains` came first. Every target
* here has a namesake on another type, so a guess can't pass.
*/
import { describe, it, expect, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const roots: string[] = [];
afterAll(() => {
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
});
const FILES: Record<string, string> = {
'Sources/App/Store.swift': `final class Store {
func contains(_ id: Int) -> Bool { false }
}
final class Bag {
func contains(_ id: Int) -> Bool { true }
}
`,
'Sources/App/Config.swift': `enum Config {
static func check() -> Bool { true }
static func offset() -> Int { 1 }
}
enum Audit {
static func check() -> Bool { false }
static func offset() -> Int { 2 }
}
`,
'Sources/App/Settings.swift': `enum Settings {
enum Defaults {
static func isOn() -> Bool { true }
}
}
enum Flags {
enum Defaults {
static func isOn() -> Bool { false }
}
}
`,
'Sources/App/Parser.swift': `final class Parser {
static func make() -> Parser { Parser() }
func parse(_ s: String) throws -> Int { 0 }
}
final class Lexer {
func parse(_ s: String) throws -> Int { 1 }
}
final class Reader {
func read(_ s: String) throws -> Int { 0 }
}
final class Tape {
func read(_ s: String) throws -> Int { 1 }
}
final class Loader {
static func shared() -> Loader { Loader() }
func fetch(_ s: String) async throws -> Int { 0 }
}
final class Cache {
func fetch(_ s: String) async throws -> Int { 1 }
}
`,
'Sources/App/Screen.swift': `final class Screen {
let store = Store()
func isReady() -> Bool { true }
func one() -> Bool { !isReady() }
func two(_ id: Int) -> Bool { !store.contains(id) }
func three() -> Bool { !Config.check() }
func four() -> Int { -Config.offset() }
func five() -> Bool { !Settings.Defaults.isOn() }
func six(_ s: String) throws -> Int { try Parser.make().parse(s) }
func seven(_ s: String) -> Int { try! Reader().read(s) }
func eight(_ s: String) async throws -> Int { try await Loader.shared().fetch(s) }
func nine() -> Bool { !(isReady()) }
}
`,
};
describe('Swift: a call through a prefix operator, try or await', () => {
it('keeps its name and receiver', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-swift-wrapped-'));
roots.push(root);
for (const [rel, content] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
const cg = await CodeGraph.init(root, { index: true });
try {
const reached = (caller: string): string[] => {
const from = cg.getNodesByName(caller).find((n) => n.kind === 'method');
expect(from, caller).toBeDefined();
return cg
.getOutgoingEdgesFrom([from!.id], ['calls', 'instantiates'])
.map((e) => cg.getNode(e.target))
.filter((n) => n && n.language === 'swift')
.map((n) => n!.qualifiedName)
.sort();
};
// `!isReady()` was a call of `!isReady`.
expect(reached('one')).toEqual(['Screen::isReady']);
// Through the receiver's type, not the first `contains`/`check`/`isOn`.
expect(reached('two')).toEqual(['Store::contains']);
expect(reached('three')).toEqual(['Config::check']);
expect(reached('four')).toEqual(['Config::offset']);
expect(reached('five')).toEqual(['Settings::Defaults::isOn']);
// A chain after `try`, `try!` and `try await`: on what the first call returns.
expect(reached('six')).toEqual(['Parser::make', 'Parser::parse']);
expect(reached('seven')).toEqual(['Reader', 'Reader::read']);
expect(reached('eight')).toEqual(['Loader::fetch', 'Loader::shared']);
// A negated parenthesized call reaches the call inside.
expect(reached('nine')).toEqual(['Screen::isReady']);
} finally {
cg.close();
}
});
});
+71 -4
View File
@@ -128,6 +128,57 @@ fn is_type_path(s: &str) -> bool {
segments >= 2 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 { struct Scope {
row: u32, row: u32,
kind: &'static str, kind: &'static str,
@@ -1023,6 +1074,13 @@ impl<'t> Walker<'t> {
.child_by_field_name("function") .child_by_field_name("function")
.or_else(|| node.named_child(0)); .or_else(|| node.named_child(0));
let Some(func) = func else { return }; 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(); let mut callee_name = String::new();
if func.kind() == "navigation_expression" { if func.kind() == "navigation_expression" {
@@ -1043,11 +1101,14 @@ impl<'t> Walker<'t> {
}); });
if let Some(property) = property { if let Some(property) = property {
let method_name = self.text(property); let method_name = self.text(property);
// Through a prefix operator, `try` and `await`:
// `(!store).contains(x)`, `(try! A()).b()`.
let receiver = func let receiver = func
.child_by_field_name("object") .child_by_field_name("object")
.or_else(|| func.child_by_field_name("operand")) .or_else(|| func.child_by_field_name("operand"))
.or_else(|| func.child_by_field_name("argument")) .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 let Some(r) = receiver {
if is_literal_receiver(r.kind()) { if is_literal_receiver(r.kind()) {
return; // `"lit".upper()` / `5.times()` — nothing 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) { } else if receiver.map(|r| r.kind() == "call_expression").unwrap_or(false) {
// #750 swift re-encode: innerNav = receiver.namedChild(0), // #750 swift re-encode: innerNav = receiver.namedChild(0),
// ws-stripped; capitalized chains only. // 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 = receiver.unwrap().named_child(0);
let inner_callee = 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 let reencode = inner_callee
.as_bytes() .as_bytes()
.first() .first()
@@ -1081,11 +1144,12 @@ impl<'t> Walker<'t> {
}; };
} else if let Some(path) = receiver } else if let Some(path) = receiver
.filter(|r| r.kind() == "navigation_expression") .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)) .filter(|p| is_type_path(p))
{ {
// A type path, `API.PackageController.GetRoute.query(on:)`: // 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}"); callee_name = format!("{path}.{method_name}");
} else { } else {
// self_expression / super_expression / instance nav / // 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.child_by_field_name("scope"))
.or_else(|| node.named_child(0)); .or_else(|| node.named_child(0));
let Some(recv) = recv else { return }; 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!( if matches!(
recv.kind(), recv.kind(),
"identifier" | "type_identifier" | "simple_identifier" | "name" | "scoped_type_identifier" "identifier" | "type_identifier" | "simple_identifier" | "name" | "scoped_type_identifier"
+84 -10
View File
@@ -455,6 +455,57 @@ const THIS_FIELD_PROPERTY_TYPES = new Set(['property_identifier', 'private_prope
/** A Swift receiver that is a path of types, `API.PackageController.GetRoute` — two segments or more, each capitalized. */ /** A Swift receiver that is a path of types, `API.PackageController.GetRoute` — two segments or more, each capitalized. */
const SWIFT_TYPE_PATH_RECEIVER = /^(?!Self\.)[A-Z]\w*(?:\.[A-Z]\w*)+$/; const SWIFT_TYPE_PATH_RECEIVER = /^(?!Self\.)[A-Z]\w*(?:\.[A-Z]\w*)+$/;
/**
* Swift wrappers that leave a call's target the same. tree-sitter-swift binds
* a prefix operator to the leftmost operand, so `!isReady()` parses as
* `(!isReady)()` and `!store.contains(x)` as `(!store).contains(x)`; and
* `try`/`await` before a chain bind to the part before its last call, so
* `try! A().b()` parses as `(try! A()).b()`. An implicit member's leading `.`
* (`.success(x)`) is part of the name, not a wrapper. Mirrored in the kernel's
* swift.rs.
*/
function isSwiftCallWrapper(node: SyntaxNode, source: string): boolean {
if (node.type === 'try_expression' || node.type === 'await_expression') return true;
return node.type === 'prefix_expression' && !getNodeText(node, source).startsWith('.');
}
/** The operand a {@link isSwiftCallWrapper} wraps. */
function swiftWrappedOperand(node: SyntaxNode): SyntaxNode | null {
return getChildByField(node, node.type === 'prefix_expression' ? 'target' : 'expr');
}
/** Strip {@link isSwiftCallWrapper} wrappers off a Swift callee or receiver. */
function peelSwiftCallWrappers(node: SyntaxNode, source: string): SyntaxNode {
let cur = node;
while (isSwiftCallWrapper(cur, source)) {
const inner = swiftWrappedOperand(cur);
if (!inner) break;
cur = inner;
}
return cur;
}
/**
* A Swift 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`.
*/
function swiftUnwrappedText(node: SyntaxNode, source: string): string {
let start = node.startIndex;
let cur: SyntaxNode | null = node;
while (cur) {
if (isSwiftCallWrapper(cur, source)) {
cur = swiftWrappedOperand(cur);
if (cur) start = cur.startIndex;
} else if (cur.type === 'navigation_expression' || cur.type === 'call_expression') {
cur = cur.namedChild(0);
} else {
break;
}
}
return source.substring(start, node.endIndex);
}
/** /**
* Identifier-rooted member chains have no inferred property type (#1566), * Identifier-rooted member chains have no inferred property type (#1566),
* including host API chains (#1707). Keep the existing `window.MyNamespace` * including host API chains (#1707). Keep the existing `window.MyNamespace`
@@ -5347,7 +5398,17 @@ export class TreeSitterExtractor {
} }
} }
} else { } else {
const func = getChildByField(node, 'function') || node.namedChild(0); let func = getChildByField(node, 'function') || node.namedChild(0);
if (func && this.language === 'swift') {
// `-(a + b)`: an operator token applied to a parenthesized value
// leaves the call with no callee node, only its arguments — which
// must not be read as a callee name. (`!(x)` keeps its `bang` callee:
// a project may declare a `prefix func !` for its own types.)
// Mirrored in the kernel's extract_call (swift.rs).
if (func.type === 'call_suffix') return;
func = peelSwiftCallWrappers(func, this.source);
}
// C++ explicit operator call `a.operator+(b)` / `p->operator+(b)` (#1247): // C++ explicit operator call `a.operator+(b)` / `p->operator+(b)` (#1247):
// tree-sitter-cpp can't parse an operator_name in field position, so the // tree-sitter-cpp can't parse an operator_name in field position, so the
@@ -5427,10 +5488,13 @@ export class TreeSitterExtractor {
// TS/JS: look through wrappers that keep the receiver the same // TS/JS: look through wrappers that keep the receiver the same
// object (`(x).m()`, `x!.m()`, `(x as T).m()`, `(await f()).m()`), // object (`(x).m()`, `x!.m()`, `(x as T).m()`, `(await f()).m()`),
// so the branches below see the identifier / call / chain the // so the branches below see the identifier / call / chain the
// call is really made on. // call is really made on. Swift: through a prefix operator, `try`
// and `await` (`(!store).contains(x)`, `(try! A()).b()`).
const receiver = rawReceiver && TS_JS_CHAIN_LANGUAGES.has(this.language) const receiver = rawReceiver && TS_JS_CHAIN_LANGUAGES.has(this.language)
? peelTsJsReceiver(rawReceiver) ? peelTsJsReceiver(rawReceiver)
: rawReceiver; : rawReceiver && this.language === 'swift'
? peelSwiftCallWrappers(rawReceiver, this.source)
: rawReceiver;
// A LITERAL receiver — `", ".join(...)`, `"x".toUpperCase()`, // A LITERAL receiver — `", ".join(...)`, `"x".toUpperCase()`,
// `5.times`, `[].concat(...)` — calls a builtin of the literal's // `5.times`, `[].concat(...)` — calls a builtin of the literal's
// type, never a project symbol. The bare-name fallback below let // type, never a project symbol. The bare-name fallback below let
@@ -5506,7 +5570,12 @@ export class TreeSitterExtractor {
// call_expression's first named child (a navigation_expression // call_expression's first named child (a navigation_expression
// `Foo.getInstance`, or a bare identifier for a free/constructor call). // `Foo.getInstance`, or a bare identifier for a free/constructor call).
const innerNav = receiver.namedChild(0); const innerNav = receiver.namedChild(0);
innerCallee = innerNav ? getNodeText(innerNav, this.source).replace(/\s+/g, '') : ''; // Swift: without a `try`/`await`/prefix operator on its left
// edge (`try! Foo.make().bar().baz()`).
const innerText = !innerNav ? ''
: this.language === 'swift' ? swiftUnwrappedText(innerNav, this.source)
: getNodeText(innerNav, this.source);
innerCallee = innerText.replace(/\s+/g, '');
// Only re-encode a CLASS / companion-factory / constructor chain, // Only re-encode a CLASS / companion-factory / constructor chain,
// whose receiver chain starts with a capitalized type // whose receiver chain starts with a capitalized type
// (`Foo.getInstance().bar()`, `Foo().bar()`). An instance chain // (`Foo.getInstance().bar()`, `Foo().bar()`). An instance chain
@@ -5541,7 +5610,7 @@ export class TreeSitterExtractor {
this.language === 'swift' && this.language === 'swift' &&
receiver && receiver &&
receiver.type === 'navigation_expression' && receiver.type === 'navigation_expression' &&
SWIFT_TYPE_PATH_RECEIVER.test(getNodeText(receiver, this.source).replace(/\s+/g, '')) SWIFT_TYPE_PATH_RECEIVER.test(swiftUnwrappedText(receiver, this.source).replace(/\s+/g, ''))
) { ) {
// Swift call through a type path — `API.PackageController.GetRoute.query(on:)`, // Swift call through a type path — `API.PackageController.GetRoute.query(on:)`,
// on one line or split before the `.query`. Keep the path: the // on one line or split before the `.query`. Keep the path: the
@@ -5549,9 +5618,11 @@ export class TreeSitterExtractor {
// type's `query` came first (every route in a Vapor app has one). // type's `query` came first (every route in a Vapor app has one).
// The resolver finds the member on the type the path names, or // The resolver finds the member on the type the path names, or
// leaves the call unresolved. An instance chain (`self.store.load()`, // leaves the call unresolved. An instance chain (`self.store.load()`,
// `viewModel.state.reset()`) is not a type path and stays bare. // `viewModel.state.reset()`) is not a type path and stays bare. A
// Mirrored in the kernel's extract_call (swift.rs). // prefix operator on the path's head is not part of it
calleeName = `${getNodeText(receiver, this.source).replace(/\s+/g, '')}.${methodName}`; // (`!Settings.Defaults.isOn()`). Mirrored in the kernel's
// extract_call (swift.rs).
calleeName = `${swiftUnwrappedText(receiver, this.source).replace(/\s+/g, '')}.${methodName}`;
} else if ( } else if (
this.language === 'cfscript' && this.language === 'cfscript' &&
receiver && receiver &&
@@ -6035,12 +6106,15 @@ export class TreeSitterExtractor {
// The receiver must be a SIMPLE capitalized identifier — `Type.X`, not the // The receiver must be a SIMPLE capitalized identifier — `Type.X`, not the
// nested `a.B.c` (whose own head member-access is visited separately) nor a // nested `a.B.c` (whose own head member-access is visited separately) nor a
// lowercase `obj.field` / `pkg.func`. // lowercase `obj.field` / `pkg.func`.
const recv = const rawRecv =
getChildByField(node, 'object') ?? getChildByField(node, 'object') ??
getChildByField(node, 'expression') ?? getChildByField(node, 'expression') ??
getChildByField(node, 'scope') ?? getChildByField(node, 'scope') ??
node.namedChild(0); node.namedChild(0);
if (!recv) return; if (!rawRecv) return;
// Swift binds a prefix operator to the head: `!Config.shared.isOn` is
// `(!Config).shared.isOn`. Mirrored in the kernel (swift.rs).
const recv = this.language === 'swift' ? peelSwiftCallWrappers(rawRecv, this.source) : rawRecv;
const t = recv.type; const t = recv.type;
if ( if (
t === 'identifier' || t === 'type_identifier' || t === 'simple_identifier' || t === 'identifier' || t === 'type_identifier' || t === 'simple_identifier' ||