# Mixed iOS + React Native Bridging — Coverage Design **Audience:** a Claude agent (or human) continuing this work after #165 landed pure-Objective-C support. **Mission:** make codegraph's `trace` / `callers` / `callees` / `impact` / flow-context calls connect end-to-end across **cross-language runtime dispatch boundaries** that today silently break flows: **Swift ↔ Objective-C** in mixed iOS codebases, and **JavaScript ↔ native** in React Native / Expo apps. > This doc is the **plan**, not the implementation. No code lands on this > branch — only the design, the validation corpus, and the success bar. > Coding starts on a follow-up branch per phase. This work is the next item on the [dynamic-dispatch coverage playbook](./dynamic-dispatch-coverage-playbook.md) §6 matrix: row "Swift × Objective-C bridging" and a new "React Native bridge" row. Both are **resolver** patterns (named refs exist on both sides — the bridging rule is deterministic) — not synthesizer patterns. See §3a of the playbook for the reference Django ORM resolver. --- ## 1. Why this matters (the gap today) After #165, codegraph indexes Swift, Objective-C, and JavaScript/TypeScript each correctly **in isolation**. But the value is in cross-language flows — exactly where iOS apps and React Native apps live: - **Mixed iOS app:** `MyViewController.swift` calls `imageDownloader.download(url:completion:)`, which is `-[ImageDownloader downloadURL:completion:]` in `ImageDownloader.m`. Today: a `trace("MyViewController.viewDidLoad", "downloadURL:completion:")` call returns no path. The Swift callsite parses as a `call_expression` whose selector goes nowhere; the ObjC method exists as a node with no incoming edge. The agent reads both files to reconstruct the bridge. - **React Native app:** `useEffect(() => NativeModules.Geolocation.getCurrentPosition(cb))` in `App.js` reaches `RCT_EXPORT_METHOD(getCurrentPosition:(RCTResponseSenderBlock)cb)` in `RNCGeolocation.m`. Today: the JS callsite has no outgoing edge to the ObjC implementation; the ObjC handler has no incoming edge from JS. `impact(getCurrentPosition)` (ObjC side) shows no JS callers. - **Expo module:** `await ExpoCamera.takePictureAsync(options)` (JS) reaches `AsyncFunction("takePictureAsync") { ... }` in `ExpoCamera.swift` (Expo Modules API). Same break. In every case **a name exists on both sides** that an agent or a name-matcher can correlate — Swift's auto-bridged ObjC selector, `RCT_EXPORT_METHOD`'s literal first argument, an Expo `Function("name")` literal. The fix is a **resolver** that knows the bridging rules per channel and emits `references` edges with `provenance:'heuristic'` and `metadata.synthesizedBy:''`. The playbook's load-bearing warning applies here harder than usual: > **Partial coverage is WORSE than none.** Bridging one boundary but not the > next reveals a hop the agent then drills + reads to finish. Always close > the flow end-to-end and re-measure — never ship a half-bridged flow. For mixed iOS, this means **both directions** (Swift→ObjC and ObjC→Swift) and **all bridged kinds** (methods, properties, init/initializers, protocols) must close before measuring. For React Native, JS→native AND native→JS (`RCTEventEmitter`, `sendEvent`) must both close, AND on **both the legacy bridge and TurboModules**, or apps that mix them will half-bridge. --- ## 2. The bridging mechanisms to model Each row is a separate **dispatch channel** in the playbook's vocabulary — each gets its own resolver (or synthesizer if no static ref exists), its own validation, its own row in the §6 matrix. | # | Direction | Channel | Mapping rule | Where it lives | Difficulty | |---|---|---|---|---|---| | 1 | Swift → ObjC | direct call, ObjC class imported via `-Bridging-Header.h` | Swift call `obj.x(y:z:)` ↔ ObjC selector `-x:z:` (literal mapping, see §3a) | resolver in `frameworks/swift-objc.ts` | medium | | 2 | ObjC → Swift | `@objc` exposure | Swift `@objc func foo(bar:)` ↔ ObjC `-fooWithBar:` (auto-name); `@objc(custom:)` overrides | resolver in `frameworks/swift-objc.ts` | medium | | 3 | Swift ↔ ObjC | property/getter/setter bridging | Swift `var name: String` ↔ ObjC `-name` / `-setName:` | resolver in `frameworks/swift-objc.ts` | low | | 4 | Swift ↔ ObjC | initializer bridging | Swift `init(name:age:)` ↔ ObjC `-initWithName:age:` | resolver in `frameworks/swift-objc.ts` | low | | 5 | Swift ↔ ObjC | protocol bridging (`@objc protocol`) | conformance edges across language | resolver in `frameworks/swift-objc.ts` | medium | | 6 | JS → ObjC (RN legacy bridge) | `NativeModules..` ↔ `RCT_EXPORT_METHOD(:...)` or `RCT_REMAP_METHOD(, :...)` | name match keyed by `RCT_EXPORT_MODULE()` literal on the ObjC side | resolver in `frameworks/react-native.ts` | medium | | 7 | JS → Java/Kotlin (RN legacy bridge, Android) | `NativeModules..` ↔ `@ReactMethod` annotated method on a `ReactContextBaseJavaModule` subclass with `getName()` returning `` | resolver — same shape as #6, JVM side | medium | | 8 | JS ↔ native (RN TurboModules / Codegen) | `TurboModuleRegistry.get('Mod')` ↔ generated spec interface (`NativeMod` TS type) ↔ ObjC++/Kotlin impl matching the spec | resolver that reads the spec file as ground truth | hard | | 9 | Native → JS (events) | ObjC `[self sendEventWithName:@"x" body:b]` (extending `RCTEventEmitter`) ↔ JS `new NativeEventEmitter(NativeModules.Mod).addListener('x', cb)` | EventEmitter-style synthesizer (matches existing `callback-synthesizer.ts` for in-language EventEmitter) | medium | | 10 | JS → native (Expo modules) | JS `ExpoX.fn(args)` ↔ Swift `Function("fn") { ... }` or `AsyncFunction("fn") { ... }` inside a `Module` subclass with `Name("ExpoX")` | resolver in `frameworks/expo-modules.ts` | medium | | 11 | JS → native (Fabric view components) | JS `` ↔ ObjC/Swift `RCT_EXPORT_VIEW_PROPERTY(prop, ...)` or Codegen view spec | resolver + JSX hop (compose with existing JSX synthesizer) | hard (defer) | The **Difficulty** column drives phasing — see §6. ### 2a. Why these are resolvers, not synthesizers In every row, **the bridging rule is deterministic from a name**: - Swift's `@objc` exposure is a documented automatic mapping; `@objc(custom:)` is an explicit override; both are statically extractable. - `RCT_EXPORT_METHOD` takes a literal selector; `RCT_EXPORT_MODULE()` takes an optional literal module name (default: class name minus `RCT` prefix); `NativeModules.Mod.fn` is a literal-property access on a known global. - Expo Modules `Function("name") { ... }` and `Module { Name("ExpoX"); ... }` are literal strings inside `Module` definitions. - TurboModules spec interfaces are literal `Native` exports with `TurboModuleRegistry.get<...>('')`. So the work is: **extract the bridging-side names → make the resolver match them**. Same shape as `djangoResolver` resolving `_iterable_class` to `ModelIterable` — no whole-graph correlation pass needed. The one exception is **#9 native→JS events**, where the registration sites look very much like the in-language EventEmitter pattern the existing callback synthesizer already handles. Extending that synthesizer with a cross-language channel is the natural fit. --- ## 3. Concrete bridging rules (the reference table) ### 3a. Swift → ObjC selector mapping (auto) Swift uses standard rules to derive an ObjC selector from a Swift method: | Swift declaration | ObjC selector | |---|---| | `func greet()` | `greet` | | `func say(_ msg: String)` | `say:` | | `func set(name: String)` | `setWithName:` | | `func setName(_ name: String)` | `setName:` | | `func move(to point: CGPoint)` | `moveTo:` | | `func move(from a: CGPoint, to b: CGPoint)` | `moveFrom:to:` | | `init(name: String)` | `initWithName:` | | `init(name: String, age: Int)` | `initWithName:age:` | | `var name: String` (getter) | `name` | | `var name: String` (setter) | `setName:` | | `@objc(customSel:) func f(...)` | `customSel:` (explicit override) | The full rule set is at [Apple — Importing Swift into Objective-C](https://developer.apple.com/documentation/swift/importing-swift-into-objective-c) — specifically the "method name translation" and "initializer name translation" sections. The resolver implements this mapping in **one direction at extract time** (Swift declarations produce the bridged ObjC name, attached as an alias on the Swift method node), so name resolution on the ObjC side finds the Swift method through normal name-matching. ### 3b. React Native legacy bridge — name resolution ```objc // Native side (ObjC) @implementation RCTGeolocation RCT_EXPORT_MODULE(); // module name: "Geolocation" (RCT prefix stripped) RCT_EXPORT_METHOD(getCurrentPosition:(RCTResponseSenderBlock)cb) { ... } @end ``` ```js // JS side import { NativeModules } from 'react-native'; NativeModules.Geolocation.getCurrentPosition(cb); // resolves to the ObjC method above ``` Rule: 1. On the native side, extract a synthetic `module` node per class containing `RCT_EXPORT_MODULE()`. Name = explicit string argument if present, else class name with `RCT` prefix stripped. 2. Each `RCT_EXPORT_METHOD()` and `RCT_REMAP_METHOD(, )` becomes a method node attached to that module node, with the JS-visible name (``'s first keyword for `RCT_EXPORT_METHOD`, or `` for `RCT_REMAP_METHOD`). 3. On the JS side, the resolver matches the literal property chain `NativeModules..` against `(module, jsName)` pairs from the native side. 4. Resolver emits `references` (`provenance:'heuristic'`, `synthesizedBy:'rn-bridge'`) from the JS callsite to the native method. ### 3c. React Native TurboModule — name resolution ```ts // Spec (TS) — codegen ground truth export interface Spec extends TurboModule { getCurrentPosition(cb: (loc: Location) => void): void; } export default TurboModuleRegistry.getEnforcing('Geolocation'); ``` ```objc // ObjC++ impl @implementation RCTGeolocation - (void)getCurrentPosition:(RCTResponseSenderBlock)cb { ... } @end ``` ```js import Geolocation from './NativeGeolocation'; Geolocation.getCurrentPosition(cb); // resolves to the ObjC method via the spec ``` Rule: 1. The spec file is the source of truth: parse `TurboModuleRegistry.get*('')` to find the module name, then read the `Spec` interface methods. 2. Match each spec method to the native impl's same-named method (by selector first-keyword, in the class identified by name convention or by reading any `JSI_EXPORT_MODULE` macro if present). 3. JS imports of the spec file get name resolution through the spec. 4. Emits the same `references` edges as #3b, with `synthesizedBy:'rn-turbomodule'`. ### 3d. Expo Modules — name resolution ```swift // Native (Swift, expo-modules-core API) public class ExpoCameraModule: Module { public func definition() -> ModuleDefinition { Name("ExpoCamera") AsyncFunction("takePictureAsync") { (options: CameraOptions) in /* ... */ } View(ExpoCameraView.self) { Prop("type") { (view: ExpoCameraView, type: String) in /* ... */ } } } } ``` ```js import { requireNativeModule } from 'expo-modules-core'; const ExpoCamera = requireNativeModule('ExpoCamera'); await ExpoCamera.takePictureAsync({ quality: 1 }); ``` Rule: 1. On the native side: a class extending `Module` whose `definition()` (or `init { /* DSL */ }` for newer API) contains a `Name("X")` call defines the module. Each `Function("y")` / `AsyncFunction("y")` literal defines a method. The trailing closure is the implementation body — extract as a method node named `y`, attached to module `X`. 2. On the JS side: `requireNativeModule('X')` produces a binding; resolve property accesses on it to the named methods. 3. `Prop("name")` for view modules behaves like RN's `RCT_EXPORT_VIEW_PROPERTY` — defer with the rest of the view-component frontier. --- ## 4. What edges need to exist For each channel, the closed flow is: - **JS callsite → bridged-method-node** (`references`, heuristic, `synthesizedBy:''`) - **Bridged-method-node → native-impl-method** (already extracted; for #6/#7 the bridged-method IS the native impl; for #10 the closure body IS the impl) - **Native-impl-method → its own callees** (already extracted in-language) For Swift↔ObjC specifically, the cleanest model is **alias-name on the declaration node**: extend Swift method extraction to compute the ObjC auto-bridged name and store it as an alternate name the resolver considers. No new edges between Swift and ObjC method nodes are needed — normal name resolution suffices because both sides agree on the bridged selector after extraction. The MCP read tools surface heuristic edges inline already (see `metadata.synthesizedBy` plumbing from #312/#403); these new edges ride that path with no additional plumbing. --- ## 5. Validation corpus (the small/medium/large bar) Following CLAUDE.md's validation methodology — **≥3 flow prompts each on small / medium / large repos, with deterministic probes + agent A/B, ≥2 runs/arm**. Picks below are candidates to commit to in the implementation branch; the implementation PR confirms the choices after verifying each repo still builds an index cleanly. ### 5a. Mixed iOS (Swift+ObjC) — pick 3 | Tier | Repo | Why | Canonical flow | |---|---|---|---| | **Small** | [Charts](https://github.com/danielgindi/Charts) (~150 files Swift+ObjC) | Swift-first lib with ObjC compatibility layer; well-known | "How does setting `data` on a `ChartView` reach the renderer?" | | **Small (alt)** | [Lottie-ios](https://github.com/airbnb/lottie-ios) (~300 files, was mixed; current may be pure-Swift — verify) | Animation engine, well-known mix | "How does `AnimationView.play()` reach the layer compositor?" | | **Medium** | [Realm-Cocoa](https://github.com/realm/realm-swift) (~500 files) | Heavy Swift-on-top-of-ObjC: Swift API wraps an ObjC core that wraps C++ Realm Core | "How does `Realm.write { realm.add(obj) }` reach the ObjC persistence layer?" | | **Large** | [Wikipedia-iOS](https://github.com/wikimedia/wikipedia-ios) (~2500 Swift+ObjC files) | Real app, deeply mixed, active development | "How does tapping a search result reach the article-fetch network call?" | | **Large (alt)** | [WordPress-iOS](https://github.com/wordpress-mobile/WordPress-iOS) | Heavier ObjC legacy + Swift additions | "How does a new-post draft save reach Core Data persistence?" | Bar per repo: 1. Pure-language probes still pass (Swift-in-Swift trace; ObjC-in-ObjC trace) — no regression vs #165's pure-ObjC baseline. 2. **Cross-language probe passes:** the canonical flow above traces end-to-end with `trace`, no break at the language boundary. 3. **Agent A/B (with vs without codegraph, ≥2 runs/arm):** Read = 0 within the explore-call budget; faster than without-codegraph; no regression on a pure-Swift or pure-ObjC control repo (e.g. Texture). 4. **No node-count explosion** vs pre-bridging baseline (`select count(*) from nodes` before/after). ### 5b. React Native — pick 3 | Tier | Repo | Why | Canonical flow | |---|---|---|---| | **Small** | [react-native-svg](https://github.com/software-mansion/react-native-svg) (~100 files JS+ObjC+Java) | Small, well-scoped native module set | "How does setting `` reach the iOS Core Graphics call?" | | **Medium** | [react-native-screens](https://github.com/software-mansion/react-native-screens) (~300 files, JS+native) | Real navigation primitives, both legacy bridge and Fabric | "How does navigating to a new screen reach UINavigationController?" | | **Medium (alt)** | [react-native-firebase](https://github.com/invertase/react-native-firebase) (~1000 files across packages) | Many native modules, both platforms — stresses module discovery | "How does `firestore().collection('x').get()` reach the iOS Firebase SDK call?" | | **Large** | [facebook/react-native](https://github.com/facebook/react-native) RNTester subset (~3000 files) | The framework itself + sample app; canonical bridge usage | "How does pressing a button in RNTester's GeolocationExample reach the iOS Core Location call?" | Bar per repo: 1. Pure-JS probes unchanged (`useState` → re-render flow still resolves — existing react synthesizer not regressed). 2. **JS → ObjC bridge probe passes** for ≥1 known RCT_EXPORT_METHOD on each repo. 3. **JS → TurboModule probe passes** on a repo that uses TurboModules (react-native main has both; pick one of each). 4. **Native → JS event probe passes** for ≥1 emitter (NativeEventEmitter pattern). 5. **Agent A/B** as above. Critical: a question that *crosses the bridge* (e.g. "how does pressing Button X reach the network call") must drop Read to 0 in ≥1 run with codegraph. 6. **No regression** on a pure-JS control repo (existing react-realworld / excalidraw measurements unchanged). ### 5c. Expo — pick 2 (smaller scope, narrower API surface) | Tier | Repo | Why | |---|---|---| | **Small/Medium** | [expo/expo](https://github.com/expo/expo) — one SDK module like `expo-camera` or `expo-location` | The cleanest Expo Modules API examples; live | | **Large** | full `expo/expo` monorepo (all SDK modules + the JS API) | Stress-test module-name resolution across many packages | Canonical flow: "How does `await Camera.takePictureAsync()` (JS) reach the native camera API call (Swift `AVCaptureSession` or Kotlin `CameraDevice`)?" --- ## 6. Phasing — what comes first Per the playbook's difficulty gradient and the half-bridge rule, the order is fixed by what closes a flow end-to-end on the **smallest repo first**. ### Phase 1 — Swift ↔ ObjC bridging (rows 1–5 above) Smallest scope, deterministic name mapping, no JS involved. Validate on the Charts/Realm/Wikipedia corpus before moving on. **Don't proceed to Phase 2 until Phase 1 passes the §5a bar on all three repos.** ### Phase 2 — React Native legacy bridge (rows 6–7, ObjC + Java/Kotlin) Both iOS and Android sides must close in the same PR — half-bridging one platform reveals the half-coverage hop on the other and the agent reads. Validate on the §5b corpus. ### Phase 3 — Native → JS events (row 9) Extends the existing callback synthesizer with a cross-language channel. Validate on the same §5b corpus (most RN libs use at least one event emitter). ### Phase 4 — Expo Modules (row 10) Layered on Phase 1's Swift extraction. Smaller corpus (§5c). ### Phase 5 — RN TurboModules / Codegen (row 8) Requires reading the spec file as cross-language ground truth. Validate on the §5b corpus's TurboModule users (react-native main, post-0.73 libs). ### Phase 6 — Fabric view components (row 11) Deferred — composes with the existing JSX synthesizer and the view side of TurboModules. Address when ≥1 of the §5b corpus repos has its bridge otherwise closed but a Fabric flow still breaks. --- ## 7. Anti-goals (what we will not try to do) - **Android Kotlin/Java extraction quality** — out of scope. We use what Kotlin/Java extractors already produce. If they miss a `@ReactMethod` annotation's literal name we may add a tiny extractor refinement, but we do not redesign JVM extraction. - **Dynamic / computed bridge keys** — `NativeModules[someVar]`, `requireNativeModule(name)` where `name` is a parameter, etc. We only resolve literal-key access (matches the [agent-eval Lua frontier](./dynamic-dispatch-coverage-playbook.md) — anonymous-only patterns deferred). - **Bridging-header file content parsing** — we *do* index `.h` files (already does via #165's content sniff) but we do **not** parse the bridging header's `#import` list as a special "what's visible to Swift" manifest. Treat it as a normal ObjC header. - **Runtime dispatch on `performSelector:`** — out of scope; matches the same "named-only" anti-goal. - **JSI (raw, non-TurboModule)** — out of scope. Apps using bare JSI call into native through a custom `Host*` interface that has no documented declarative spec. Wait for those apps to migrate to TurboModules. - **Swift-only generics over ObjC protocols / Swift extensions on ObjC classes** — extension methods are still callable in ObjC if `@objc`, so they go through the same Phase 1 path. Generics are not — we silently miss them. Acceptable; matches Java/Kotlin generics frontier. --- ## 8. Coverage-matrix entries — measured | Language | Framework | Canonical flow | Mechanism | Status | |---|---|---|---|---| | Swift × Objective-C | bridging | Swift call → ObjC selector; ObjC call → @objc Swift method | R | ✅ Phase 1 (§8a) | | JavaScript × Objective-C/Java/Kotlin | React Native legacy bridge | `NativeModules..` → `RCT_EXPORT_METHOD` / `@ReactMethod` | R | ✅ Phase 2 (§8b) | | JavaScript × native | React Native TurboModules | spec interface ↔ impl | R (spec as ground truth) | ✅ partial — name-match path lands (§8b) | | Objective-C/Java/Kotlin → JavaScript | React Native event emitters | `[self sendEventWithName:]` → `addListener` | S (cross-lang channel) | ✅ Phase 3 (§8e) | | JavaScript × Swift/Kotlin | Expo Modules | `requireNativeModule('X').fn(...)` → `Function("fn") { }` | R (extract synthesizes method nodes) | ✅ Phase 4 (§8f) | | JavaScript × native | React Native Fabric views | `` → Codegen spec component + NativeProps | R (extract) + S (native-impl) + JSX | ✅ Phase 6 (§8g) | ### 8a. Phase 1 measurements — Swift ↔ ObjC | Repo | Source files | Bridge edges (framework-resolved) | Sample edges | |---|---|---|---| | **Charts** (small) | 269 (205 Swift + 59 ObjC/.h) | 28 objc→swift, 1 swift→objc | `handleOption:forChartView:` → `animate` · `setupPieChartView:` → `setExtraOffsets` · `setDataCount:range:` → `setColor` | | **realm-swift** (medium) | 369 (151 Swift + 218 ObjC family) | 36 objc→swift, 1185 swift→objc | `valueForUndefinedKey:` → `get` · `setValue:forUndefinedKey:` → `set` · `promote:on:` → `initialize` | | **wikipedia-ios** (large) | 1734 (1234 Swift + 500 ObjC/.h) | 52 objc→swift, 983 swift→objc | real-iOS-app bridging across many feature modules | All three: in-language baselines unchanged, no node-count explosion, `trace` connects canonical flows across the boundary (verified on Charts: `trace(handleOption:forChartView:, animate)` surfaces the bridge edge directly). ### 8b. Phase 2 + 5 (partial) measurements — React Native bridge | Repo | Source files | Bridge edges (framework-resolved) | Notes | |---|---|---|---| | **react-native-svg** (small/medium) | ~700 (93 .mm + 115 .java + 6 .kt + 49 js + 92 ts + 154 tsx) | 9 tsx→java via TurboModule spec | RNSvg's iOS uses TurboModule auto-gen (no `RCT_EXPORT_METHOD`); resolutions land on Java. All 9 precise: `isPointInStroke`, `isPointInFill`, `getTotalLength`, `getPointAtLength`, `getCTM`, `getScreenCTM`, `getBBox`, `toDataURL`. | | **AsyncStorage** (small, pure legacy bridge) | ~60 (28 kt + 2 mm + 16 ts + 14 tsx + …) | **8/8 precise** | The canonical legacy bridge test — Kotlin `@ReactMethod` + ObjC `RCT_EXPORT_METHOD`. JS `setItem` → Kotlin `legacy_multiSet`; `getItem` → `legacy_multiGet`; `clear` → `legacy_clear`; etc. | | **react-native-firebase** (large) | ~1100 (111 .java + 63 .m + 13 .mm + 239 js + 427 ts + 9 tsx) | 18 after RCTEventEmitter blocklist (was 78 before) | Initial 78 included 60 false positives targeting `addListener:` / `remove:` (every RCTEventEmitter declares them; every JS call to `.addListener(...)` resolved into noise). Blocklist cut to 18, all precise: `httpsCallable:region:emulatorHost:...`, `signInWithProvider`, `configureProvider`, `removeFunctionsStreaming:`. | | **react-native-screens** (medium) | 1211 | 0 — empty TurboModule spec, no `RCT_EXPORT_METHOD`, all Fabric/Codegen view-side | RNScreens lives entirely in Phase 6 (Fabric, deferred). The bridge declining to over-match here is the right behavior. | ### 8c. Architectural fix discovered during validation The resolver's `initialize()` runs at CodeGraph construction — before any files are indexed — so framework resolvers whose `detect()` consults the indexed file list (UIKit / SwiftUI scanning for imports, `swift-objc-bridge` looking for both Swift and ObjC files, `react-native-bridge` looking for RN markers) all returned false on that initial pass and silently dropped themselves. This affected every framework resolver in the codebase that read `context.getAllFiles()` / `context.readFile()` rather than scanning the filesystem directly — a pre-existing latent bug, not bridge-specific. Fixed: `indexAll()` now calls `resolver.initialize()` after extraction completes, so detect() runs against the populated index. ### 8d. Bridge-precision blocklists (lessons learned) | Bridge | Blocked names | Reason | |---|---|---| | swift-objc | `init`, `description`, `hash`, `isEqual`, `copy`, `count`, `value`, `data`, `string`, `object`, `add`, `remove`, `update`, `load`, `save`, `reload`, `cancel`, `start`, `stop`, `pause`, `resume`, `close`, `open`, `show`, `hide`, `dealloc`, `release`, `retain`, `autorelease`, … | Every NSObject subclass implements these; bridging them to arbitrary project-local ObjC methods produces noise. Regular name-matcher handles them on its own. | | react-native | `addListener`, `removeListeners`, `remove`, `invalidate`, `startObserving`, `stopObserving` | Every `RCTEventEmitter` subclass declares these via `RCT_EXPORT_METHOD`. JS callers of `.addListener(...)` / `.remove(...)` go through `NativeEventEmitter` (JS abstraction), not the native bridge directly. | ### 8e. Phase 3 measurements — RN native → JS event channel Synthesizer pattern; extends `src/resolution/callback-synthesizer.ts` with a cross-language event channel keyed by literal event name. Validates on **RNFirebase** (large): | Synthesized event channel | Edges | Sample | |---|---|---| | `messaging_message_received` | 2 | `application:didReceiveRemoteNotification:fetchCompletionHandler:` → TS `onMessage` (and the `UNUserNotificationCenter` willPresent variant → same `onMessage`) | | `messaging_notification_opened` | 1 | `userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:` → TS `onNotificationOpenedApp` | Each edge is `provenance:'heuristic'`, `metadata.synthesizedBy:'rn-event-channel'`. Same `EVENT_FANOUT_CAP = 6` as the in-language channel — generic event names with too many handlers or dispatchers skip rather than over-link. The synthesizer also handles the **subscribe-wrapper pattern** common in RN libraries (`messaging().onMessage(listener)` where `listener` is a parameter that flows up to user code): when the JS handler arg isn't a named symbol, it attributes the listener to the ENCLOSING JS function (reachability-correct, attributes to the abstraction layer). ### 8f. Phase 4 measurements — Expo Modules Framework `extract()` parses Swift / Kotlin source for literal `Function("X") { … }` / `AsyncFunction("X") { … }` / `Property("X") { … }` / `Constants` declarations inside `class X: Module` (or `: Module()` in Kotlin) and emits a `method` node named `X` per literal. The standard name-matcher resolves JS callsites like `Foo.takePictureAsync(...)` to these synthetic nodes via the existing `obj.method` → method-name path. Validated on real Expo SDK packages: | Package | Files indexed | Expo method nodes extracted | Cross-language edges | |---|---|---|---| | **expo-haptics** | 14 | 6 (3 Swift + 3 Kotlin: `notificationAsync`, `impactAsync`, `selectionAsync` / `performHapticsAsync`) | Module nodes registered; consumer-app callers resolve via name-match | | **expo-camera** | 72 | 41 (Swift + Kotlin; covers `takePictureAsync`, `record`, `resumePreview`, `getAvailableLenses`, `scanFromURLAsync`, `requestCameraPermissionsAsync`, view-side `width` / `height` properties, …) | 9 swift→expo, 7 kotlin→expo internal edges. JS-side callsites in the package shadow the native names with TS wrappers (`pausePreview()` defined on `CameraView.tsx`); name-match correctly prefers the local TS method. An external consumer app of `Camera.takePictureAsync()` resolves through to the native method directly. | Five tests cover the extractor + an end-to-end fixture: `JS callsite of literal AsyncFunction("uniqueExpoHapticCall") resolves to the native impl node` — confirms the resolver-free bridge path works when names aren't shadowed. ### 8g. Phase 6 measurements — Fabric / Codegen view components Two-part design: 1. **Framework extractor** (`src/resolution/frameworks/fabric.ts`) — parses TS / TSX spec files for `codegenNativeComponent('Name', ...)` declarations. Emits: - One `component` node per declaration (named after the JS-visible component name; matches the JSX synthesizer's name+kind filter). - One `property` node per declared field of the `NativeProps` interface — surfacing JSX-callable props like `onTap`, `nativeContainerBackgroundColor` as discoverable graph nodes. 2. **Synthesizer** (`fabricNativeImplEdges` in `callback-synthesizer.ts`) — walks every `fabric-component:*` node and looks for a native class matching its name with one of RN's convention suffixes (empty / `View` / `ViewManager` / `ComponentView` / `Manager`). Emits a `calls` edge with `metadata.synthesizedBy:'fabric-native-impl'` from the component to each match. The convention is precise enough that there's no name collision in well-formed RN libraries. Combined with the existing `reactJsxChildEdges` JSX synthesizer, this closes the full JSX → native flow: consumer-app JSX `` → Fabric `component` node `MyView` → native class `MyViewView` (or `MyViewManager` / `MyViewComponentView` / …). Re-validated on **react-native-screens** (the corpus repo that was entirely Fabric and showed 0 bridges in Phase 2): | Metric | Count | |---|---| | `codegenNativeComponent` spec declarations | 54 | | Fabric component nodes extracted | 27 (one per non-web spec; the `*.web.ts` variants are filtered out by spec validity) | | Fabric prop nodes extracted | 272 (the full NativeProps interface surface across all components) | | `fabric-native-impl` bridge edges | 68 | Sample bridge edges: | JS component | Native class | Suffix | |---|---|---| | `RNSFullWindowOverlay` | `RNSFullWindowOverlay` (ObjC) | (exact) | | `RNSFullWindowOverlay` | `RNSFullWindowOverlayManager` (ObjC) | `Manager` | | `RNSModalScreen` | `RNSModalScreenManager` (ObjC) | `Manager` | | `RNSScreenContainer` | `RNSScreenContainerView` (ObjC) | `View` | Four tests cover the extractor + a full end-to-end fixture (`App (TSX) → MyView (fabric-component) → MyViewView (ObjC class)`) that asserts the JSX→component edge AND the component→native-class edge both exist after indexing. --- ## 9. Open questions to settle in Phase 1 These are not blocking the start of Phase 1 — they're the first things to decide *while* writing the Swift↔ObjC resolver: 1. **Alias on declaration vs new bridge edge?** Storing the auto-bridged ObjC selector as an alternate name on the Swift method node is cheaper and aligns with how name resolution already works. The alternative (synthesize a cross-language `references` edge between matching nodes) is more explicit in `trace` output but adds N edges per `@objc` symbol. **Default: alias.** Verify the alias surfaces in `callers`/`callees`/`trace` results. 2. **How does `trace` display a cross-language hop?** The MCP `trace` tool inlines each hop's body. A Swift → ObjC hop should make this obvious in the rendered output ("Swift `func foo(bar:)` → bridged to ObjC selector `-fooWithBar:` → ObjC `-[ImageDownloader fooWithBar:]`"). Will likely need a small renderer tweak in `trace.ts` to label the bridge. 3. **Where do the resolver bridging rules live?** Suggest a `src/resolution/frameworks/swift-objc.ts` for the auto-name mapping (a pure function) imported by both the Swift extractor (to compute the alias at extract time) and tests. Keeps the mapping in one place. 4. **What about `@objcMembers`?** Class-level export — applies to all members unless `@nonobjc`. Handle by checking the class's modifiers in the Swift extractor and defaulting each member's `@objc`-ness from that. --- ## 10. Done-bar (so we know when to stop) Phase 1 (Swift↔ObjC) is done when: - All three §5a corpora pass: pure-language probes unchanged; cross-language canonical flow probe finds the path end-to-end; agent A/B shows Read = 0 in ≥1 run with codegraph, faster than without. - Coverage matrix row in §6 of the playbook is filled in with numbers. - A CHANGELOG `[Unreleased]` entry exists, written user-side. Each subsequent Phase has the same shape — its own §5 corpus, its own matrix row, its own CHANGELOG entry — and **doesn't ship until the previous one passes**. Half-bridges are not optional to avoid here; they actively make codegraph worse on these codebases than not having any bridging at all.