diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4e7cc32..4e59ced8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/`. - 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 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 diff --git a/__tests__/fixtures/kernel-parity/TortureWrappedCalls.swift b/__tests__/fixtures/kernel-parity/TortureWrappedCalls.swift new file mode 100644 index 000000000..00f50406c --- /dev/null +++ b/__tests__/fixtures/kernel-parity/TortureWrappedCalls.swift @@ -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 = .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 } +} diff --git a/__tests__/kernel-swift-parity.test.ts b/__tests__/kernel-swift-parity.test.ts index 4db4060e7..4a098af6c 100644 --- a/__tests__/kernel-swift-parity.test.ts +++ b/__tests__/kernel-swift-parity.test.ts @@ -18,7 +18,9 @@ * 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 * 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/ * 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([]); }); + // 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(/(? { + 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 = .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)', () => { // A NEW-only regression construct (`#if` between enum cases — the swift // checklist's grammar-bump delta 5) — errors on the 0.7.3 grammar. diff --git a/__tests__/swift-wrapped-call-receivers.test.ts b/__tests__/swift-wrapped-call-receivers.test.ts new file mode 100644 index 000000000..c78769940 --- /dev/null +++ b/__tests__/swift-wrapped-call-receivers.test.ts @@ -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 = { + '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(); + } + }); +}); diff --git a/codegraph-kernel/src/swift.rs b/codegraph-kernel/src/swift.rs index f1d3ae4ac..057a73f19 100644 --- a/codegraph-kernel/src/swift.rs +++ b/codegraph-kernel/src/swift.rs @@ -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.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" diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 0ee4595c8..0bee1075e 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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. */ 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), * including host API chains (#1707). Keep the existing `window.MyNamespace` @@ -5347,7 +5398,17 @@ export class TreeSitterExtractor { } } } 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): // 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 // object (`(x).m()`, `x!.m()`, `(x as T).m()`, `(await f()).m()`), // 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) ? peelTsJsReceiver(rawReceiver) - : rawReceiver; + : rawReceiver && this.language === 'swift' + ? peelSwiftCallWrappers(rawReceiver, this.source) + : rawReceiver; // A LITERAL receiver — `", ".join(...)`, `"x".toUpperCase()`, // `5.times`, `[].concat(...)` — calls a builtin of the literal's // 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 // `Foo.getInstance`, or a bare identifier for a free/constructor call). 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, // whose receiver chain starts with a capitalized type // (`Foo.getInstance().bar()`, `Foo().bar()`). An instance chain @@ -5541,7 +5610,7 @@ export class TreeSitterExtractor { this.language === 'swift' && receiver && 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:)`, // 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). // The resolver finds the member on the type the path names, or // leaves the call unresolved. An instance chain (`self.store.load()`, - // `viewModel.state.reset()`) is not a type path and stays bare. - // Mirrored in the kernel's extract_call (swift.rs). - calleeName = `${getNodeText(receiver, this.source).replace(/\s+/g, '')}.${methodName}`; + // `viewModel.state.reset()`) is not a type path and stays bare. A + // prefix operator on the path's head is not part of it + // (`!Settings.Defaults.isOn()`). Mirrored in the kernel's + // extract_call (swift.rs). + calleeName = `${swiftUnwrappedText(receiver, this.source).replace(/\s+/g, '')}.${methodName}`; } else if ( this.language === 'cfscript' && receiver && @@ -6035,12 +6106,15 @@ export class TreeSitterExtractor { // 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 // lowercase `obj.field` / `pkg.func`. - const recv = + const rawRecv = getChildByField(node, 'object') ?? getChildByField(node, 'expression') ?? getChildByField(node, 'scope') ?? 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; if ( t === 'identifier' || t === 'type_identifier' || t === 'simple_identifier' ||