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
|
#### 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)
|
- 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)
|
- 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');
|
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', () => {
|
it('should extract trait declarations', () => {
|
||||||
const code = `
|
const code = `
|
||||||
pub trait Repository {
|
pub trait Repository {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
* SAME ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
|
* SAME ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
|
||||||
* unresolved refs compared as canonicalized multisets — over the checked-in
|
* unresolved refs compared as canonicalized multisets — over the checked-in
|
||||||
* torture fixture (torture.rs: impl/trait quirks incl. generic / lifetime /
|
* 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-
|
* const identifiers, use-binding refs incl. nested groups + wildcard-emits-
|
||||||
* nothing, chained-call re-encode, turbofish, Rocket route macros body-only,
|
* nothing, chained-call re-encode, turbofish, Rocket route macros body-only,
|
||||||
* fn-ref shapes, value-ref shadowing, attribute-broken docstrings, dead-code
|
* 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`
|
//! kind is always `variable`, no signature, and EVERY direct `identifier`
|
||||||
//! child mints a node (`const MAX: u32 = OTHER;` → two nodes, `MAX` + the
|
//! child mints a node (`const MAX: u32 = OTHER;` → two nodes, `MAX` + the
|
||||||
//! phantom `OTHER`). Top-level initializer values are never body-walked.
|
//! phantom `OTHER`). Top-level initializer values are never body-walked.
|
||||||
//! - Unit structs (`struct Unit;`, no body field) mint NO node; `mod_item`
|
//! - `mod_item` mints no module node and adds no QN prefix.
|
||||||
//! mints no module node and adds no QN prefix.
|
|
||||||
//! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` →
|
//! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` →
|
||||||
//! `Foo::new().bar`); a call through a field of the enclosing type keeps
|
//! `Foo::new().bar`); a call through a field of the enclosing type keeps
|
||||||
//! the owner-field shape (`self.inner.run()` → `self.inner.run`, #1585);
|
//! the owner-field shape (`self.inner.run()` → `self.inner.run`, #1585);
|
||||||
@@ -590,10 +589,12 @@ impl<'t> Walker<'t> {
|
|||||||
self.stack.pop();
|
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) {
|
fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) {
|
||||||
stack_guard!();
|
stack_guard!();
|
||||||
let Some(body) = node.child_by_field_name("body") else { return };
|
|
||||||
let name = self.extract_name(node);
|
let name = self.extract_name(node);
|
||||||
let extra = Extra {
|
let extra = Extra {
|
||||||
docstring: preceding_docstring(node, self.src),
|
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 };
|
let Some(row) = self.create_node(kind, &name, node, extra) else { return };
|
||||||
self.extract_inheritance(node, row);
|
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 });
|
self.stack.push(Scope { row, kind, name });
|
||||||
for i in 0..body.named_child_count() {
|
for i in 0..body.named_child_count() {
|
||||||
if let Some(c) = body.named_child(i) {
|
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_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 |
|
| `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) |
|
| `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 |
|
| `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) |
|
| `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)
|
## Gates (per plan §5, no exceptions)
|
||||||
|
|
||||||
- **Torture fixture `torture.rs`** (+ CRLF variant, derived in-memory), pinning
|
- **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
|
unit+tuple+struct variants; trait with supertraits incl. a SCOPED one
|
||||||
(`fmt::Debug` — dropped) + `function_signature_item` + default method +
|
(`fmt::Debug` — dropped) + `function_signature_item` + default method +
|
||||||
associated type/const (no node; const value call attributes to trait);
|
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'],
|
methodTypes: ['function_item', 'function_signature_item'],
|
||||||
interfaceTypes: ['trait_item'],
|
interfaceTypes: ['trait_item'],
|
||||||
structTypes: ['struct_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
|
// Unions share struct member syntax and impl attachment, but retain their
|
||||||
// distinct semantic kind in the graph.
|
// distinct semantic kind in the graph.
|
||||||
unionTypes: ['union_item'],
|
unionTypes: ['union_item'],
|
||||||
|
|||||||
@@ -187,6 +187,19 @@ export interface LanguageExtractor {
|
|||||||
* bodiless class IS complete (Kotlin `class Empty`, Scala `case object`). (#1093)
|
* bodiless class IS complete (Kotlin `class Empty`, Scala `case object`). (#1093)
|
||||||
*/
|
*/
|
||||||
skipBodilessClass?: boolean;
|
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' */
|
/** NodeKind to use for interface-like declarations (Rust: 'trait'). Default: 'interface' */
|
||||||
interfaceKind?: NodeKind;
|
interfaceKind?: NodeKind;
|
||||||
|
|
||||||
|
|||||||
@@ -1974,8 +1974,16 @@ export class TreeSitterExtractor {
|
|||||||
// Skip forward declarations and type references (no body = not a definition)
|
// Skip forward declarations and type references (no body = not a definition)
|
||||||
// — EXCEPT C# positional records (`record struct M(decimal Amount);`),
|
// — EXCEPT C# positional records (`record struct M(decimal Amount);`),
|
||||||
// complete definitions with no body block. (#831)
|
// 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);
|
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 name = extractName(node, this.source, this.extractor);
|
||||||
const docstring = getPrecedingDocstring(node, this.source);
|
const docstring = getPrecedingDocstring(node, this.source);
|
||||||
|
|||||||
Reference in New Issue
Block a user