Procházet zdrojové kódy

feat(impact): extract RCT_EXPORT_METHOD / RCT_REMAP_METHOD as ObjC method nodes

`RCT_EXPORT_METHOD(foo:(...))` — the most common way to declare an iOS native
module method — parses as a macro-expression (an ERROR node), NOT a
`method_definition`, so the ObjC extractor never made a node for it. The iOS half
of a native module was invisible: a JS call to it couldn't resolve, and the
cross-platform pairing had nothing to pair. Only regular `- (void)foo` ObjC
methods were nodes.

The React Native bridge resolver now implements the `extract()` framework hook
for `.m`/`.mm` files: it reuses the existing `RCT_EXPORT_MODULE` /
`RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` source parser to emit a method node per
exported method, named by its JS-visible name (the selector's first keyword, or
the explicit `RCT_REMAP_METHOD` JS name) so it lines up with the Android
`@ReactMethod` method and the JS callsite. Node id is prefixed `rn-export:`.

Measured on react-native-device-info: JS→ObjC `calls` went 7 → 37, and Java↔ObjC
cross-platform pairs 22 → 29 (ObjC↔C++ 8 → 24) — the iOS bridge surface is now
visible and connected. Full suite green; node count grows only by the newly-
visible iOS methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry před 2 týdny
rodič
revize
d06a5ec

+ 8 - 1
__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(

+ 52 - 5
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<string, Native
 
 export const reactNativeBridgeResolver: FrameworkResolver = {
   name: 'react-native-bridge',
-  languages: ['javascript', 'typescript', 'tsx', 'jsx'],
+  // objc/mm included so `extract()` sees the native files — `resolve()` still
+  // only redirects JS callers (it returns null for native languages).
+  languages: ['javascript', 'typescript', 'tsx', 'jsx', 'objc'],
+
+  /**
+   * Extract `RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` declarations as method
+   * nodes. These macros parse as a macro-expression (an ERROR node), NOT a
+   * `method_definition`, so the ObjC extractor never made a node for them — the
+   * iOS half of a native module was invisible, so a JS call couldn't resolve to
+   * it and the cross-platform pairing had nothing to pair. The node is named by
+   * the JS-visible name (the selector's first keyword, or the explicit
+   * `RCT_REMAP_METHOD` JS name) so it matches the Android `@ReactMethod` method.
+   */
+  extract(filePath, source) {
+    if (!filePath.endsWith('.m') && !filePath.endsWith('.mm')) return { nodes: [], references: [] };
+    if (!/RCT_EXPORT_MODULE\b/.test(source)) return { nodes: [], references: [] };
+    const exports = parseObjcRNExports(source, findObjcClassName(source));
+    const now = Date.now();
+    const nodes: Node[] = [];
+    const seen = new Set<string>();
+    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