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:
Colby Mchenry
2026-09-30 21:15:33 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e7be363f95
commit a329df883a
3 changed files with 71 additions and 5 deletions
+60
View File
@@ -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']);
});
});