Files
codegraph/__tests__/objc-message-scope.test.ts
T
Colby Mchenry b238e240e2 fix(objc): a message reached by name alone must be in reach (#2150)
* 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>
2026-09-30 10:33:32 +00:00

119 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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']);
});
});