Skip to content

Commit 60b12b1

Browse files
committed
fix(resolution): stream native bridge method scans
1 parent bc1790e commit 60b12b1

4 files changed

Lines changed: 65 additions & 8 deletions

File tree

__tests__/react-native-bridge.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ function makeContext(nodes: Node[], fileContents: Record<string, string> = {}):
2727
getNodesByName: (name) => byName.get(name) ?? [],
2828
getNodesByQualifiedName: () => { throw new Error('not used'); },
2929
getNodesByKind: (kind) => nodes.filter((n) => n.kind === kind),
30+
iterateNodesByKind: function* (kind) {
31+
yield* nodes.filter((n) => n.kind === kind);
32+
},
3033
getNodesByLowerName: () => { throw new Error('not used'); },
3134
fileExists: (fp) => allFiles.has(fp),
3235
readFile: (fp) => fileContents[fp] ?? null,
@@ -121,6 +124,27 @@ describe('React Native bridge resolver', () => {
121124
expect(result?.resolvedBy).toBe('framework');
122125
});
123126

127+
it('streams method nodes when building the bridge map instead of materializing every method', () => {
128+
const native = method('getCurrentPosition:', 'objc', 'RCTGeolocation.m');
129+
const ctx = {
130+
...makeContext([native], {
131+
'package.json': '{"dependencies":{"react-native":"^0.73"}}',
132+
'RCTGeolocation.m':
133+
'@implementation RCTGeolocation\n' +
134+
'RCT_EXPORT_MODULE()\n' +
135+
'RCT_EXPORT_METHOD(getCurrentPosition:(RCTResponseSenderBlock)cb) {}\n' +
136+
'@end',
137+
}),
138+
getNodesByKind: () => { throw new Error('full method scan should not be used'); },
139+
} satisfies ResolutionContext;
140+
141+
const result = reactNativeBridgeResolver.resolve(
142+
ref('getCurrentPosition', 'javascript', 'App.js'),
143+
ctx
144+
);
145+
expect(result?.targetNodeId).toBe(native.id);
146+
});
147+
124148
it('resolves via explicit module name in RCT_EXPORT_MODULE(name)', () => {
125149
const native = method('startScan:', 'objc', 'Bluetooth.m');
126150
const ctx = makeContext([native], {

__tests__/swift-objc-bridge-resolver.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ function makeContext(nodes: Node[], fileContents: Record<string, string> = {}):
2121
getNodesByName: (name) => byName.get(name) ?? [],
2222
getNodesByQualifiedName: () => { throw new Error('not used'); },
2323
getNodesByKind: (kind) => nodes.filter((n) => n.kind === kind),
24+
iterateNodesByKind: function* (kind) {
25+
yield* nodes.filter((n) => n.kind === kind);
26+
},
2427
getNodesByLowerName: () => { throw new Error('not used'); },
2528
fileExists: (fp) => allFiles.has(fp),
2629
readFile: (fp) => fileContents[fp] ?? null,
@@ -113,6 +116,20 @@ describe('swiftObjcBridgeResolver integration', () => {
113116
expect(result?.confidence).toBe(0.6);
114117
});
115118

119+
it('streams ObjC method nodes when building the bridge map', () => {
120+
const objcTarget = method('fetchEntryForKey:', 'objc', 'Cache.m');
121+
const ctx = {
122+
...makeContext([objcTarget]),
123+
getNodesByKind: () => { throw new Error('full method scan should not be used'); },
124+
} satisfies ResolutionContext;
125+
126+
const result = swiftObjcBridgeResolver.resolve(
127+
ref('fetchEntry', 'swift', 'Caller.swift'),
128+
ctx
129+
);
130+
expect(result?.targetNodeId).toBe(objcTarget.id);
131+
});
132+
116133
it('does NOT bridge generic Cocoa names like "init" or "description"', () => {
117134
// Bridging Swift `init()` calls to arbitrary ObjC `init*:` methods is
118135
// noise — every NSObject subclass has them. The regular name-matcher

src/resolution/frameworks/react-native.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ const nativeMethodMaps: WeakMap<
6262
{ byJsName: Map<string, NativeMethod[]> }
6363
> = new WeakMap();
6464

65+
function methodNodes(context: ResolutionContext): Iterable<Node> {
66+
return context.iterateNodesByKind
67+
? context.iterateNodesByKind('method')
68+
: context.getNodesByKind('method');
69+
}
70+
6571
// ─── Native-side extraction ─────────────────────────────────────────────────
6672

6773
/**
@@ -270,7 +276,7 @@ function buildRNMaps(context: ResolutionContext): { byJsName: Map<string, Native
270276
// their bridge exports.
271277
const objcMethodsByFirstKw = new Map<string, Node[]>();
272278
const jvmMethodsByName = new Map<string, Node[]>();
273-
for (const node of context.getNodesByKind('method')) {
279+
for (const node of methodNodes(context)) {
274280
if (node.language === 'objc') {
275281
const firstKw = node.name.includes(':') ? node.name.split(':')[0] : node.name;
276282
if (firstKw) {

src/resolution/frameworks/swift-objc.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ const objcByCandidateSwiftBase: WeakMap<
5252
Map<string, Node[]>
5353
> = new WeakMap();
5454

55+
function methodNodes(context: ResolutionContext): Iterable<Node> {
56+
return context.iterateNodesByKind
57+
? context.iterateNodesByKind('method')
58+
: context.getNodesByKind('method');
59+
}
60+
5561
/**
5662
* Build the reverse-bridge map: for every ObjC method node in the graph,
5763
* compute the Swift base names that would auto-bridge to its selector and
@@ -118,10 +124,8 @@ function buildObjcMap(context: ResolutionContext): Map<string, Node[]> {
118124
if (cached) return cached;
119125

120126
const map = new Map<string, Node[]>();
121-
const objcMethods = context
122-
.getNodesByKind('method')
123-
.filter((n) => n.language === 'objc');
124-
for (const node of objcMethods) {
127+
for (const node of methodNodes(context)) {
128+
if (node.language !== 'objc') continue;
125129
const candidates = swiftBaseNamesForObjcSelector(node.name);
126130
for (const c of candidates) {
127131
// Skip the trivial case where the Swift base name equals the ObjC
@@ -227,9 +231,15 @@ function resolveObjcCallToSwift(
227231

228232
const candidates = swiftBaseNamesForObjcSelector(rawSelector);
229233
for (const candidate of candidates) {
230-
const matches = context
231-
.getNodesByName(candidate)
232-
.filter((n) => n.language === 'swift' && (n.kind === 'method' || n.kind === 'function'));
234+
const matches = context.getNodesByNameFiltered
235+
? context.getNodesByNameFiltered(candidate, {
236+
language: 'swift',
237+
kinds: ['method', 'function'],
238+
limit: 2000,
239+
})
240+
: context
241+
.getNodesByName(candidate)
242+
.filter((n) => n.language === 'swift' && (n.kind === 'method' || n.kind === 'function'));
233243
for (const match of matches) {
234244
const window = declarationSourceWindow(match, context);
235245
if (isObjcExposed(window)) {

0 commit comments

Comments
 (0)