mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
fix(objc,swift): a class conforming to its namesake protocol implements the protocol (#2242)
The SwiftUI/UIKit/Vapor resolvers apply to Swift refs only; the shared name pick never returns the declaring type for an inheritance ref; and the supertype re-choice prefers the declaring file, then a protocol/interface for `implements`. 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
|
||||
|
||||
- An Objective-C class that conforms to a protocol of its own name now links to that protocol instead of to itself. Swift's naming conventions for views, models and controllers also no longer apply to Objective-C code. Before, every one of SDWebImage's `@interface SDDiskCache : NSObject <SDDiskCache>`-style classes implemented itself.
|
||||
- In Solidity, a call to an overloaded function now reaches the overload whose parameter count it fits, as in the other languages with overloading. Before, OpenZeppelin's `_checkRole(role, _msgSender())` inside the one-argument `_checkRole(bytes32 role)` was linked to itself.
|
||||
- In JavaScript and TypeScript, the React conventions for hooks (`use…`) and contexts (`…Context`, `…Provider`) now pick only from the reference's own file. Another file's hook or context is reached only through an import. An optional call like `onSuccess?.(…)` now gets the same scope rules as a plain call. Before, trpc's WebSocket adapter's `createContext?.(…)` option linked to an example app's `createContext`, zod's `schemas.ParseContext` linked to the v3 copy instead of v4's, and bulletproof-react's apps linked `useUser()` to one another's.
|
||||
- In R, a call like `c(1, 2)` is no longer linked to a data variable that happens to share a base function's name, since R skips non-function bindings when it looks up a call. Before, ggplot2's facet tests bound `c <- data_frame(b = 3)`, and every `c(…)` call in those files pointed at that data frame. Functions built by a factory, like ggplot2's `geom_point <- make_constructor(…)`, still resolve.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* An Objective-C class that conforms to a protocol of its own name —
|
||||
* SDWebImage's `@interface SDDiskCache : NSObject <SDDiskCache>` — implements
|
||||
* the protocol, not itself. The SwiftUI / UIKit / Vapor conventions are
|
||||
* Swift's (they took the ObjC ref), and among the protocol's copies the
|
||||
* declaring header's own is the one.
|
||||
*/
|
||||
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;
|
||||
|
||||
const header = `#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol SDDiskCache <NSObject>
|
||||
- (nullable NSData *)dataForKey:(nonnull NSString *)key;
|
||||
@end
|
||||
|
||||
@interface SDDiskCache : NSObject <SDDiskCache>
|
||||
@end
|
||||
`;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-objc-protocol-'));
|
||||
const files: Record<string, string> = {
|
||||
'SDWebImage/Core/SDDiskCache.h': header,
|
||||
'SDWebImage/include/SDWebImage/SDDiskCache.h': header,
|
||||
'Examples/SwiftUIDemo/ContentView.swift': `import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View { Text("x") }
|
||||
}
|
||||
`,
|
||||
};
|
||||
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('an Objective-C class conforming to its namesake protocol', () => {
|
||||
it('implements the protocol in its own header', () => {
|
||||
const cls = cg.getNodesInFile('SDWebImage/Core/SDDiskCache.h').find((n) => n.kind === 'class')!;
|
||||
const targets = cg.getOutgoingEdgesFrom([cls.id]).filter((e) => e.kind === 'implements').map((e) => cg.getNode(e.target)!);
|
||||
expect(targets.map((t) => `${t.kind} ${t.filePath}`)).toEqual(['protocol SDWebImage/Core/SDDiskCache.h']);
|
||||
});
|
||||
});
|
||||
@@ -44,8 +44,11 @@ export function pickByNameAndKind(
|
||||
context: ResolutionContext,
|
||||
options: NamePickOptions = {},
|
||||
): string | null {
|
||||
const inheritance = ref.referenceKind === 'extends' || ref.referenceKind === 'implements';
|
||||
const candidates = context.getNodesByName(ref.referenceName).filter((n) =>
|
||||
kinds.has(n.kind) &&
|
||||
// A type never inherits from itself.
|
||||
!(inheritance && n.id === ref.fromNodeId) &&
|
||||
isLexicallyReachable(n, ref, context) &&
|
||||
(n.filePath === ref.filePath || (!isNestedType(n, context) && isVisibleAcrossFiles(n, ref, context))) &&
|
||||
(!options.accept || options.accept(n)));
|
||||
|
||||
@@ -40,6 +40,9 @@ export const swiftUIResolver: FrameworkResolver = {
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
// Swift's conventions, for Swift's refs: an Objective-C `@interface SDDiskCache
|
||||
// : NSObject <SDDiskCache>` is no SwiftUI view or model (languages gates only extraction).
|
||||
if (ref.language !== 'swift') return null;
|
||||
// Pattern 1: View references (SwiftUI views are PascalCase ending in View)
|
||||
if (ref.referenceName.endsWith('View') && /^[A-Z]/.test(ref.referenceName)) {
|
||||
const result = resolveByNameAndKind(ref, VIEW_KINDS, VIEW_DIRS, context);
|
||||
@@ -106,6 +109,9 @@ export const uikitResolver: FrameworkResolver = {
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
// Swift's conventions, for Swift's refs: an Objective-C `@interface SDDiskCache
|
||||
// : NSObject <SDDiskCache>` is no SwiftUI view or model (languages gates only extraction).
|
||||
if (ref.language !== 'swift') return null;
|
||||
// Pattern 1: ViewController references
|
||||
if (ref.referenceName.endsWith('ViewController')) {
|
||||
const result = resolveByNameAndKind(ref, CLASS_KINDS, VC_DIRS, context);
|
||||
@@ -194,13 +200,15 @@ export const vaporResolver: FrameworkResolver = {
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
// Swift's conventions, for Swift's refs: an Objective-C `@interface SDDiskCache
|
||||
// : NSObject <SDDiskCache>` is no SwiftUI view or model (languages gates only extraction).
|
||||
if (ref.language !== 'swift') return null;
|
||||
// Pattern 0: a route's handler — `use: SearchController.show` arrives as
|
||||
// `SearchController@show`, `use: self.index` / `use: index` as `@index`.
|
||||
// Resolved on the type the route names, never by the method's name alone:
|
||||
// every controller has a `show`. No match, or two, is left unresolved.
|
||||
const handler = VAPOR_HANDLER.exec(ref.referenceName);
|
||||
if (handler) {
|
||||
if (ref.language !== 'swift') return null;
|
||||
const target = resolveVaporHandler(handler[1] ?? null, handler[2]!, ref, context);
|
||||
return target ? { original: ref, targetNodeId: target, confidence: 0.9, resolvedBy: 'framework' } : null;
|
||||
}
|
||||
|
||||
@@ -8923,7 +8923,18 @@ function otherSupertypeNamed(ref: UnresolvedRef, context: ResolutionContext): Re
|
||||
const candidates = context.getNodesByName(name).filter((n) =>
|
||||
n.id !== ref.fromNodeId && isSupertypeTarget(n) && sameLanguageFamily(n.language, ref.language) &&
|
||||
(qualifier === '' || ownerPathOf(n, context) === qualifier || ownerPathOf(n, context).endsWith(`.${qualifier}`)));
|
||||
return candidates.length === 1 ? { original: ref, targetNodeId: candidates[0]!.id, confidence: 0.8, resolvedBy: 'qualified-name' } : null;
|
||||
// Among several, the declaring file's own, then (for `implements`) a protocol / interface / trait:
|
||||
// SDWebImage's `@interface SDWebImageCacheKeyFilter : NSObject <SDWebImageCacheKeyFilter>`.
|
||||
let pool = candidates;
|
||||
if (pool.length > 1) {
|
||||
const sameFile = pool.filter((n) => n.filePath === ref.filePath);
|
||||
if (sameFile.length > 0) pool = sameFile;
|
||||
}
|
||||
if (pool.length > 1 && ref.referenceKind === 'implements') {
|
||||
const conformable = pool.filter((n) => n.kind === 'protocol' || n.kind === 'interface' || n.kind === 'trait');
|
||||
if (conformable.length > 0) pool = conformable;
|
||||
}
|
||||
return pool.length === 1 ? { original: ref, targetNodeId: pool[0]!.id, confidence: 0.8, resolvedBy: 'qualified-name' } : null;
|
||||
}
|
||||
|
||||
/** A declaration's dotted owner path — its file's package clauses, then its enclosing types (`cats.kernel.instances`, `cats.Bifoldable`). */
|
||||
|
||||
Reference in New Issue
Block a user