fix(rust): index unit structs — bodiless is a definition, not a forward decl

`extractStruct` skips a struct with no body as a forward declaration. That
is right for C/C++ (`struct Foo;`), but Rust has no forward declarations —
`struct Unit;` is a complete unit struct. The type never entered the graph,
and neither did anything attached to it: every `impl SomeTrait for Unit`
lost its edge because the source endpoint did not exist.

Brace and tuple structs were unaffected — both carry a body field
(`field_declaration_list` / `ordered_field_declaration_list`), so only the
unit form fell through.

This is the same situation two earlier patches already handled, just a case
each of them left uncovered:

  #831  hardcoded a carve-out for C# positional records — "complete
        definitions with no body block".
  #1093 introduced `skipBodilessClass` so a bodiless CLASS is kept unless a
        language opts into skipping it.

Struct kept the unconditional skip. This adds `allowBodilessStruct`, the
per-language opt-in for the struct side, and sets it for Rust only. The
polarity is inverted relative to `skipBodilessClass` because the defaults
differ — that is documented on both flags.

The kernel's Rust walker had the same guard, placed before create_node, and
its header listed the behavior as a mirrored bug-for-bug quirk. Node
creation now happens first and the body is walked only when present, which
is how extract_interface in the same file was already shaped. Kernel↔wasm
parity is preserved (the parity suite compares the two, and passes).

Four lines of logic across four source files; the rest is comments, docs and
the two regression tests. The kernel header's quirk list, the parity suite's
docstring and the R7b port checklist all documented the old behavior as
intended, so each is updated — the checklist declares itself the living
quirk reference for the walker, so a stale entry there would misinform the
next reader.

Effect on a real Rust/TS repository (2669 nodes): 13 structs and 9
implements edges recovered.

Tests: two regression cases in extraction.test.ts. Verified failing without
the fix on the wasm path (CODEGRAPH_KERNEL=0) and passing with it, on both
the kernel and wasm paths. Full suite green — 170 files, 2922 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ctype_lab
2026-08-06 14:38:03 +09:00
parent d6d17288be
commit a94c9dc94e
8 changed files with 69 additions and 9 deletions
+9 -5
View File
@@ -19,8 +19,7 @@
//! kind is always `variable`, no signature, and EVERY direct `identifier`
//! child mints a node (`const MAX: u32 = OTHER;` → two nodes, `MAX` + the
//! phantom `OTHER`). Top-level initializer values are never body-walked.
//! - Unit structs (`struct Unit;`, no body field) mint NO node; `mod_item`
//! mints no module node and adds no QN prefix.
//! - `mod_item` mints no module node and adds no QN prefix.
//! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` →
//! `Foo::new().bar`); instance chains, parens, `.await`, 2-hop fields, and
//! `self` receivers all collapse to the bare method name (`self` is node
@@ -579,10 +578,11 @@ impl<'t> Walker<'t> {
self.stack.pop();
}
/// extractStruct — body field REQUIRED (unit structs mint no node; tuple
/// structs' ordered_field_declaration_list is a body).
/// extractStruct — the body field is OPTIONAL. A unit struct (`struct U;`)
/// has no body and is still a complete definition, so it mints a node with
/// no members; tuple structs' ordered_field_declaration_list is a body.
/// Mirrors the TS reference's `allowBodilessStruct`.
fn extract_struct(&mut self, node: Node<'t>) {
let Some(body) = node.child_by_field_name("body") else { return };
let name = self.extract_name(node);
let extra = Extra {
docstring: preceding_docstring(node, self.src),
@@ -592,6 +592,10 @@ impl<'t> Walker<'t> {
let Some(row) = self.create_node("struct", &name, node, extra) else { return };
self.extract_inheritance(node, row);
// Unit structs have no body to walk — the node itself is the whole
// definition.
let Some(body) = node.child_by_field_name("body") else { return };
self.stack.push(Scope { row, kind: "struct", name });
for i in 0..body.named_child_count() {
if let Some(c) = body.named_child(i) {