mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-22 01:07: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:
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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(/(?<!\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)', () => {
|
||||
// A NEW-only regression construct (`#if` between enum cases — the swift
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -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' ||
|
||||
|
||||
Reference in New Issue
Block a user