mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-02 01:37:32 +08:00
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:
co-authored by
Claude Opus 5.5
parent
b55836daa3
commit
4c381aedbd
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user