fix(swift): a local declared through try, await or a generic constructor has the type constructed (#2689)

The Swift declaration pattern behind receiver typing read `name = Type(`
only, so a local declared through `try`, `try!`, `try?` or `await`, or with
a generic type's constructor, had no type at all. Kingfisher's
`let storage = try! DiskStorage.Backend<String>(config: config)` left
`storage.store(…)` to name matching among every `store` in the project.

The pattern now reads type arguments, as the Java and C# patterns do, and
steps over `try`/`await` where the line declares the local (`let`/`var`)
and no member follows the constructor call there. A bare assignment through
`try` is left as before: swift-nio's `channel = try ServerBootstrap(…)` goes
on into `.bind(…).wait()` on the lines below, a value of another type, and
typing it by the chain's head hid the `var channel: Channel?` it was
declared as. Likewise realm-swift's `let obj = try! Realm().write { … }` is
a value of what `write` returns. resolveMethodOnType still checks the
method on the type, so a local typed wrongly links nothing.

swift-try-initialized-locals.test.ts gives every target a namesake declared
before it on another type, names no local after its type, and pins the
assignment and the chained-call cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 23:36:16 +00:00
committed by GitHub
parent e8b478a00e
commit 12ea27faf4
3 changed files with 156 additions and 1 deletions
+1
View File
@@ -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
@@ -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();
}
});
});
+17 -1
View File
@@ -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
@@ -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':