Ports the #645/#608 chained-receiver mechanism to Objective-C. A message send whose receiver is itself a message send — `[[Foo create] doIt]` — used to drop the receiver, so `doIt` name-matched a same-named method on an unrelated class (commonly a test helper's `init` or an Apple-SDK method). - objc.ts: getReturnType reads the method's `method_type`, SKIPPING nullability / ARC qualifiers (`nonnull instancetype` must yield instancetype, not `nonnull`). - tree-sitter.ts: the message_expression branch now re-encodes a chained send `[[Foo create] doIt]` as `Foo.create().doIt` when the inner receiver is a capitalized class and the outer selector is unary. - name-matcher.ts: `objc` joins the dotted-chain gate + CHAIN_LANGUAGES. A class-message factory returns an instance of the RECEIVER class by convention (`instancetype`), so when the factory's own return type isn't recoverable (`alloc`/`new`/`shared…` return instancetype, or aren't user nodes), the receiver's type is the class itself — this resolves the ubiquitous `[[X alloc] init]` and singleton chains. resolveMethodOnType validates against the class and its supertypes, so a wrong inference yields no edge. Validation: 4 synthetic tests (factory+decoy, superclass conformance, absent-method safety, the nonnull-instancetype singleton). Real-repo A/B on SDWebImage (208 files): +35 / -75 — all corrections (the -75 are wrong `init` mis-matches to a test helper / wrong class, retargeted to the right class's init in the +35, plus 2 Apple-SDK chains on unindexed classes). db stable, no node explosion. EXTRACTION_VERSION 14->15. Full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
16c73e2b0e
commit
d21d2dfa50
@@ -2994,4 +2994,141 @@ void run() {
|
||||
expect(incoming.some((e) => e.kind === 'instantiates')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Objective-C chained message-send call resolution (#645/#608 mechanism)', () => {
|
||||
function callerNamesOf(qualifiedName: string): string[] {
|
||||
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
||||
if (!target) return [];
|
||||
const names = cg
|
||||
.getIncomingEdges(target.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.source)?.name)
|
||||
.filter((n): n is string => !!n);
|
||||
return [...new Set(names)].sort();
|
||||
}
|
||||
|
||||
it('resolves a chained message send [[Foo create] doIt] via the return type, never a same-named decoy', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.m'),
|
||||
`@interface Bar : NSObject
|
||||
- (void)doIt;
|
||||
@end
|
||||
@implementation Bar
|
||||
- (void)doIt {}
|
||||
@end
|
||||
@interface Decoy : NSObject
|
||||
- (void)doIt;
|
||||
@end
|
||||
@implementation Decoy
|
||||
- (void)doIt {}
|
||||
@end
|
||||
@interface Foo : NSObject
|
||||
+ (Bar *)create;
|
||||
@end
|
||||
@implementation Foo
|
||||
+ (Bar *)create { return nil; }
|
||||
- (void)run { [[Foo create] doIt]; }
|
||||
@end
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Bar::doIt')).toEqual(['run']);
|
||||
expect(callerNamesOf('Decoy::doIt')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a chained message whose method is inherited from a superclass (via conformance)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.m'),
|
||||
`@interface Base : NSObject
|
||||
- (void)render;
|
||||
@end
|
||||
@implementation Base
|
||||
- (void)render {}
|
||||
@end
|
||||
@interface Widget : Base
|
||||
@end
|
||||
@implementation Widget
|
||||
@end
|
||||
@interface Decoy : NSObject
|
||||
- (void)render;
|
||||
@end
|
||||
@implementation Decoy
|
||||
- (void)render {}
|
||||
@end
|
||||
@interface Factory : NSObject
|
||||
+ (Widget *)make;
|
||||
@end
|
||||
@implementation Factory
|
||||
+ (Widget *)make { return nil; }
|
||||
- (void)run { [[Factory make] render]; }
|
||||
@end
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Base::render')).toEqual(['run']);
|
||||
expect(callerNamesOf('Decoy::render')).toEqual([]);
|
||||
});
|
||||
|
||||
it('creates NO edge when the factory return type lacks the method (silent miss)', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.m'),
|
||||
`@interface Bar : NSObject
|
||||
@end
|
||||
@implementation Bar
|
||||
@end
|
||||
@interface Other : NSObject
|
||||
- (void)onlyOther;
|
||||
@end
|
||||
@implementation Other
|
||||
- (void)onlyOther {}
|
||||
@end
|
||||
@interface Foo : NSObject
|
||||
+ (Bar *)create;
|
||||
@end
|
||||
@implementation Foo
|
||||
+ (Bar *)create { return nil; }
|
||||
- (void)run { [[Foo create] onlyOther]; }
|
||||
@end
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
// Bar has no onlyOther — must not mis-attach to the same-named Other::onlyOther.
|
||||
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves a singleton chain [[Cache shared] clearAll] whose factory returns nonnull instancetype', async () => {
|
||||
// The factory returns `nonnull instancetype` — the nullability qualifier must
|
||||
// be skipped (not captured AS the type), and an instancetype class-message
|
||||
// factory returns the receiver class, so clearAll resolves on Cache, never a
|
||||
// same-named decoy. (Regression for both: the captured-`nonnull` bug and the
|
||||
// ubiquitous `[[X alloc] init]` / singleton pattern.)
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'main.m'),
|
||||
`@interface Cache : NSObject
|
||||
+ (nonnull instancetype)shared;
|
||||
- (void)clearAll;
|
||||
@end
|
||||
@implementation Cache
|
||||
+ (nonnull instancetype)shared { return nil; }
|
||||
- (void)clearAll {}
|
||||
@end
|
||||
@interface Decoy : NSObject
|
||||
- (void)clearAll;
|
||||
@end
|
||||
@implementation Decoy
|
||||
- (void)clearAll {}
|
||||
@end
|
||||
@interface Caller : NSObject
|
||||
- (void)run;
|
||||
@end
|
||||
@implementation Caller
|
||||
- (void)run { [[Cache shared] clearAll]; }
|
||||
@end
|
||||
`
|
||||
);
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
expect(callerNamesOf('Cache::clearAll')).toEqual(['run']);
|
||||
expect(callerNamesOf('Decoy::clearAll')).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user