diff --git a/CHANGELOG.md b/CHANGELOG.md index 5362328..ac84ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes -- C, C++, Objective-C and Rust unions are now indexed as first-class `union` nodes. A `union` declaration previously produced no symbol at all, so it never appeared in search or `codegraph_explore`, and anything attached to it disappeared with it — in Rust, every `impl SomeTrait for MyUnion` lost its edge, and the methods from that impl were left pointing at a type the graph did not contain. A `typedef union { … } Name;` in C now carries the typedef's name and remains distinguishable from a struct. Re-index after upgrading to replace the earlier struct-shaped union nodes. +- C, C++, Objective-C and Rust unions are now indexed as first-class `union` nodes. A `union` declaration previously produced no symbol at all, so it never appeared in search or `codegraph_explore`, and anything attached to it disappeared with it — in Rust, every `impl SomeTrait for MyUnion` lost its edge, the methods from that impl were left pointing at a type the graph did not contain, and asking which types implement a trait quietly skipped the union ones. A union-shaped dispatch table in C now resolves its function pointers like a struct-shaped one. A `typedef union { … } Name;` in C keeps the typedef's name and is no longer mistaken for a plain type alias. Thanks @ctype-lab. Re-index after upgrading to pick up unions in existing projects. (#1515) - `codegraph_explore` now concentrates its answer on the code that actually answers your question instead of spreading it across files that merely share a word with it, so more of the answer arrives in a single call. Thanks @LeDuyViet for the detailed measurements and reproduction. (#1500) - Files only weakly related to your question now come back as a name, symbol and line number instead of spending the answer on their source — name one of them in a follow-up `codegraph_explore` to get it back in full. (#1500) diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 3bf260c..637b4a9 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -1073,6 +1073,52 @@ int invoke() { return Ops::run(); } expect(outgoing.some((e) => e.kind === 'calls' && e.target === run!.id)).toBe(true); }); + it('bridges a Rust trait method to a union implementor (interface-impl)', async () => { + // A Rust union can `impl Trait` exactly as a struct can. Trait-dispatch + // synthesis enumerates concrete kinds explicitly, so a union implementor + // only becomes a candidate if `union` is in that list. Making unions + // first-class nodes is not enough on its own: without this, `Reg` has an + // `implements` edge and is still silently dropped from the fan-out, so + // "who implements this trait" answers wrongly rather than incompletely + // — the struct beside it resolves and the union does not (#1515). + fs.writeFileSync( + path.join(tempDir, 'lib.rs'), + `pub union Reg { pub raw: u32 } +pub struct Ctl { pub n: u32 } + +pub trait Describe { fn describe(&self) -> String; } + +impl Describe for Reg { fn describe(&self) -> String { "reg".into() } } +impl Describe for Ctl { fn describe(&self) -> String { "ctl".into() } } +` + ); + + cg = await CodeGraph.init(tempDir, { index: true }); + + const methods = cg.getNodesByKind('method'); + const traitMethod = methods.find((n) => n.qualifiedName === 'Describe::describe'); + const unionImpl = methods.find((n) => n.qualifiedName === 'Reg::describe'); + const structImpl = methods.find((n) => n.qualifiedName === 'Ctl::describe'); + expect(traitMethod, 'trait method should be in the graph').toBeDefined(); + expect(unionImpl, 'union impl method should be in the graph').toBeDefined(); + expect(structImpl, 'struct impl method should be in the graph').toBeDefined(); + + const synth = cg + .getOutgoingEdges(traitMethod!.id) + .filter((e) => e.kind === 'calls' && e.provenance === 'heuristic'); + const targets = new Set(synth.map((e) => e.target)); + + // The struct implementor bridged before unions were nodes at all; it is + // the control that proves the synthesizer ran for this trait. + expect(targets.has(structImpl!.id), 'struct implementor should bridge').toBe(true); + expect(targets.has(unionImpl!.id), 'union implementor should bridge').toBe(true); + + const unionEdge = synth.find((e) => e.target === unionImpl!.id); + expect( + (unionEdge!.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy + ).toBe('interface-impl'); + }); + it('records instantiates for C++ stack/brace construction, targeting the class (#1035)', async () => { // `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init) // carry the constructor args directly on the declarator — there's no diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index b8bc104..dc83331 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -1061,7 +1061,7 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield // Concrete-side kinds vary by language: `class` covers Java / Kotlin / // C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value // types that conform to protocols. Iterate both. - const concreteKinds = ['class', 'struct'] as const; + const concreteKinds = ['class', 'struct', 'union'] as const; for (const kind of concreteKinds) { for (const cls of queries.iterateNodesByKind(kind)) { if ((++scanned255 & 63) === 0) await onYield();