Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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.
- In Swift, a call on a local created through `try`, `try!`, `try?` or `await`, or with a generic type's constructor, now links to that type's method: after `let storage = try! DiskStorage.Backend<String>(config: config)`, `storage.store(…)` links to `Backend`'s `store`. Before, such a local had no known type, so the call was left unlinked or matched by name to another type's method. Re-index Swift projects after upgrading.

## [1.6.2] - 2026-10-03

Expand Down
138 changes: 138 additions & 0 deletions __tests__/swift-try-initialized-locals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* A Swift local initialized through `try`, `try!`, `try?` or `await`, or with
* a generic type's constructor, has that type: a call on it is a method of
* the type constructed.
*
* `let x = try! Storage(path: p)` read as no type at all — the declaration
* patterns wanted `= Type(` — and so did `let v = Disk.Backend<Int>(…)`, so
* `x.store(1)` fell to name matching among every `store` in the project
* (Kingfisher's `let storage = try! DiskStorage.Backend<String>(config:)`
* then `storage.store(…)`). Every target here has a namesake declared before
* it on another type, and no local is named like its type, so neither a
* first-declared nor a receiver-name guess can 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/Decoys.swift': `final class Archive {
func store(_ v: Int) {}
}
final class Reloader {
func load() {}
}
final class Lookup {
func value(forKey k: String) -> Int? { nil }
}
final class Outbox {
func send() {}
}
`,
'Sources/App/Types.swift': `final class Storage {
init(path: String) throws {}
func store(_ v: Int) {}
}
final class Loader {
init() async {}
func load() {}
}
enum Disk {
final class Backend<T> {
init(config: Int) throws {}
func value(forKey k: String) throws -> T? { nil }
}
}
final class Pool<T> {
init(capacity: Int) {}
func value(forKey k: String) -> T? { nil }
}
final class Connection {
func send() {}
}
final class Vault {
init() throws {}
func write<T>(_ block: () -> T) -> T { block() }
func freeze() -> Vault { self }
}
final class Note {
func freeze() -> Note { self }
}
final class Builder {
init(host: String) throws {}
func connect() -> Connection { Connection() }
}
`,
'Sources/App/Screen.swift': `final class Screen {
func a() { let x = try! Storage(path: "x"); x.store(1) }
func b() throws { let y = try Storage(path: "y"); y.store(2) }
func c() { let z = try? Storage(path: "z"); z?.store(3) }
func d() async { let w = await Loader(); w.load() }
func e() async throws { let r = try await Loader(); r.load() }
func f() throws { let v = try Disk.Backend<Int>(config: 1); _ = try v.value(forKey: "k") }
func g() { let t = Pool<String>(capacity: 1); _ = t.value(forKey: "k") }
func h() { let u = Archive(); u.store(4) }
func i() throws {
var conn: Connection? = nil
conn = try Builder(host: "i")
.connect()
conn?.send()
}
func j() {
let obj = try! Vault().write { Note() }
_ = obj.freeze()
}
}
`,
};

describe('Swift: a local initialized through try, await or a generic constructor', () => {
it('has the type constructed', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-swift-try-local-'));
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 {
// The methods each function calls (constructions aside).
const methods = (caller: string): string[] => {
const from = cg.getNodesByName(caller).find((n) => n.kind === 'method');
expect(from, caller).toBeDefined();
return cg
.getOutgoingEdgesFrom([from!.id], ['calls'])
.map((e) => cg.getNode(e.target))
.filter((n) => n && n.kind === 'method')
.map((n) => n!.qualifiedName)
.sort();
};
expect(methods('a')).toEqual(['Storage::store']);
expect(methods('b')).toEqual(['Storage::store']);
expect(methods('c')).toEqual(['Storage::store']);
expect(methods('d')).toEqual(['Loader::load']);
expect(methods('e')).toEqual(['Loader::load']);
expect(methods('f')).toEqual(['Disk::Backend::value']);
expect(methods('g')).toEqual(['Pool::value']);
// Already typed before: a plain constructor.
expect(methods('h')).toEqual(['Archive::store']);
// An assignment through `try` that goes on into a chain (`.connect()` on
// the next line) is no declaration of the chain head's type: the local
// keeps the type it was declared with.
expect(methods('i')).toEqual(['Builder::connect', 'Connection::send']);
// A member after the call makes the local a value of what it returns
// (realm-swift's `let obj = try! Realm().write { … }`): not a Vault.
expect(methods('j')).toContain('Vault::write');
expect(methods('j')).not.toContain('Vault::freeze');
} finally {
cg.close();
}
});
});
18 changes: 17 additions & 1 deletion src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13101,6 +13101,13 @@ export function arrayElementType(type: string): string | undefined {
*/
const TYPE_ARGS = '(?:<[^;=(){}<>]*(?:<[^;=(){}<>]*>[^;=(){}<>]*)*>)?\\s*(?:\\[[\\s,]*\\]\\s*)*';

/**
* A Swift constructor call's argument list that no member follows: closed on
* the line (two levels of nested parentheses) with no `.`/`?.` after it, or
* left open at the line's end — its arguments go on below.
*/
const SWIFT_UNCHAINED_CALL = '(?:\\((?:[^()]|\\((?:[^()]|\\([^()]*\\))*\\))*\\)(?!\\s*\\??\\.)|\\([^)]*$)';

/**
* Per-language patterns that recover a local variable's (or typed parameter's)
* type from its declaration/initializer. Each regex captures the type in group
Expand Down Expand Up @@ -13375,7 +13382,16 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[]
];
case 'swift':
return [
new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // let lg = Logger(...)
// let lg = Logger(...), also with type arguments: `let p = Pool<String>(…)`
new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*${TYPE_ARGS}\\(`),
// ...and through `try` / `try!` / `try?` / `await` when it declares the
// local: `let storage = try! DiskStorage.Backend<String>(…)`. Not in a
// bare assignment — `channel = try ServerBootstrap(…)` goes on into
// `.bind(…).wait()` on the lines below, a value of another type, and
// must not hide what `var channel: Channel?` declared — nor when a
// member follows the call (`try! Realm().write { … }` is a value of
// what `write` returns).
new RegExp(`\\b(?:let|var)\\s+${r}\\b\\s*=\\s*(?:try[!?]\\s*|(?:try|await)\\s+)+([A-Z][\\w.]*)\\s*${TYPE_ARGS}${SWIFT_UNCHAINED_CALL}`),
new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // let lg: Logger / param
];
case 'rust':
Expand Down