fix(resolution): a framework-resolved edge names its resolver (#2159)

The README said every bridge hop carries `metadata.synthesizedBy`, but only
the synthesized channels (rn-event-channel, fabric-native-impl) did; a
resolver-made hop (Swift↔ObjC, RN legacy / TurboModules, Expo Modules JS)
carried only `resolvedBy: 'framework'`, and `expo-module-extract` named
nothing that exists. The framework loop now adds `metadata.framework` (the
resolver's name: swift-objc-bridge, react-native-bridge, expo-modules-js,
fabric-view, rails, laravel, …) and the README says what each kind carries.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 11:36:02 +00:00
committed by GitHub
parent 9269f92dd5
commit 5ba3674629
4 changed files with 64 additions and 2 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- A link made by a framework or bridge resolver now records which one made it (`metadata.framework`, such as `swift-objc-bridge` or `react-native-bridge`), as the README describes. Before, only synthesized event and view channels were named.
- ASP.NET apps built with FastEndpoints now have their routes: each endpoint class's `Get(…)` / `Post(…)` in `Configure()` becomes a route linked to its own `HandleAsync` / `ExecuteAsync`, including paths kept in a request class's `Route` constant. ardalis/CleanArchitecture went from no routes to 24. Minimal API routes written without a leading slash (`app.MapGet("api/todos", …)`) are now named `/api/todos`.
- Vapor routes whose handler is a trailing closure (`app.get("hello") { req in … }`), WebSocket routes (`app.webSocket("chat") { … }`) and `routes.on(.POST, "x", use: handler)` registrations are now found. Only routes with a `use:` handler were read before: the Swift Package Index server went from 13 routes to 34, and a WebSocket-only chat server from none to one.
- Play projects kept in subdirectories are recognized, so each one's `conf/routes` is read. A repository with no Play build at its root, like playframework's samples, went from no routes to over 130.
+1 -1
View File
@@ -377,7 +377,7 @@ Real iOS and React Native codebases live across multiple languages — a Swift c
| Expo Modules | expo-haptics | expo-camera | expo SDK sweep (7 packages) |
| Fabric / Paper views | [react-native-segmented-control](https://github.com/react-native-segmented-control/segmented-control) | [react-native-screens](https://github.com/software-mansion/react-native-screens) | [react-native-skia](https://github.com/Shopify/react-native-skia) |
Each bridge emits edges tagged `provenance:'heuristic'` with `metadata.synthesizedBy:` set to a stable channel name (e.g. `swift-objc-bridge`, `rn-event-channel`, `fabric-native-impl`, `expo-module-extract`), so the agent can tell at a glance how a hop got into the graph.
Every bridge hop says how it got into the graph. A hop matched by a bridge resolver carries `metadata.resolvedBy: 'framework'` and `metadata.framework` naming the resolver (`swift-objc-bridge`, `react-native-bridge`, `expo-modules-js`, `fabric-view`). A synthesized channel is tagged `provenance:'heuristic'` with `metadata.synthesizedBy` (`rn-event-channel`, `fabric-native-impl`).
---
+57
View File
@@ -0,0 +1,57 @@
/**
* A link a framework or bridge resolver made names that resolver on the edge
* (`metadata.framework`), as a synthesized channel names itself with
* `synthesizedBy`: a Swift → Objective-C hop says `swift-objc-bridge`.
*/
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-framework-edge-'));
const files: Record<string, string> = {
'Sources/Cache.h': `#import <Foundation/Foundation.h>
@interface Cache : NSObject
- (id)fetchEntryForKey:(NSString *)key;
@end
`,
'Sources/Cache.m': `#import "Cache.h"
@implementation Cache
- (id)fetchEntryForKey:(NSString *)key {
return nil;
}
@end
`,
'Sources/Caller.swift': `import Foundation
func load(cache: Cache) {
_ = cache.fetchEntry(forKey: "x")
}
`,
};
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('framework-resolved edges', () => {
it('name the resolver that made them', () => {
const caller = cg.getNodesInFile('Sources/Caller.swift').find((n) => n.name === 'load')!;
const bridged = cg.getOutgoingEdges(caller.id).find((e) => cg.getNode(e.target)?.name === 'fetchEntryForKey:');
expect(bridged).toBeDefined();
expect(bridged!.metadata?.resolvedBy).toBe('framework');
expect(bridged!.metadata?.framework).toBe('swift-objc-bridge');
});
});
+5 -1
View File
@@ -1160,7 +1160,11 @@ export class ReferenceResolver {
const tFw = this.profileStages ? process.hrtime.bigint() : 0n;
let fwEarly: ResolvedRef | null = null;
for (const framework of this.frameworks) {
const result = this.gateFrameworkLanguage(framework.resolve(ref, this.context), ref);
const resolved = this.gateFrameworkLanguage(framework.resolve(ref, this.context), ref);
// Name the resolver on the edge (`metadata.framework`): a Swift→ObjC or
// React Native bridge hop says how it got into the graph, as a
// synthesized edge's `synthesizedBy` does.
const result = resolved ? { ...resolved, metadata: { ...resolved.metadata, framework: framework.name } } : null;
if (result) {
if (result.confidence >= 0.9) {
fwEarly = result; // High confidence, return immediately (below)