mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
fix(swift): a call on a typed property is a member of its type (#2205)
The declared-member receiver inference #2171 gave C#, Java and Kotlin now reads Swift too: `var cache: ImageCache!`, `let storage: DiskStorage.Backend<Data>`, `self.x` as well as `x`, a function's own `let` shadowing the property. The dotted-call path types the receiver from it, and a chained bare call keeps only members of that type or its supertypes / protocols. Kingfisher: `cache.imageCachedType(forKey:)` -> ImageCache's (was a test subclass's override), `storage.store/value/isCached` -> DiskStorage.Backend, `manager.retrieveImage` -> KingfisherManager (was a test double), DispatchQueue `ioQueue.async` no longer a project method. realm-swift: `List::append`, `RealmCollection::sum/index` (were Objective-C RLMArray selectors); vapor: `self.storage.write` -> NIOHTTPBodyWriterStorage. Kingfisher -100 +179, realm-swift -108 +203, Alamofire -16 +54, vapor -15 +10, IceCubesApp -6 +5; Newtonsoft.Json, okhttp unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- A Swift call on a property declared with a type, like Kingfisher's `var cache: ImageCache!` or `self.storage.write(…)`, now reaches that type's method or one it inherits. It no longer lands on a subclass override in a test, or on another type's method of the same name.
|
||||
- Calls in your own JavaScript no longer link to a same-named function inside a vendored minified script, like a `*.min.js` or a bundle made of a few enormous lines. Those names are mangled. In healthchecks, every jQuery `$(…)` call had gone to a one-letter helper inside `bootstrap-native.min.js`.
|
||||
- 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.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* A Swift call on a property the enclosing type declares with a type —
|
||||
* Kingfisher's `var cache: ImageCache!` — is a member of that type (or of
|
||||
* what it inherits): `cache.imageCachedType(forKey:)` is ImageCache's, not a
|
||||
* test subclass's override (`EvictingPrefetchCache`) that shared its name.
|
||||
*/
|
||||
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-member-'));
|
||||
const files: Record<string, string> = {
|
||||
'Sources/Cache/ImageCache.swift': `open class ImageCache {
|
||||
open func imageCachedType(forKey key: String) -> Int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
`,
|
||||
'Tests/KingfisherTests/ImagePrefetcherTests.swift': `class EvictingPrefetchCache: ImageCache {
|
||||
override func imageCachedType(forKey key: String) -> Int {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
`,
|
||||
'Tests/KingfisherTests/ImageCacheTests.swift': `class ImageCacheTests {
|
||||
var cache: ImageCache!
|
||||
|
||||
func testCachedType() {
|
||||
_ = cache.imageCachedType(forKey: "a")
|
||||
_ = self.cache.imageCachedType(forKey: "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 typed property', () => {
|
||||
it('reach the declared type’s method, not a subclass override elsewhere', () => {
|
||||
const ids = cg.getNodesInFile('Tests/KingfisherTests/ImageCacheTests.swift').map((n) => n.id);
|
||||
const targets = [...new Set(cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.target)!.qualifiedName))];
|
||||
expect(targets).toEqual(['ImageCache::imageCachedType']);
|
||||
});
|
||||
});
|
||||
@@ -2643,6 +2643,11 @@ function isSwiftCallTarget(n: Node, shape: SwiftCallShape | null, ref: Unresolve
|
||||
if (shape.constructed && !context.getNodesByName(shape.constructed).some((f) => f.kind === 'function')) {
|
||||
return swiftTypeClosure(shape.constructed, context).has(owner);
|
||||
}
|
||||
// A property the type around the call declares with a type — Kingfisher's
|
||||
// `var cache: ImageCache!` — is that type: `cache.imageCachedType(…)` is
|
||||
// ImageCache's, not a test subclass's override.
|
||||
const typed = /^(?:self\.)?[A-Za-z_]\w*$/.test(shape.receiver) ? inferMemberReceiverType(shape.receiver, ref, context) : null;
|
||||
if (typed && /^[A-Z]/.test(typed)) return swiftTypeClosure(typed, 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);
|
||||
@@ -5315,7 +5320,7 @@ function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[]
|
||||
}
|
||||
|
||||
/** Languages whose fields and properties declare their type where the class declares them. */
|
||||
const MEMBER_TYPED_LANGUAGES: ReadonlySet<string> = new Set(['csharp', 'java', 'kotlin']);
|
||||
const MEMBER_TYPED_LANGUAGES: ReadonlySet<string> = new Set(['csharp', 'java', 'kotlin', 'swift']);
|
||||
const MEMBER_CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'interface', 'enum', 'record']);
|
||||
const MEMBER_TYPE_MEMO = new WeakMap<ResolutionContext, Map<string, string | null>>();
|
||||
/** Words that can stand where a declaration's type does without being one. */
|
||||
@@ -5336,7 +5341,7 @@ const MEMBER_TYPE_NON_TYPES: ReadonlySet<string> = new Set([
|
||||
* TraceJsonWriter went to TraceJsonWriter's own `WriteValue` by name.
|
||||
*/
|
||||
function inferMemberReceiverType(receiver: string, ref: UnresolvedRef, context: ResolutionContext): string | null {
|
||||
const name = receiver.replace(/^this\./, '');
|
||||
const name = receiver.replace(/^(?:this|self)\./, '');
|
||||
if (!/^[A-Za-z_]\w*$/.test(name)) return null;
|
||||
const inFile = context.getNodesInFile(ref.filePath).filter((n) => n.language === ref.language);
|
||||
let cls: Node | undefined;
|
||||
@@ -5368,7 +5373,7 @@ function bindsNameItself(fn: Node, name: string, context: ResolutionContext): bo
|
||||
const lines = context.getFileLines?.(fn.filePath) ?? context.readFile(fn.filePath)?.split(/\r?\n/) ?? [];
|
||||
const body = lines.slice(fn.startLine - 1, fn.endLine).join('\n');
|
||||
const r = name.replace(/\$/g, '\\$');
|
||||
const binds = new RegExp(`\\b(?:var|val|out\\s+[\\w.<>?]+|foreach\\s*\\(\\s*[\\w.<>?,\\s]+?)\\s+${r}\\b|\\bfor\\s*\\([^;)]*\\s${r}\\s*:|\\b${r}\\s*=>|[(,]\\s*${r}\\s*(?:,[^()]*)?\\)\\s*=>|\\b${r}\\s*(?:,[^{}]*)?->`).test(body);
|
||||
const binds = new RegExp(`\\b(?:var|val|let|out\\s+[\\w.<>?]+|foreach\\s*\\(\\s*[\\w.<>?,\\s]+?)\\s+${r}\\b|\\bfor\\s*\\([^;)]*\\s${r}\\s*:|\\b${r}\\s*=>|[(,]\\s*${r}\\s*(?:,[^()]*)?\\)\\s*=>|\\b${r}\\s*(?:,[^{}]*)?->`).test(body);
|
||||
memo.set(key, binds);
|
||||
return binds;
|
||||
}
|
||||
@@ -5567,8 +5572,8 @@ function classMemberType(cls: Node, name: string, context: ResolutionContext): s
|
||||
const key = `${cls.id}|${name}`;
|
||||
if (memo.has(key)) return memo.get(key)!;
|
||||
const r = name.replace(/\$/g, '\\$');
|
||||
const pattern = cls.language === 'kotlin'
|
||||
? new RegExp(`\\b(?:val|var)\\s+${r}\\s*:\\s*([A-Z][\\w.]*)`)
|
||||
const pattern = cls.language === 'kotlin' || cls.language === 'swift'
|
||||
? new RegExp(`\\b(?:val|var|let)\\s+${r}\\s*:\\s*([A-Z][\\w.]*)`)
|
||||
: new RegExp(`(?:^|[\\s(,])([A-Za-z_][\\w.]*)\\s*${TYPE_ARGS}\\??\\s+${r}\\s*(?:[=;,)]|\\{)`);
|
||||
let found: string | null = null;
|
||||
for (const { text, depth } of classMemberLines(cls, context)) {
|
||||
|
||||
Reference in New Issue
Block a user