fix(resolution): trait dispatch reaches union implementors (#1515)

Making unions first-class nodes leaves the third loss in #1515 open:
interfaceOverrideEdges enumerates its concrete side as ['class','struct'],
so a union implementor is skipped even though it now has a real node and a
real `implements` edge. "Who implements this trait" then answers wrongly
rather than incompletely — the struct beside it bridges and the union does
not.

Add 'union' to that tuple, plus a regression test that pins the Rust
trait -> union-impl hop (the struct implementor is the control proving the
synthesizer ran). Verified the test fails on the union assertion alone
before this change.

No EXTRACTION_VERSION bump: main is already at 25 against v1.5.0's 24, so
existing indexes are flagged stale for the next release regardless, and
over-bumping is what turns the re-index hint into noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-07 21:23:06 -05:00
parent e2195940fb
commit 5b0c4b8b93
3 changed files with 48 additions and 2 deletions
+1 -1
View File
@@ -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)
+46
View File
@@ -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
+1 -1
View File
@@ -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();