diff --git a/__tests__/react-native-bridge.test.ts b/__tests__/react-native-bridge.test.ts index 0760d7bd9..dec3ce5cb 100644 --- a/__tests__/react-native-bridge.test.ts +++ b/__tests__/react-native-bridge.test.ts @@ -314,12 +314,19 @@ describe('React Native cross-platform pairing — end to end', () => { fs.writeFileSync(path.join(dir, 'RNThing.m'), "@implementation RNThing\n" + "RCT_EXPORT_MODULE()\n" + - "- (void)uniquePingMethod:(RCTResponseSenderBlock)cb {}\n@end\n"); + "RCT_EXPORT_METHOD(uniquePingMethod:(RCTResponseSenderBlock)cb) {}\n@end\n"); const cg = await CodeGraph.init(dir, { silent: true }); await cg.indexAll(); const db = (cg as any).db.db; + // The iOS `RCT_EXPORT_METHOD` is extracted as an ObjC method node (the macro + // parses as a macro-expression, not a method, so it had no node before). + const objc = db.prepare( + "SELECT * FROM nodes WHERE name='uniquePingMethod' AND language='objc' AND id LIKE 'rn-export:%'" + ).all(); + expect(objc).toHaveLength(1); + // The Java and ObjC impls of `uniquePingMethod` are linked to each other, so // a JS call that resolves to one platform reaches the other. const pair = db.prepare( diff --git a/src/resolution/frameworks/react-native.ts b/src/resolution/frameworks/react-native.ts index c0e0d764c..f55fa4e18 100644 --- a/src/resolution/frameworks/react-native.ts +++ b/src/resolution/frameworks/react-native.ts @@ -99,8 +99,8 @@ function defaultObjcModuleName(className: string): string { function parseObjcRNExports( source: string, className: string | null -): Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string }> { - const results: Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string }> = []; +): Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string; line: number }> { + const results: Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string; line: number }> = []; // RCT_EXPORT_MODULE — one per file by convention. Capture the optional arg. const moduleMatch = source.match(/RCT_EXPORT_MODULE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)?\s*\)/); @@ -111,6 +111,12 @@ function parseObjcRNExports( (className ? defaultObjcModuleName(className) : null); if (!moduleName) return results; + const lineOf = (idx: number): number => { + let line = 1; + for (let i = 0; i < idx && i < source.length; i++) if (source.charCodeAt(i) === 10) line++; + return line; + }; + // RCT_EXPORT_METHOD(selectorFirstKw:(args)…) // The first keyword (everything up to the first `:` or open paren) is the // JS-visible name. We don't try to parse full multi-keyword selectors — @@ -119,7 +125,7 @@ function parseObjcRNExports( let m: RegExpExecArray | null; while ((m = exportRegex.exec(source)) !== null) { const kw = m[1]; - if (kw) results.push({ moduleName, jsName: kw, nativeSelectorFirstKw: kw }); + if (kw) results.push({ moduleName, jsName: kw, nativeSelectorFirstKw: kw, line: lineOf(m.index) }); } // RCT_REMAP_METHOD(jsName, nativeSelectorFirstKw:(args)…) @@ -129,7 +135,7 @@ function parseObjcRNExports( const jsName = m[1]; const nativeKw = m[2]; if (jsName && nativeKw) { - results.push({ moduleName, jsName, nativeSelectorFirstKw: nativeKw }); + results.push({ moduleName, jsName, nativeSelectorFirstKw: nativeKw, line: lineOf(m.index) }); } } @@ -355,7 +361,48 @@ function buildRNMaps(context: ResolutionContext): { byJsName: Map(); + for (const e of exports) { + if (seen.has(e.jsName)) continue; + seen.add(e.jsName); + nodes.push({ + id: `rn-export:${filePath}:${e.moduleName}.${e.jsName}`, + kind: 'method', + name: e.jsName, + qualifiedName: `${filePath}::${e.moduleName}.${e.jsName}`, + filePath, + language: 'objc', + startLine: e.line, + endLine: e.line, + startColumn: 0, + endColumn: 0, + isExported: true, + docstring: `RCT_EXPORT_METHOD ${e.nativeSelectorFirstKw} (module ${e.moduleName})`, + signature: `RCT_EXPORT_METHOD(${e.nativeSelectorFirstKw}:…)`, + updatedAt: now, + }); + } + return { nodes, references: [] }; + }, /** * Detect: package.json depends on `react-native`, OR any source file