fix(rust): index unit structs — bodiless is a definition, not a forward decl (#1800)
Land upstream PR #1514 for issue #1513 by cherry-picking
ctype_lab's a94c9dc94eee348bf63c7e2dd567c0b43577678a.
Keep allowBodilessStruct as a Rust-only opt-in, with matching behavior in
the wasm/TypeScript walker and native kernel. Create the node before
checking for a body and walk members only when present. Resolve against
main's shared struct/union walker while retaining its stack guard, kinds,
fields, and existing impl-receiver fixes.
Verified FAIL to PASS on Linux x64 with Node 22.19.0 after rebuilding via
tsc, copy-assets, and build:kernel. The identical fixture on main 7b339373
had two structs and two implements edges; fresh wasm (CODEGRAPH_KERNEL=0)
and native indexes now have three of each. UnitStruct and its Greet
implements edge are recovered; tuple and brace structs remain intact.
The native run loaded the rebuilt kernel and completed without fallback.
Focused extraction.test.ts and kernel-rustlang-parity.test.ts runs:
645 tests passed with the kernel disabled, and 645 with it enabled;
all three parity tests ran in each configuration, with no skips.
(cherry picked from commit a94c9dc94eee348bf63c7e2dd567c0b43577678a)
Co-authored-by: ctype_lab <cksgud1226@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
ctype_lab
Claude Opus 5
parent
7b339373b4
commit
71d049cd28
@@ -225,6 +225,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
#### Symbols, tests and the viewer
|
||||
|
||||
- Rust unit structs (`struct Unit;`) and their trait implementation relationships now appear in the graph after re-indexing. (#1513, #1514)
|
||||
- Imports from Node built-ins or npm packages no longer connect to unrelated type members with matching names; re-index after upgrading to clear existing false dependencies. Thanks @ctype-lab. (#1537)
|
||||
|
||||
- Inheritance relationships no longer attach external Rust or npm supertypes to unrelated local symbols with the same name, including in Svelte, Vue and Astro components; re-index after upgrading to clear existing false relationships. Thanks @ctype-lab. (#1536)
|
||||
|
||||
@@ -1252,6 +1252,35 @@ pub struct User {
|
||||
expect(structNode?.name).toBe('User');
|
||||
});
|
||||
|
||||
it('should extract unit and tuple structs, not just brace structs', () => {
|
||||
// A unit struct has no body field, but it IS a complete definition —
|
||||
// Rust has no forward declarations. Skipping it dropped the type and
|
||||
// every `impl Trait for UnitStruct` edge with it.
|
||||
const code = `
|
||||
pub struct Unit;
|
||||
pub struct Tuple(pub u32);
|
||||
pub struct Brace { pub x: u32 }
|
||||
`;
|
||||
const result = extractFromSource('shapes.rs', code);
|
||||
|
||||
const structs = result.nodes.filter((n) => n.kind === 'struct').map((n) => n.name).sort();
|
||||
expect(structs).toEqual(['Brace', 'Tuple', 'Unit']);
|
||||
});
|
||||
|
||||
it('should link impl Trait for a unit struct', () => {
|
||||
const code = `
|
||||
pub struct Unit;
|
||||
pub trait Greet { fn hi(&self) -> String; }
|
||||
impl Greet for Unit { fn hi(&self) -> String { "unit".into() } }
|
||||
`;
|
||||
const result = extractFromSource('greet.rs', code);
|
||||
|
||||
const unit = result.nodes.find((n) => n.kind === 'struct' && n.name === 'Unit');
|
||||
expect(unit).toBeDefined();
|
||||
const trait = result.nodes.find((n) => n.kind === 'trait' && n.name === 'Greet');
|
||||
expect(trait).toBeDefined();
|
||||
});
|
||||
|
||||
it('should extract trait declarations', () => {
|
||||
const code = `
|
||||
pub trait Repository {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* SAME ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
|
||||
* unresolved refs compared as canonicalized multisets — over the checked-in
|
||||
* torture fixture (torture.rs: impl/trait quirks incl. generic / lifetime /
|
||||
* reference / scoped / generic-trait impl receivers (#1588), unit-struct skip, phantom
|
||||
* reference / scoped / generic-trait impl receivers (#1588), unit structs
|
||||
* (a bodiless struct IS a definition — both walkers mint a node), phantom
|
||||
* const identifiers, use-binding refs incl. nested groups + wildcard-emits-
|
||||
* nothing, chained-call re-encode, turbofish, Rocket route macros body-only,
|
||||
* fn-ref shapes, value-ref shadowing, attribute-broken docstrings, dead-code
|
||||
|
||||
@@ -20,8 +20,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`); a call through a field of the enclosing type keeps
|
||||
//! the owner-field shape (`self.inner.run()` → `self.inner.run`, #1585);
|
||||
@@ -590,10 +589,12 @@ impl<'t> Walker<'t> {
|
||||
self.stack.pop();
|
||||
}
|
||||
|
||||
/// Extract a Rust struct or union with a body; unit structs remain skipped.
|
||||
/// Extract a Rust struct or union — 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_aggregate(&mut self, node: Node<'t>, kind: &'static str) {
|
||||
stack_guard!();
|
||||
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),
|
||||
@@ -603,6 +604,10 @@ impl<'t> Walker<'t> {
|
||||
let Some(row) = self.create_node(kind, &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, name });
|
||||
for i in 0..body.named_child_count() {
|
||||
if let Some(c) = body.named_child(i) {
|
||||
|
||||
@@ -140,7 +140,7 @@ undefined; **no isConst means `const_item`/`static_item` extract as kind
|
||||
|---|---|---|
|
||||
| `function_item` (top level) | functionTypes, tree-sitter.ts:994 → extractFunction:1517 | not inside class-like at file scope → extractFunction; **first line of extractFunction (1522): if getReceiverType returns a value → extractMethod instead** (this is how impl-block fns become methods — impl_item does NOT push a scope) |
|
||||
| `function_signature_item` | same | in a trait body (trait pushed, class-like) → extractMethod; no `body` field → no body walk |
|
||||
| `struct_item` | structTypes:1059 → extractStruct:1869 | `body` field required: **unit structs `struct Unit;` have no body → NO node minted** (1876, `record_declaration` exemption is C#-only). Tuple structs have body `ordered_field_declaration_list` → extracted. `field_declaration` children make NO nodes (rust has no fieldTypes) — visitNode recurses into them and finds nothing |
|
||||
| `struct_item` | structTypes:1059 → extractStruct:1869 | ~~`body` field required: unit structs `struct Unit;` have no body → NO node minted~~ — **superseded: Rust now sets `allowBodilessStruct`, so `struct Unit;` mints a node with no members.** Rust has no forward declarations, so the bodiless skip (meant for C/C++) never applied here; the `record_declaration` exemption is the C# form of the same carve-out. Tuple structs have body `ordered_field_declaration_list` → extracted. `field_declaration` children make NO nodes (rust has no fieldTypes) — visitNode recurses into them and finds nothing |
|
||||
| `enum_item` | enumTypes:1064 → extractEnum:1914 | body `enum_variant_list`; `enum_variant` children → extractEnumMembers:1958 — **`name` field path: one `enum_member` node from `getChildByField(node,'name')`, then return** (variant payload bodies `B(u32)` / `C { x }` are never walked). Non-variant children (e.g. `attribute_item`) → visitNode (no-op) |
|
||||
| `trait_item` | interfaceTypes:1054 → extractInterface:1834 | kind `'trait'` (interfaceKind); extractInheritance sees the `trait_bounds` child (see below); body `declaration_list` children visited with the trait pushed → fn items become methods with QN `Trait::name` via nodeStack |
|
||||
| `impl_item` | dedicated branch:1273-1276 → extractRustImplItem:5690 | emits the implements back-reference (below); **skipChildren stays false** → the `declaration_list` is then visited normally by the loop at 1295 (that's how impl members are reached; impl pushes NOTHING on the nodeStack) |
|
||||
@@ -490,7 +490,7 @@ inner `array_expression`, but `const CB: fn() = handler;` captures nothing
|
||||
## Gates (per plan §5, no exceptions)
|
||||
|
||||
- **Torture fixture `torture.rs`** (+ CRLF variant, derived in-memory), pinning
|
||||
at minimum: unit struct (NO node) / tuple struct / field struct; enum with
|
||||
at minimum: unit struct (node, no members) / tuple struct / field struct; enum with
|
||||
unit+tuple+struct variants; trait with supertraits incl. a SCOPED one
|
||||
(`fmt::Debug` — dropped) + `function_signature_item` + default method +
|
||||
associated type/const (no node; const value call attributes to trait);
|
||||
|
||||
@@ -81,6 +81,9 @@ export const rustExtractor: LanguageExtractor = {
|
||||
methodTypes: ['function_item', 'function_signature_item'],
|
||||
interfaceTypes: ['trait_item'],
|
||||
structTypes: ['struct_item'],
|
||||
// `struct Unit;` is a unit struct — a complete definition with no body
|
||||
// field, not a forward declaration. Rust has no forward declarations.
|
||||
allowBodilessStruct: true,
|
||||
// Unions share struct member syntax and impl attachment, but retain their
|
||||
// distinct semantic kind in the graph.
|
||||
unionTypes: ['union_item'],
|
||||
|
||||
@@ -187,6 +187,19 @@ export interface LanguageExtractor {
|
||||
* bodiless class IS complete (Kotlin `class Empty`, Scala `case object`). (#1093)
|
||||
*/
|
||||
skipBodilessClass?: boolean;
|
||||
/**
|
||||
* Keep a bodiless struct node — it IS a complete definition, not a forward
|
||||
* declaration. Set only for languages where a bodiless `struct` is complete:
|
||||
* Rust's unit struct (`struct Unit;`). Leave unset for C/C++, where
|
||||
* `struct Foo;` is a forward declaration.
|
||||
*
|
||||
* Opposite polarity from `skipBodilessClass` (#1093) because the defaults
|
||||
* differ: a bodiless CLASS is kept unless a language opts into skipping,
|
||||
* a bodiless STRUCT is skipped unless a language opts into keeping. The
|
||||
* hardcoded C# `record_declaration` carve-out (#831) is the same situation
|
||||
* predating this flag.
|
||||
*/
|
||||
allowBodilessStruct?: boolean;
|
||||
/** NodeKind to use for interface-like declarations (Rust: 'trait'). Default: 'interface' */
|
||||
interfaceKind?: NodeKind;
|
||||
|
||||
|
||||
@@ -1974,8 +1974,16 @@ export class TreeSitterExtractor {
|
||||
// Skip forward declarations and type references (no body = not a definition)
|
||||
// — EXCEPT C# positional records (`record struct M(decimal Amount);`),
|
||||
// complete definitions with no body block. (#831)
|
||||
//
|
||||
// `allowBodilessStruct` is the per-language escape hatch for the same
|
||||
// situation: a bodiless struct that IS a complete definition (Rust's unit
|
||||
// struct `struct Unit;`). Opposite polarity from `skipBodilessClass`
|
||||
// (#1093) because the two defaults differ — a bodiless CLASS is kept
|
||||
// unless a language opts into skipping, a bodiless STRUCT is skipped
|
||||
// unless a language opts into keeping.
|
||||
const body = getChildByField(node, this.extractor.bodyField);
|
||||
if (!body && node.type !== 'record_declaration') return;
|
||||
if (!body && node.type !== 'record_declaration' && !this.extractor.allowBodilessStruct)
|
||||
return;
|
||||
|
||||
const name = extractName(node, this.source, this.extractor);
|
||||
const docstring = getPrecedingDocstring(node, this.source);
|
||||
|
||||
Reference in New Issue
Block a user