diff --git a/codegraph-kernel/src/buffers.rs b/codegraph-kernel/src/buffers.rs index 66ae140..8934ed7 100644 --- a/codegraph-kernel/src/buffers.rs +++ b/codegraph-kernel/src/buffers.rs @@ -77,7 +77,7 @@ pub const EDGE_ROW_SIZE: usize = 44; pub const REF_ROW_SIZE: usize = 40; /// Mirror of NODE_KINDS in src/types.ts — order is the wire contract. -pub const NODE_KINDS: [&str; 22] = [ +pub const NODE_KINDS: [&str; 23] = [ "file", "module", "class", @@ -100,6 +100,7 @@ pub const NODE_KINDS: [&str; 22] = [ "export", "route", "component", + "union", ]; /// Mirror of EDGE_KINDS in src/types.ts — order is the wire contract. diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index ee68845..9f058f4 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -455,7 +455,7 @@ impl<'t> Walker<'t> { fn inside_class_like(&self) -> bool { self.stack .last() - .map(|s| matches!(s.kind, "class" | "struct" | "interface" | "trait" | "enum" | "module")) + .map(|s| matches!(s.kind, "class" | "struct" | "union" | "interface" | "trait" | "enum" | "module")) .unwrap_or(false) } @@ -561,7 +561,7 @@ impl<'t> Walker<'t> { let parent_ok = self .stack .last() - .map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "enum")) + .map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "union" | "enum")) .unwrap_or(false); if parent_ok { self.fs_values.insert(name.to_string(), row); @@ -812,11 +812,11 @@ impl<'t> Walker<'t> { } else if self.variant == Variant::Cpp && kind == "class_specifier" { self.extract_class(node); skip_children = true; - } else if matches!(kind, "struct_specifier" | "union_specifier") { - // `union_specifier` mirrors structTypes on the TS side: a named - // `union U { … };` is a definition, extracted with kind "struct" - // (NodeKind has no "union"). Bodiless stays a forward declaration. - self.extract_struct(node); + } else if kind == "struct_specifier" { + self.extract_aggregate(node, "struct"); + skip_children = true; + } else if kind == "union_specifier" { + self.extract_aggregate(node, "union"); skip_children = true; } else if kind == "enum_specifier" { self.extract_enum(node); @@ -928,7 +928,7 @@ impl<'t> Walker<'t> { .iter() .position(|m| { m.name == *receiver_type - && matches!(m.kind, "struct" | "class" | "enum" | "trait") + && matches!(m.kind, "struct" | "union" | "class" | "enum" | "trait") }) .map(|i| i as u32); if let Some(owner_row) = owner_row { @@ -974,8 +974,8 @@ impl<'t> Walker<'t> { self.stack.pop(); } - /// extractStruct: bodiless specifiers (fwd decls / elaborated refs) skip. - fn extract_struct(&mut self, node: Node<'t>) { + /// Extract a struct-like declaration while preserving its semantic kind. + fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) { let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); let extra = Extra { @@ -983,9 +983,9 @@ impl<'t> Walker<'t> { visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, ..Extra::default() }; - let Some(row) = self.create_node("struct", &name, node, extra) else { return }; + let Some(row) = self.create_node(kind, &name, node, extra) else { return }; self.extract_inheritance(node, row); - self.stack.push(Scope { row, kind: "struct", name }); + self.stack.push(Scope { row, kind, name }); for i in 0..body.named_child_count() { if let Some(c) = body.named_child(i) { self.visit_node(c); @@ -1044,24 +1044,27 @@ impl<'t> Walker<'t> { resolved = Some("enum"); break; } - if matches!(child.kind(), "struct_specifier" | "union_specifier") - && child.child_by_field_name("body").is_some() - { + if child.kind() == "struct_specifier" && child.child_by_field_name("body").is_some() { resolved = Some("struct"); break; } + if child.kind() == "union_specifier" && child.child_by_field_name("body").is_some() { + resolved = Some("union"); + break; + } } - if resolved == Some("struct") { + if matches!(resolved, Some("struct") | Some("union")) { + let kind = resolved.unwrap(); let Some(row) = self.create_node( - "struct", + kind, &name, node, Extra { docstring, ..Extra::default() }, ) else { return true; }; - self.stack.push(Scope { row, kind: "struct", name }); + self.stack.push(Scope { row, kind, name }); let type_child = node .child_by_field_name("type") .or_else(|| self.find_child_by_kind(node, "struct_specifier")) @@ -1562,8 +1565,12 @@ impl<'t> Walker<'t> { self.extract_class(node); return; } - if matches!(kind, "struct_specifier" | "union_specifier") { - self.extract_struct(node); + if kind == "struct_specifier" { + self.extract_aggregate(node, "struct"); + return; + } + if kind == "union_specifier" { + self.extract_aggregate(node, "union"); return; } if kind == "enum_specifier" { diff --git a/codegraph-kernel/src/rustlang.rs b/codegraph-kernel/src/rustlang.rs index a7d6067..16447c2 100644 --- a/codegraph-kernel/src/rustlang.rs +++ b/codegraph-kernel/src/rustlang.rs @@ -217,7 +217,7 @@ impl<'t> Walker<'t> { fn inside_class_like(&self) -> bool { self.stack .last() - .map(|s| matches!(s.kind, "class" | "struct" | "interface" | "trait" | "enum" | "module")) + .map(|s| matches!(s.kind, "class" | "struct" | "union" | "interface" | "trait" | "enum" | "module")) .unwrap_or(false) } @@ -326,7 +326,7 @@ impl<'t> Walker<'t> { let parent_ok = self .stack .last() - .map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "enum")) + .map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "union" | "enum")) .unwrap_or(false); if parent_ok { self.fs_values.insert(name.to_string(), row); @@ -446,10 +446,11 @@ impl<'t> Walker<'t> { } else if kind == "trait_item" { self.extract_interface(node); skip_children = true; - } else if matches!(kind, "struct_item" | "union_item") { - // `union_item` mirrors structTypes on the TS side: same `body:` - // field, same extractor, kind "struct" (NodeKind has no "union"). - self.extract_struct(node); + } else if kind == "struct_item" { + self.extract_aggregate(node, "struct"); + skip_children = true; + } else if kind == "union_item" { + self.extract_aggregate(node, "union"); skip_children = true; } else if kind == "enum_item" { self.extract_enum(node); @@ -531,7 +532,7 @@ impl<'t> Walker<'t> { .iter() .position(|m| { m.name == *receiver - && matches!(m.kind, "struct" | "class" | "enum" | "trait") + && matches!(m.kind, "struct" | "union" | "class" | "enum" | "trait") }) .map(|i| i as u32); if let Some(owner_row) = owner_row { @@ -581,9 +582,8 @@ 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). - fn extract_struct(&mut self, node: Node<'t>) { + /// Extract a Rust struct or union with a body; unit structs remain skipped. + fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) { let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); let extra = Extra { @@ -591,10 +591,10 @@ impl<'t> Walker<'t> { visibility: Some(self.visibility_of(node)), ..Extra::default() }; - let Some(row) = self.create_node("struct", &name, node, extra) else { return }; + let Some(row) = self.create_node(kind, &name, node, extra) else { return }; self.extract_inheritance(node, row); - self.stack.push(Scope { row, kind: "struct", name }); + self.stack.push(Scope { row, kind, name }); for i in 0..body.named_child_count() { if let Some(c) = body.named_child(i) { self.visit_node(c); @@ -1059,7 +1059,7 @@ impl<'t> Walker<'t> { let target_row = self .nodes_meta .iter() - .position(|m| m.name == type_name && matches!(m.kind, "struct" | "enum" | "class")) + .position(|m| m.name == type_name && matches!(m.kind, "struct" | "union" | "enum" | "class")) .map(|i| i as u32); if let Some(target_row) = target_row { self.push_ref_at(target_row, &trait_name, edge_kind_index("implements").unwrap(), trait_node); @@ -1132,8 +1132,12 @@ impl<'t> Walker<'t> { } // Structural nodes inside bodies. - if matches!(kind, "struct_item" | "union_item") { - self.extract_struct(node); + if kind == "struct_item" { + self.extract_aggregate(node, "struct"); + return; + } + if kind == "union_item" { + self.extract_aggregate(node, "union"); return; } if kind == "enum_item" { diff --git a/src/context/index.ts b/src/context/index.ts index 3e60723..e297cab 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -559,7 +559,7 @@ export class ContextBuilder { // but are almost never what exploration queries want. const searchKinds = opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds - : ['file', 'module', 'class', 'struct', 'interface', 'trait', 'protocol', + : ['file', 'module', 'class', 'struct', 'union', 'interface', 'trait', 'protocol', 'function', 'method', 'property', 'field', 'variable', 'constant', 'enum', 'enum_member', 'type_alias', 'namespace', 'export', 'route', 'component'] as NodeKind[]; diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 4a6e1a5..52b7ed0 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -343,7 +343,7 @@ export function getExploreOutputBudget(fileCount: number): ExploreOutputBudget { */ export const RELEVANCE_KIND_WEIGHT: Readonly> = { // Callables and types: the answer lives in one of these. - function: 1, method: 1, class: 1, struct: 1, interface: 1, trait: 1, + function: 1, method: 1, class: 1, struct: 1, union: 1, interface: 1, trait: 1, protocol: 1, component: 1, route: 1, enum: 1, type_alias: 1, constructor: 1, // Containers: real structure, but a whole namespace/module matching a term is // a coarser signal than a callable matching it. @@ -3968,7 +3968,7 @@ export class ToolHandler { const superMany = new Map(); const definesPolymorphicSupertype = (nodes: Node[]): boolean => { for (const n of nodes) { - if (n.kind !== 'class' && n.kind !== 'interface' && n.kind !== 'struct' + if (n.kind !== 'class' && n.kind !== 'interface' && n.kind !== 'struct' && n.kind !== 'union' && n.kind !== 'trait' && n.kind !== 'protocol' && n.kind !== 'type_alias') continue; let many = superMany.get(n.id); if (many === undefined) { diff --git a/src/resolution/c-fnptr-synthesizer.ts b/src/resolution/c-fnptr-synthesizer.ts index 13055e2..cc7ea1d 100644 --- a/src/resolution/c-fnptr-synthesizer.ts +++ b/src/resolution/c-fnptr-synthesizer.ts @@ -703,7 +703,7 @@ export async function cFnPointerDispatchEdges( if (prof) { prof.nodesMs += Date.now() - tN; prof.nodesN++; } const structs: CfnptrFileIn['structs'] = []; for (const st of fileNodes) { - if (st.kind !== 'struct') continue; + if (st.kind !== 'struct' && st.kind !== 'union') continue; // sliceLinesPre semantics ride along: falsy startLine never parses, // and `endLine ?? startLine` is applied here so the kernel sees the // exact slice bounds the JS sweep would use. @@ -740,7 +740,7 @@ export async function cFnPointerDispatchEdges( if (prof) { prof.nodesMs += Date.now() - tN; prof.nodesN++; } let lines: string[] | null = null; for (const st of fileNodes) { - if (st.kind !== 'struct') continue; + if (st.kind !== 'struct' && st.kind !== 'union') continue; lines ??= s.split('\n'); const body = sliceLinesPre(lines, st.startLine, st.endLine); const open = body.indexOf('{'); diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index 07f18cb..cf5620c 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -1827,6 +1827,7 @@ function resolveRustPathReference( n.name === leaf && (n.kind === 'function' || n.kind === 'struct' || + n.kind === 'union' || n.kind === 'enum' || n.kind === 'trait' || n.kind === 'type_alias' || diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 967f0b1..208213f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -798,12 +798,12 @@ function lookupCalleeReturnType( return candidates.find((n) => n.kind === 'function')?.returnType ?? null; } -/** Does the graph contain a class/struct named `name`'s last segment? */ +/** Does the graph contain an aggregate type named `name`'s last segment? */ function cppClassExists(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean { const last = cppLastSegment(name); return context .getNodesByName(last) - .some((n) => (n.kind === 'class' || n.kind === 'struct') && n.language === ref.language); + .some((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'union') && n.language === ref.language); } /** @@ -1771,7 +1771,7 @@ export function matchMethodCall( ); for (const classNode of classCandidates) { - if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') { + if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'union' || classNode.kind === 'interface') { // Skip cross-language class matches if (classNode.language !== ref.language) continue; @@ -1807,7 +1807,7 @@ export function matchMethodCall( ref.filePath, ); for (const classNode of fuzzyClassCandidates) { - if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') { + if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'union' || classNode.kind === 'interface') { // Skip cross-language class matches if (classNode.language !== ref.language) continue; diff --git a/src/search/query-utils.ts b/src/search/query-utils.ts index 1a7b121..e3db25b 100644 --- a/src/search/query-utils.ts +++ b/src/search/query-utils.ts @@ -393,6 +393,7 @@ export function kindBonus(kind: Node['kind']): number { interface: 9, type_alias: 6, struct: 6, + union: 6, trait: 9, enum: 5, component: 8,