fix(rust): index unit structs — bodiless is a definition, not a forward declaration (#1513) - #1514
Open
ctype-lab wants to merge 1 commit into
Open
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1513.
Summary
Rust unit structs (
struct Unit;) never enter the graph, and everyimpl SomeTrait for Unitloses 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.
record struct M(decimal Amount);) are "complete definitions with no body block" — hardcoded carve-out inextractStructskipBodilessClass: a bodiless class is kept unless a language opts into skipping (C/C++ forward decls)The rule itself is correct and stays: in C/C++,
struct Foo;is a forwarddeclaration 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 toskipBodilessClass,opt-in, set for Rust only. No other language changes behavior.
The polarity is inverted relative to
skipBodilessClassbecause the twodefaults 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_interfacein that same file already had. Kernel↔wasmparity is preserved.
Brace and tuple structs were never affected: both carry a body field
(
field_declaration_list/ordered_field_declaration_list). Only the unitform fell through.
The whole change
Four lines of logic across four source files:
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.
The comment weight is deliberate:
allowBodilessStructandskipBodilessClasshave opposite polarity, and without the reasonwritten 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:
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 "remainsthe quirk reference for the walker" — its
struct_itemrow is markedsuperseded rather than deleted, so the port survey stays readable as
history.
Reproduction
structnodesUnitStructmissingimplementsedgesOn a real Rust/TypeScript repository (2,669 nodes): +13 structs, +9
implementsedges. The recovered types are exactly the codebase'szero-sized markers, test doubles and stub implementations —
SystemClock,StubTransport,MockEncoder,StubSettings, … — which is the idiom unitstructs exist for, so the loss concentrated in test and adapter layers.
Tests
Two regression cases in
__tests__/extraction.test.ts: one asserting allthree 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 astaged build.
kernel-rustlang-paritypasses (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 theprovable 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 ageneral fix would likely simplify those too.