fix(swift): a call on a constructed value is that type's member (#2203)

A chained Swift call accepted any same-named member unless its name was a
standard-library one: vapor's `JSONDecoder().decode(...)` and even its own
`URLQueryDecoder().decode(...)` went to `ClientRequest._URLQueryContainer
.decode`, IceCubes' `JSONEncoder().encode(self)` to a filter's `encode`,
realm's `Realm().create(...)` to a test object's `create`.

When the link before the call is a constructor - `TypeName(...)` with flat
arguments - the member must belong to that type or a supertype / protocol
the project gives it; an outside type (JSONDecoder) keeps only the
project's extensions of it. A capitalized C function
(`RLMObjectBaseObjectSchema(obj)!`) constructs nothing and is left alone.

vapor -63 +62 (`URLQueryDecoder::decode`, `URLEncodedFormParser::parse`),
IceCubesApp -23 +1, realm-swift -19 +19 (`Realm::create`), Alamofire -1;
Kingfisher unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 21:03:23 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b55836daa3
commit 4c381aedbd
3 changed files with 86 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- A Swift call on a value built on the spot, like `JSONDecoder().decode(…)` or `Realm().create(…)`, now resolves to that type's method, or to one it inherits. It no longer lands on some other type's method of the same name: Vapor's `JSONDecoder().decode` calls had gone to a request's private container.
- A bare Lua call like `ipairs(t)`, `error("…")` or busted's `setup()` now means Lua's own function unless the file defines one. It no longer lands on a table method of the same name: telescope's `ipairs` calls had gone to a linked list's `ipairs`, and kong's `error` calls to its response module. A function's own alias of a host table member, like `local get_query = kong.request.get_query`, is now followed as well.
- A C++ call into a macro-opened namespace, like `fmt::format("{}", x)`, now picks the overload its arguments fit. Before, the first overload indexed won: fmt's 1,400 `fmt::format` calls all reached the `text_style` overload in `color.h`. A narrow string literal now fits a narrow format string, a wide `L"…"` fits the wide overload, and the argument count has to fit too.
- Calls inside an inline route handler now keep their receivers, so they resolve like any other method call. Before, Hono's `app.get('/', (c) => c.text('Hello'))` linked the route to the one other `text` method in the project, its client's `ClientResponse.text`; that happened in over 400 routes. A call like `userService.lookup(id)` reaches the service it names.
@@ -0,0 +1,62 @@
/**
* A Swift call on a just-constructed value — `URLQueryDecoder().decode(…)`,
* `JSONDecoder().decode(…)` — is a member of the type constructed (or of what
* it inherits), never some other type's method of that name: vapor's
* `JSONDecoder().decode` went to a request's private
* `_URLQueryContainer.decode`, and so did its own decoders' calls.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-swift-constructed-'));
const files: Record<string, string> = {
'Sources/App/URLQueryDecoder.swift': `struct URLQueryDecoder {
func decode<D>(_ type: D.Type) -> Int {
return 0
}
}
`,
'Sources/App/Request.swift': `struct Request {
struct _Container {
func decode<D>(_ type: D.Type) -> Int {
return 1
}
}
}
`,
'Sources/App/Use.swift': `import Foundation
func run(data: Data) throws -> Int {
let a = URLQueryDecoder().decode(Int.self)
let b = try JSONDecoder().decode(Int.self, from: data)
return a + b
}
`,
};
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);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('Swift calls on a constructed value', () => {
it('reach the constructed type’s member, and no other type’s', () => {
const run = cg.getNodesInFile('Sources/App/Use.swift').find((n) => n.name === 'run')!;
const decodes = cg.getOutgoingEdges(run.id).filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!).filter((t) => t.name === 'decode').map((t) => t.qualifiedName);
expect(decodes).toEqual(['URLQueryDecoder::decode']);
});
});
+23
View File
@@ -2369,6 +2369,8 @@ interface SwiftCallShape {
label: string;
/** `name[…]`: a subscript of a value, not a call. */
subscript: boolean;
/** `URLEncodedFormDecoder().decode(…)`: the type the link before the call constructs. */
constructed?: string;
}
function swiftCallShape(ref: UnresolvedRef, context: ResolutionContext): SwiftCallShape | null {
@@ -2393,6 +2395,7 @@ function swiftCallShape(ref: UnresolvedRef, context: ResolutionContext): SwiftCa
shape: 'chained',
receiver: rustGoReceiverName(before.replace(/[?!]\s*\./g, '.')),
label: /^\s*\(\s*([A-Za-z_]\w*)\s*:(?!:)/.exec(after)?.[1] ?? '',
constructed: /(?<![\w$.])([A-Z][\w$]*)\s*(?:<[^<>()]*>)?\s*\([^()]*\)\s*[?!]?\s*\.\s*$/.exec(before)?.[1],
};
}
@@ -2522,6 +2525,19 @@ function swiftHierarchyAt(ref: UnresolvedRef, context: ResolutionContext): Map<s
return depths;
}
/** A Swift type and every supertype and protocol the project says it has. */
function swiftTypeClosure(typeName: string, context: ResolutionContext): Set<string> {
const seen = new Set<string>();
const queue = [typeName];
while (queue.length > 0 && seen.size < 40) {
const name = queue.shift()!;
if (seen.has(name)) continue;
seen.add(name);
queue.push(...swiftDeclOf(name, context).supers);
}
return seen;
}
/**
* Of the in-scope members a bare / `self.` / `super.` Swift call could mean,
* the nearest: the type's own, else its superclass's — Alamofire's
@@ -2601,6 +2617,13 @@ function isSwiftCallTarget(n: Node, shape: SwiftCallShape | null, ref: Unresolve
}
if (owner === null) return true;
if (shape.shape === 'chained') {
// A member of what the link before constructs, or of what that inherits:
// vapor's `URLEncodedFormDecoder().decode(…)` is not a request's private
// `_URLQueryContainer.decode`, `JSONDecoder().decode(…)` no project type's.
// (A capitalized C function — realm's `RLMObjectBaseObjectSchema(obj)!` — constructs nothing.)
if (shape.constructed && !context.getNodesByName(shape.constructed).some((f) => f.kind === 'function')) {
return swiftTypeClosure(shape.constructed, context).has(owner);
}
if (shape.receiver === '' || !SWIFT_STD_METHODS.has(n.name)) return true;
return sharesReceiverWord(shape.receiver.split('.').pop()!, n) || !swiftDeclOf(owner, context).projectType ||
swiftDeclaresLabel(n, shape.label, context);