Skip to content

fix(rust): index unit structs — bodiless is a definition, not a forward declaration (#1513) - #1514

Open
ctype-lab wants to merge 1 commit into
colbymchenry:mainfrom
ctype-lab:fix/rust-unit-struct-not-indexed
Open

fix(rust): index unit structs — bodiless is a definition, not a forward declaration (#1513)#1514
ctype-lab wants to merge 1 commit into
colbymchenry:mainfrom
ctype-lab:fix/rust-unit-struct-not-indexed

Conversation

@ctype-lab

Copy link
Copy Markdown

Fixes #1513.

Summary

Rust unit structs (struct Unit;) never enter the graph, and every
impl SomeTrait for Unit loses its edge with them.

This is not a new class of bug — it is the one case two earlier patches
left uncovered
. Both established that "no body" does not mean "not a
definition"; struct just never got the same treatment.

what it settled left uncovered
#831 C# positional records (record struct M(decimal Amount);) are "complete definitions with no body block" — hardcoded carve-out in extractStruct only that one node type
#1093 skipBodilessClass: a bodiless class is kept unless a language opts into skipping (C/C++ forward decls) struct kept its unconditional skip

The rule itself is correct and stays: in C/C++, struct Foo; is a forward
declaration and must be skipped. Rust has no forward declarations — a
bodiless struct is always a complete definition. The C-shaped rule was simply
applied to a language it does not describe.

The fix

allowBodilessStruct — the struct-side counterpart to skipBodilessClass,
opt-in, set for Rust only. No other language changes behavior.

// src/extraction/languages/rust.ts
allowBodilessStruct: true,

The polarity is inverted relative to skipBodilessClass because the two
defaults differ (bodiless class → kept by default; bodiless struct → skipped
by default). That is documented on both flags so the asymmetry is not a
surprise later.

The kernel's Rust walker carried the same guard, placed before create_node,
and its header listed the behavior in the mirrored bug-for-bug quirk list.
Node creation now happens first and the body is walked only when present —
the shape extract_interface in that same file already had. Kernel↔wasm
parity is preserved.

Brace and tuple structs were never affected: both carry a body field
(field_declaration_list / ordered_field_declaration_list). Only the unit
form fell through.

The whole change

Four lines of logic across four source files:

  src/extraction/tree-sitter.ts
- if (!body && node.type !== 'record_declaration') return;
+ if (!body && node.type !== 'record_declaration' && !this.extractor.allowBodilessStruct)
+   return;

  src/extraction/tree-sitter-types.ts
+ allowBodilessStruct?: boolean;

  src/extraction/languages/rust.ts
+ allowBodilessStruct: true,

  codegraph-kernel/src/rustlang.rs
- let Some(body) = node.child_by_field_name("body") else { return };   // before create_node
+ let Some(body) = node.child_by_field_name("body") else { return };   // after create_node

The kernel line is moved, not changed — the early return simply happens after
the node is created instead of before it.

Everything else is comments, the two regression tests, and the docs that
described the old behavior as intended.

files lines
source logic 4 4
source comments (same 4) 32
regression tests 1 +29
docs describing the old behavior 3 ±7
CHANGELOG 1 +2

The comment weight is deliberate: allowBodilessStruct and
skipBodilessClass have opposite polarity, and without the reason
written on both flags the next reader will assume one of them is backwards.

Three places documented the skip as intended behavior and are updated with
the code:

  • the kernel header's mirrored bug-for-bug quirk list (the entry is gone —
    it is no longer mirrored),
  • kernel-rustlang-parity's docstring, which listed "unit-struct skip"
    among the quirks the torture fixture pins,
  • docs/design/rust-lang-kernel-port-checklist.md, which states it "remains
    the quirk reference for the walker" — its struct_item row is marked
    superseded rather than deleted, so the port survey stays readable as
    history.

Reproduction

pub struct UnitStruct;
pub struct TupleStruct(pub u32);
pub struct BraceStruct { pub x: u32 }

pub trait Greet { fn hi(&self) -> String; }

impl Greet for UnitStruct  { fn hi(&self) -> String { "unit".into()  } }
impl Greet for TupleStruct { fn hi(&self) -> String { "tuple".into() } }
impl Greet for BraceStruct { fn hi(&self) -> String { "brace".into() } }
before after
struct nodes 2 — UnitStruct missing 3
implements edges 2 3

On a real Rust/TypeScript repository (2,669 nodes): +13 structs, +9
implements edges
. The recovered types are exactly the codebase's
zero-sized markers, test doubles and stub implementations — SystemClock,
StubTransport, MockEncoder, StubSettings, … — which is the idiom unit
structs exist for, so the loss concentrated in test and adapter layers.

Tests

Two regression cases in __tests__/extraction.test.ts: one asserting all
three struct forms extract, one asserting impl Trait for <unit struct>
survives.

Verified they fail without the fix and pass with it, on both extraction
paths — the wasm path via CODEGRAPH_KERNEL=0, and the kernel path with a
staged build. kernel-rustlang-parity passes (it compares the two walkers,
so a one-sided fix would have failed it).

Full suite green: 170 files, 2,922 tests.

Note for the maintainer

skipBodilessClass (#1093) settled the class side as keep by default.
Struct is still skip by default, which is why this patch needs an
inverted-polarity flag rather than reusing the existing one.

Flipping struct's default to match would remove the asymmetry, but it touches
all 27 languages that declare structTypes, so I kept this PR to the
provable Rust case. Happy to do that follow-up if you would rather converge
the two.

Two other languages already work around the same guard with their own
extraction code (solidity.ts, erlang.ts — both comment on it), so a
general fix would likely simplify those too.

…rd 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:

  colbymchenry#831  hardcoded a carve-out for C# positional records — "complete
        definitions with no body block".
  colbymchenry#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rust: unit structs (struct Unit;) are not indexed, taking their impl Trait for edges with them

1 participant