diff --git a/CHANGELOG.md b/CHANGELOG.md index 178cfd1f9..12ec8199c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index cd6c7bb44..ae3750c3b 100644 --- a/README.md +++ b/README.md @@ -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`). --- diff --git a/__tests__/framework-edge-name.test.ts b/__tests__/framework-edge-name.test.ts new file mode 100644 index 000000000..b2081c82e --- /dev/null +++ b/__tests__/framework-edge-name.test.ts @@ -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 = { + 'Sources/Cache.h': `#import +@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'); + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index f11a525d1..8b51ff37f 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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)