feat(kernel): preserve union nodes across extraction

This commit is contained in:
ctype_lab
2026-08-06 17:11:26 +09:00
parent 6978acc92e
commit 8e3cde6994
9 changed files with 59 additions and 45 deletions
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,