From 85e9ac6762fc1e4290eb2ddf1241d406119d48b4 Mon Sep 17 00:00:00 2001 From: ctype_lab Date: Thu, 6 Aug 2026 15:46:53 +0900 Subject: [PATCH] fix(c,cpp,objc,rust): index union declarations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `union` declaration produced no symbol at all in any of the four languages that have one. The type never entered the graph, and neither did anything attached to it — in Rust every `impl Trait for MyUnion` lost its edge, and the impl's methods were left with a qualifiedName pointing at a type the graph did not contain. `union_specifier` / `union_item` were absent from the extraction layer entirely: no `Types` list on the TS side, no dispatch branch in either kernel walker. They join `structTypes` (kind `struct` — NodeKind has no `union`), which is the extension point the table-driven extractors already provide. The body guard in extractStruct is untouched, so a bodiless `union U;` stays a forward declaration and is still skipped, exactly like `struct U;`. `resolveTypeAliasKind` accepts `union_specifier` too, so `typedef union { … } N;` takes the typedef's name the way `typedef struct { … } N;` already did. Without it the anonymous union body would mint a second `` node beside the alias. Both walkers change together so kernel<->wasm parity holds. Co-Authored-By: Claude Opus 5 --- codegraph-kernel/src/ccpp/mod.rs | 14 ++++++++++---- codegraph-kernel/src/rustlang.rs | 6 ++++-- src/extraction/languages/c-cpp.ts | 30 ++++++++++++++++++++++++------ src/extraction/languages/objc.ts | 11 +++++++++-- src/extraction/languages/rust.ts | 5 ++++- 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index c95d312..ee68845 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -812,7 +812,10 @@ impl<'t> Walker<'t> { } else if self.variant == Variant::Cpp && kind == "class_specifier" { self.extract_class(node); skip_children = true; - } else if kind == "struct_specifier" { + } 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); skip_children = true; } else if kind == "enum_specifier" { @@ -1041,7 +1044,9 @@ impl<'t> Walker<'t> { resolved = Some("enum"); break; } - if child.kind() == "struct_specifier" && child.child_by_field_name("body").is_some() { + if matches!(child.kind(), "struct_specifier" | "union_specifier") + && child.child_by_field_name("body").is_some() + { resolved = Some("struct"); break; } @@ -1059,7 +1064,8 @@ impl<'t> Walker<'t> { self.stack.push(Scope { row, kind: "struct", name }); let type_child = node .child_by_field_name("type") - .or_else(|| self.find_child_by_kind(node, "struct_specifier")); + .or_else(|| self.find_child_by_kind(node, "struct_specifier")) + .or_else(|| self.find_child_by_kind(node, "union_specifier")); if let Some(tc) = type_child { self.extract_inheritance(tc, row); let body = tc.child_by_field_name("body").unwrap_or(tc); @@ -1556,7 +1562,7 @@ impl<'t> Walker<'t> { self.extract_class(node); return; } - if kind == "struct_specifier" { + if matches!(kind, "struct_specifier" | "union_specifier") { self.extract_struct(node); return; } diff --git a/codegraph-kernel/src/rustlang.rs b/codegraph-kernel/src/rustlang.rs index b5b9da3..a7d6067 100644 --- a/codegraph-kernel/src/rustlang.rs +++ b/codegraph-kernel/src/rustlang.rs @@ -446,7 +446,9 @@ impl<'t> Walker<'t> { } else if kind == "trait_item" { self.extract_interface(node); skip_children = true; - } else if kind == "struct_item" { + } 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); skip_children = true; } else if kind == "enum_item" { @@ -1130,7 +1132,7 @@ impl<'t> Walker<'t> { } // Structural nodes inside bodies. - if kind == "struct_item" { + if matches!(kind, "struct_item" | "union_item") { self.extract_struct(node); return; } diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index 6b5b45e..708985e 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -186,7 +186,11 @@ export const cExtractor: LanguageExtractor = { classTypes: [], methodTypes: [], interfaceTypes: [], - structTypes: ['struct_specifier'], + // `union U { … };` is a type DEFINITION, same as `struct U { … };` — it + // declares a named type whose members other code refers to. Extracted with + // kind `struct` because NodeKind has no `union`; a bodiless `union U;` is a + // forward declaration and still falls out via extractStruct's body guard. + structTypes: ['struct_specifier', 'union_specifier'], enumTypes: ['enum_specifier'], enumMemberTypes: ['enumerator'], typeAliasTypes: ['type_definition'], // typedef @@ -207,12 +211,18 @@ export const cExtractor: LanguageExtractor = { resolveTypeAliasKind: (node, _source) => { // C typedef: `typedef enum { ... } name;` or `typedef struct { ... } name;` // The inner enum_specifier/struct_specifier is anonymous, but we want the typedef name - // to become the enum/struct node name. + // to become the enum/struct node name. `typedef union { ... } name;` takes the + // same route — otherwise the union body would mint a second, `` node + // beside the alias. for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (!child) continue; if (child.type === 'enum_specifier' && getChildByField(child, 'body')) return 'enum'; - if (child.type === 'struct_specifier' && getChildByField(child, 'body')) return 'struct'; + if ( + (child.type === 'struct_specifier' || child.type === 'union_specifier') && + getChildByField(child, 'body') + ) + return 'struct'; } return undefined; }, @@ -1551,7 +1561,10 @@ export const cppExtractor: LanguageExtractor = { skipBodilessClass: true, methodTypes: ['function_definition'], interfaceTypes: [], - structTypes: ['struct_specifier'], + // See the C extractor: a named `union U { … };` is a definition, not an + // alias. C++ unions additionally carry member functions, which extract + // through the same body walk as a struct's. + structTypes: ['struct_specifier', 'union_specifier'], enumTypes: ['enum_specifier'], enumMemberTypes: ['enumerator'], typeAliasTypes: ['type_definition', 'alias_declaration'], // typedef and using @@ -1581,12 +1594,17 @@ export const cppExtractor: LanguageExtractor = { return undefined; }, resolveTypeAliasKind: (node, _source) => { - // C++ typedef: `typedef enum { ... } name;` or `typedef struct { ... } name;` + // C++ typedef: `typedef enum { ... } name;`, `typedef struct { ... } name;`, + // or `typedef union { ... } name;` — see the C extractor. for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (!child) continue; if (child.type === 'enum_specifier' && getChildByField(child, 'body')) return 'enum'; - if (child.type === 'struct_specifier' && getChildByField(child, 'body')) return 'struct'; + if ( + (child.type === 'struct_specifier' || child.type === 'union_specifier') && + getChildByField(child, 'body') + ) + return 'struct'; } return undefined; }, diff --git a/src/extraction/languages/objc.ts b/src/extraction/languages/objc.ts index cf5ecc4..668e226 100644 --- a/src/extraction/languages/objc.ts +++ b/src/extraction/languages/objc.ts @@ -102,7 +102,9 @@ export const objcExtractor: LanguageExtractor = { methodTypes: ['method_definition'], interfaceTypes: ['protocol_declaration'], interfaceKind: 'protocol', - structTypes: ['struct_specifier'], + // Objective-C is a C superset: `union U { … };` is a definition, same as in + // the C extractor. + structTypes: ['struct_specifier', 'union_specifier'], enumTypes: ['enum_specifier'], enumMemberTypes: ['enumerator'], typeAliasTypes: ['type_definition'], @@ -128,7 +130,12 @@ export const objcExtractor: LanguageExtractor = { const child = node.namedChild(i); if (!child) continue; if (child.type === 'enum_specifier' && getChildByField(child, 'body')) return 'enum'; - if (child.type === 'struct_specifier' && getChildByField(child, 'body')) return 'struct'; + // `typedef union { … } name;` resolves like `typedef struct` — see the C extractor. + if ( + (child.type === 'struct_specifier' || child.type === 'union_specifier') && + getChildByField(child, 'body') + ) + return 'struct'; } return undefined; }, diff --git a/src/extraction/languages/rust.ts b/src/extraction/languages/rust.ts index bdc4477..2015e62 100644 --- a/src/extraction/languages/rust.ts +++ b/src/extraction/languages/rust.ts @@ -41,7 +41,10 @@ export const rustExtractor: LanguageExtractor = { classTypes: [], // Rust has impl blocks methodTypes: ['function_item', 'function_signature_item'], interfaceTypes: ['trait_item'], - structTypes: ['struct_item'], + // `union U { … }` is a definition like `struct U { … }` — same `body:` + // (`field_declaration_list`) and the same `impl Trait for U` attachment + // point. Extracted with kind `struct` because NodeKind has no `union`. + structTypes: ['struct_item', 'union_item'], enumTypes: ['enum_item'], enumMemberTypes: ['enum_variant'], typeAliasTypes: ['type_item'], // Rust type aliases