mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 16:57:09 +08:00
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:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user