Kaynağa Gözat

feat(kernel): preserve union nodes across extraction

ctype_lab 1 ay önce
ebeveyn
işleme
8e3cde6994

+ 2 - 1
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.

+ 27 - 20
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" {

+ 19 - 15
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" {

+ 1 - 1
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[];

+ 2 - 2
src/mcp/tools.ts

@@ -343,7 +343,7 @@ export function getExploreOutputBudget(fileCount: number): ExploreOutputBudget {
  */
 export const RELEVANCE_KIND_WEIGHT: Readonly<Record<string, number>> = {
   // 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<string, boolean>();
     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) {

+ 2 - 2
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('{');

+ 1 - 0
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' ||

+ 4 - 4
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;
 

+ 1 - 0
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,