mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
b238e240e2
* fix(objc): a message reached by name alone must be in reach - `[self m]` / `[super m]` / `[[self class] m]` (receiver dropped by the extractor) reach only methods of the sender's hierarchy, read from `@interface Name : Super` plus a small UIKit/AppKit/MapKit superclass table for categories on system classes. When tree-sitter-objc loses an @implementation and its methods become functions, a same-file function, one in the file named after a hierarchy class, or any when the sender's class itself was lost, still counts; `[super init]` never lands on another file's function. - C call syntax (`completionBlock()`) never means a method or property. - Local receivers are typed from ObjC declarations (`FMResultSet *rs`, `(nullable FMResultSet *)rs`, block params), and objc joins the untyped- receiver languages for the unique-name guess (`weakSelf` / `strongSelf` count as self). Shared receiver words must be 3+ letters so class prefixes (`SD`, `NS`) don't match; Ruby/CFML unchanged but one wrong lobsters edge. A/B (edges removed / added): SDWebImage -321/+47, fmdb -28/+85, CocoaLumberjack -19/+10, AFNetworking -13/+1, MBProgressHUD 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(objc): a class or typed-property receiver keeps its inherited method `[AllTypesObject objectsInRealm:…]` (a class method inherited from RLMObject) and `[managed.anyDataObj containsObject:]` (a property declared `RLMSet<…> *anyDataObj`) now keep the unique-name match when the receiver's class — or one of the property's declared types — has the method's owner in its hierarchy. realm-swift goes from -2562/+588 to -504/+529 vs main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
/**
|
||
* An Objective-C message reached by name alone must be in reach:
|
||
*
|
||
* - a message to `self` / `super` (the extractor drops the receiver) is a
|
||
* method of the sender's class hierarchy — SDWebImage's `[self class]`
|
||
* went to SDWeakProxy's `class` 71 times;
|
||
* - C call syntax (`completionBlock()`) calls a block, a function or a
|
||
* function pointer, never a method or property;
|
||
* - a message to an untyped local needs its declaration (`FMResultSet *rs`)
|
||
* or a receiver named after the class — `[image respondsToSelector:]` is
|
||
* NSObject's, not the one proxy class that overrides it.
|
||
*/
|
||
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-objc-scope-'));
|
||
const files: Record<string, string> = {
|
||
'Core/SDWeakProxy.h': `#import <Foundation/Foundation.h>
|
||
@interface SDWeakProxy : NSProxy
|
||
- (Class)class;
|
||
- (BOOL)respondsToSelector:(SEL)aSelector;
|
||
@end
|
||
`,
|
||
'Core/SDWeakProxy.m': `#import "SDWeakProxy.h"
|
||
@implementation SDWeakProxy
|
||
- (Class)class {
|
||
return nil;
|
||
}
|
||
- (BOOL)respondsToSelector:(SEL)aSelector {
|
||
return NO;
|
||
}
|
||
@end
|
||
`,
|
||
'Core/SDPrefetchToken.h': `#import <Foundation/Foundation.h>
|
||
@interface SDPrefetchToken : NSObject
|
||
@property (nonatomic, copy) void (^completionBlock)(void);
|
||
@end
|
||
`,
|
||
'Core/FMResultSet.h': `#import <Foundation/Foundation.h>
|
||
@interface FMResultSet : NSObject
|
||
- (BOOL)next;
|
||
@end
|
||
`,
|
||
'Core/FMResultSet.m': `#import "FMResultSet.h"
|
||
@implementation FMResultSet
|
||
- (BOOL)next {
|
||
return NO;
|
||
}
|
||
@end
|
||
`,
|
||
'Core/SDBase.h': `#import <Foundation/Foundation.h>
|
||
@interface SDBase : NSObject
|
||
- (void)prepare;
|
||
@end
|
||
`,
|
||
'Core/SDBase.m': `#import "SDBase.h"
|
||
@implementation SDBase
|
||
- (void)prepare {
|
||
}
|
||
@end
|
||
`,
|
||
'Core/SDCache.h': `#import "SDBase.h"
|
||
@interface SDCache : SDBase
|
||
- (void)storeWithCompletion:(void (^)(void))completionBlock image:(id)image;
|
||
@end
|
||
`,
|
||
'Core/SDCache.m': `#import "SDCache.h"
|
||
#import "FMResultSet.h"
|
||
@implementation SDCache
|
||
- (void)storeWithCompletion:(void (^)(void))completionBlock image:(id)image {
|
||
Class cls = [self class];
|
||
[super prepare];
|
||
[self helper];
|
||
FMResultSet *rs = [self query];
|
||
while ([rs next]) {
|
||
}
|
||
if ([image respondsToSelector:@selector(copy)]) {
|
||
}
|
||
completionBlock();
|
||
}
|
||
- (void)helper {
|
||
}
|
||
- (FMResultSet *)query {
|
||
return nil;
|
||
}
|
||
@end
|
||
`,
|
||
};
|
||
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('Objective-C messages reached by name alone', () => {
|
||
it('reach the sender’s hierarchy and typed locals; never a proxy override or a property through C call syntax', () => {
|
||
const method = cg.getNodesInFile('Core/SDCache.m').find((n) => n.name.startsWith('storeWithCompletion'))!;
|
||
const targets = cg
|
||
.getOutgoingEdges(method.id)
|
||
.filter((e) => e.kind === 'calls')
|
||
.map((e) => cg.getNode(e.target)!.qualifiedName)
|
||
.sort();
|
||
expect(targets).toEqual(['FMResultSet::next', 'SDBase::prepare', 'SDCache::helper', 'SDCache::query']);
|
||
});
|
||
});
|