Merge pull request #1516 from ctype-lab/fix/union-declarations-not-indexed

fix(c,cpp,objc,rust): index union declarations as a first-class `union` node kind (#1515)
This commit is contained in:
Colby Mchenry
2026-08-07 21:56:05 -05:00
committed by GitHub
30 changed files with 521 additions and 91 deletions
+2
View File
@@ -21,6 +21,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- C, C++, Objective-C and Rust unions are now indexed as first-class `union` nodes. A `union` declaration previously produced no symbol at all, so it never appeared in search or `codegraph_explore`, and anything attached to it disappeared with it — in Rust, every `impl SomeTrait for MyUnion` lost its edge, the methods from that impl were left pointing at a type the graph did not contain, and asking which types implement a trait quietly skipped the union ones. A union-shaped dispatch table in C now resolves its function pointers like a struct-shaped one. A `typedef union { … } Name;` in C keeps the typedef's name and is no longer mistaken for a plain type alias. Thanks @ctype-lab. Re-index after upgrading to pick up unions in existing projects. (#1515)
- A long-lived index no longer drifts away from what a fresh `codegraph index` would produce. When a file gained or lost a symbol, references to that name in files the sync never touched kept pointing at the definition that was correct before the change, and — because nothing distinguished two same-named definitions — the winner could come down to the order files happened to be written, which differs between a full index and a sync. On this project's own repository, replaying 80 commits through `sync` left 5.7% of connections wrong; it is now 1.3%, and the wrong-answers-still-being-asserted half drops by 99.7%. Since call edges are what flow questions follow and what `codegraph_explore` ranks files by, this quietly degraded answers as an index aged, with nothing to indicate it. Syncing is unchanged in speed, and an edit that only changes a function's body does no extra work at all. Set `CODEGRAPH_NO_REBIND=1` to opt out.
- `codegraph_explore` now concentrates its answer on the code that actually answers your question instead of spreading it across files that merely share a word with it, so more of the answer arrives in a single call. Thanks @LeDuyViet for the detailed measurements and reproduction. (#1500)
- Files only weakly related to your question now come back as a name, symbol and line number instead of spending the answer on their source — name one of them in a follow-up `codegraph_explore` to get it back in full. (#1500)
+1 -1
View File
@@ -66,7 +66,7 @@ The public API surface is `src/index.ts` — the `CodeGraph` class wires all the
Defined in `src/types.ts`. Both extractors and resolvers must use these exact strings.
- **NodeKind**: `file`, `module`, `class`, `struct`, `interface`, `trait`, `protocol`, `function`, `method`, `property`, `field`, `variable`, `constant`, `enum`, `enum_member`, `type_alias`, `namespace`, `parameter`, `import`, `export`, `route`, `component`.
- **NodeKind**: `file`, `module`, `class`, `struct`, `interface`, `trait`, `protocol`, `function`, `method`, `property`, `field`, `variable`, `constant`, `enum`, `enum_member`, `type_alias`, `namespace`, `parameter`, `import`, `export`, `route`, `component`, `union`.
- **EdgeKind**: `contains`, `calls`, `imports`, `exports`, `extends`, `implements`, `references`, `type_of`, `returns`, `instantiates`, `overrides`, `decorates`.
### Multi-agent installer
+39
View File
@@ -86,6 +86,45 @@ int dispatch(struct ops o) { return o.handler(); }
expect(edges.every((e) => e.via === 'ops.handler')).toBe(true);
});
it('bridges function-pointer fields declared in a union', async () => {
write('union-ops.c', `
union ops { int (*handler)(void); };
static int on_open(void) { return 1; }
static union ops the_ops = { .handler = on_open };
int dispatch(union ops o) { return o.handler(); }
`);
const edges = await load();
expect(has(edges, 'dispatch', 'on_open')).toBe(true);
expect(edges.every((e) => e.via === 'ops.handler')).toBe(true);
});
it('bridges an inline union table whose entries are macro-built', async () => {
write('inline-union.c', `
#define SLOT(fn) { fn }
static int on_open(void) { return 1; }
static union inline_ops { int (*handler)(void); } ops[] = { SLOT(on_open) };
int dispatch(union inline_ops o) { return o.handler(); }
`);
const edges = await load();
expect(has(edges, 'dispatch', 'on_open')).toBe(true);
});
it('bridges a union table declared through an object-macro type alias', async () => {
write('alias-union.c', `
#define OPS_TYPE union ops
#define SLOT(fn) { fn }
union ops { int (*handler)(void); };
static int on_open(void) { return 1; }
static OPS_TYPE ops[] = { SLOT(on_open) };
int dispatch(union ops o) { return o.handler(); }
`);
const edges = await load();
expect(has(edges, 'dispatch', 'on_open')).toBe(true);
});
it('bridges the typedef-field + field←field double-hop (the hook_demo.c shape)', async () => {
write('hook.c', `
typedef void (*hook_func)(void);
+16 -1
View File
@@ -135,10 +135,16 @@ export function validateEmail(email: string): boolean {
`
);
fs.writeFileSync(
path.join(srcDir, 'callback_ops.c'),
`union CallbackOps { int (*run)(int); };
`
);
// Initialize CodeGraph
cg = CodeGraph.initSync(testDir, {
config: {
include: ['**/*.ts'],
include: ['**/*.ts', '**/*.c'],
exclude: [],
},
});
@@ -194,6 +200,15 @@ export function validateEmail(email: string): boolean {
).toBe(true);
});
it('includes union definitions in the default context search', async () => {
const result = await cg.findRelevantContext('CallbackOps');
const union = [...result.nodes.values()].find(
(node) => node.kind === 'union' && node.name === 'CallbackOps'
);
expect(union).toBeDefined();
});
it('should include edges in the result', async () => {
const result = await cg.findRelevantContext('checkout', {
traversalDepth: 2,
+126
View File
@@ -1174,6 +1174,51 @@ impl Counter {
);
expect(implRefs).toHaveLength(0);
});
it('should extract union declarations and their impl edges', () => {
const code = `
pub union Reg {
pub raw: u32,
pub halves: [u16; 2],
}
pub trait Describe {
fn describe(&self) -> u32;
}
impl Describe for Reg {
fn describe(&self) -> u32 {
unsafe { self.raw }
}
}
`;
const result = extractFromSource('reg.rs', code);
// A union is a first-class type definition, not an alias — it must be a
// node, or the impl below has no source endpoint to hang off.
const reg = result.nodes.find((n) => n.name === 'Reg');
expect(reg).toBeDefined();
expect(reg?.kind).toBe('union');
const implRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'implements' && r.referenceName === 'Describe'
);
expect(implRef).toBeDefined();
expect(implRef?.fromNodeId).toBe(reg?.id);
// The impl's method attaches to the union, not to the file — without a Reg
// node it was an orphan whose qualifiedName pointed at a type that did not
// exist in the graph.
const implMethod = result.nodes.find(
(n) => n.kind === 'method' && n.qualifiedName?.includes('Reg')
);
expect(implMethod).toBeDefined();
expect(
result.edges.some(
(e) => e.kind === 'contains' && e.source === reg?.id && e.target === implMethod?.id
)
).toBe(true);
});
});
describe('Java Extraction', () => {
@@ -5642,6 +5687,71 @@ std::string use() {
});
});
describe('C/C++ union declarations', () => {
it('extracts a named union as a type node, but not a forward declaration', () => {
const code = `
union packet_hdr {
unsigned int raw;
unsigned short port;
};
/* forward declaration — not a definition */
union opaque_hdr;
static unsigned int hdr_raw(union packet_hdr *h) { return h->raw; }
`;
const result = extractFromSource('packet.c', code);
const hdr = result.nodes.find((n) => n.name === 'packet_hdr');
expect(hdr).toBeDefined();
expect(hdr?.kind).toBe('union');
// Same rule as `struct Foo;`: bodiless is a forward declaration, so it must
// not mint a phantom node beside the real definition.
expect(result.nodes.some((n) => n.name === 'opaque_hdr')).toBe(false);
// Exactly one node for the type — the definition — so a call site or a
// `union packet_hdr *` parameter has a single resolution target.
expect(result.nodes.filter((n) => n.name === 'packet_hdr')).toHaveLength(1);
});
it('gives a typedef union the typedef name, not a second <anonymous> node', () => {
const code = `
typedef union {
unsigned int u;
float f;
} word_t;
`;
const result = extractFromSource('word.c', code);
const word = result.nodes.find((n) => n.name === 'word_t');
expect(word?.kind).toBe('union');
// Resolved through the typedef the same way `typedef struct { … } X;` is,
// so the anonymous union body does not become its own node.
expect(result.nodes.some((n) => n.name === '<anonymous>')).toBe(false);
});
it('extracts a C++ union with member functions', () => {
const code = `
union Value {
int i;
double d;
int as_int() const { return i; }
};
`;
const result = extractFromSource('value.cpp', code);
const value = result.nodes.find((n) => n.name === 'Value');
expect(value?.kind).toBe('union');
const asInt = result.nodes.find((n) => n.name === 'as_int');
expect(asInt).toBeDefined();
expect(
result.edges.some((e) => e.kind === 'contains' && e.source === value?.id && e.target === asInt?.id)
).toBe(true);
});
});
describe('Dart mixins and type references', () => {
let tempDir: string;
let cg: CodeGraph;
@@ -8336,6 +8446,22 @@ void helperFunction(int count) {
expect(imports).toContain('MyClass.h');
});
it('extracts union declarations as first-class union nodes', () => {
const code = `
typedef union {
unsigned int raw;
float value;
} NumberBits;
union opaque_bits;
`;
const result = extractFromSource('NumberBits.m', code);
const numberBits = result.nodes.find((n) => n.name === 'NumberBits');
expect(numberBits?.kind).toBe('union');
expect(result.nodes.some((n) => n.name === 'opaque_bits')).toBe(false);
});
it('should record inheritance and protocol conformance', () => {
const result = extractFromSource('App.m', sample);
const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends');
@@ -152,3 +152,19 @@ static void ratelimited_warn(void) {
static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);
use_ptr(&ratelimit, 0);
}
/* named union definition, forward declaration, and anonymous typedef union —
the definition is a node, the forward decl is not (#UNION) */
union packet_hdr {
unsigned int raw;
struct { unsigned char ver, flags; } parts;
};
union opaque_hdr;
typedef union {
unsigned int u;
float f;
} word_t;
static unsigned int hdr_raw(union packet_hdr *h) { return h->raw; }
@@ -209,3 +209,10 @@ fn mount() {
}
routes![top_level_h];
pub union Reg {
pub raw: u32,
pub halves: [u16; 2],
}
impl Base for Reg {}
+121
View File
@@ -1020,6 +1020,105 @@ def bootstrap():
expect(callsToUserService).toHaveLength(0);
});
it('promotes calls→instantiates when target resolves to a C++ union', async () => {
// `Packet()` value-initializes the union. The extractor emits a calls
// reference for that expression, so resolution must preserve the
// class-like promotion that unions received when they were structs.
fs.writeFileSync(
path.join(tempDir, 'packet.cpp'),
`union Packet { unsigned int raw; };
void initialize() { Packet(); }
`
);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
const packet = cg.getNodesByKind('union').find((n) => n.name === 'Packet');
const initialize = cg.getNodesByKind('function').find((n) => n.name === 'initialize');
expect(packet).toBeDefined();
expect(initialize).toBeDefined();
const outgoing = cg.getOutgoingEdges(initialize!.id);
expect(outgoing.some((e) => e.kind === 'instantiates' && e.target === packet!.id)).toBe(true);
expect(outgoing.some((e) => e.kind === 'calls' && e.target === packet!.id)).toBe(false);
});
it('resolves a static call through an imported C++ union to its member', async () => {
fs.writeFileSync(
path.join(tempDir, 'ops.hpp'),
`union Ops {
static int run() { return 1; }
};
`
);
fs.writeFileSync(
path.join(tempDir, 'main.cpp'),
`#include "ops.hpp"
int invoke() { return Ops::run(); }
`
);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
const invoke = cg.getNodesByKind('function').find((n) => n.name === 'invoke');
const run = cg.getNodesByKind('method').find((n) => n.name === 'run');
expect(invoke).toBeDefined();
expect(run).toBeDefined();
const outgoing = cg.getOutgoingEdges(invoke!.id);
expect(outgoing.some((e) => e.kind === 'calls' && e.target === run!.id)).toBe(true);
});
it('bridges a Rust trait method to a union implementor (interface-impl)', async () => {
// A Rust union can `impl Trait` exactly as a struct can. Trait-dispatch
// synthesis enumerates concrete kinds explicitly, so a union implementor
// only becomes a candidate if `union` is in that list. Making unions
// first-class nodes is not enough on its own: without this, `Reg` has an
// `implements` edge and is still silently dropped from the fan-out, so
// "who implements this trait" answers wrongly rather than incompletely
// — the struct beside it resolves and the union does not (#1515).
fs.writeFileSync(
path.join(tempDir, 'lib.rs'),
`pub union Reg { pub raw: u32 }
pub struct Ctl { pub n: u32 }
pub trait Describe { fn describe(&self) -> String; }
impl Describe for Reg { fn describe(&self) -> String { "reg".into() } }
impl Describe for Ctl { fn describe(&self) -> String { "ctl".into() } }
`
);
cg = await CodeGraph.init(tempDir, { index: true });
const methods = cg.getNodesByKind('method');
const traitMethod = methods.find((n) => n.qualifiedName === 'Describe::describe');
const unionImpl = methods.find((n) => n.qualifiedName === 'Reg::describe');
const structImpl = methods.find((n) => n.qualifiedName === 'Ctl::describe');
expect(traitMethod, 'trait method should be in the graph').toBeDefined();
expect(unionImpl, 'union impl method should be in the graph').toBeDefined();
expect(structImpl, 'struct impl method should be in the graph').toBeDefined();
const synth = cg
.getOutgoingEdges(traitMethod!.id)
.filter((e) => e.kind === 'calls' && e.provenance === 'heuristic');
const targets = new Set(synth.map((e) => e.target));
// The struct implementor bridged before unions were nodes at all; it is
// the control that proves the synthesizer ran for this trait.
expect(targets.has(structImpl!.id), 'struct implementor should bridge').toBe(true);
expect(targets.has(unionImpl!.id), 'union implementor should bridge').toBe(true);
const unionEdge = synth.find((e) => e.target === unionImpl!.id);
expect(
(unionEdge!.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy
).toBe('interface-impl');
});
it('records instantiates for C++ stack/brace construction, targeting the class (#1035)', async () => {
// `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init)
// carry the constructor args directly on the declarator — there's no
@@ -2169,6 +2268,28 @@ func main() {
expect(result?.targetNodeId).toBe('class:logger.ts:Logger:10');
});
it('prefers a union candidate over a function for `instantiates` refs', () => {
const fn: Node = {
id: 'func:packet.cpp:Packet:5', kind: 'function', name: 'Packet',
qualifiedName: 'packet.cpp::Packet', filePath: 'packet.cpp', language: 'cpp',
startLine: 5, endLine: 7, startColumn: 0, endColumn: 0, updatedAt: Date.now(),
};
const union: Node = {
id: 'union:packet.hpp:Packet:10', kind: 'union', name: 'Packet',
qualifiedName: 'packet.hpp::Packet', filePath: 'packet.hpp', language: 'cpp',
startLine: 10, endLine: 14, startColumn: 0, endColumn: 0, updatedAt: Date.now(),
};
const ref = {
fromNodeId: 'func:main.cpp:initialize:1',
referenceName: 'Packet',
referenceKind: 'instantiates' as const,
line: 5, column: 0, filePath: 'main.cpp', language: 'cpp' as const,
};
const result = matchReference(ref, baseContext([fn, union]));
expect(result?.targetNodeId).toBe('union:packet.hpp:Packet:10');
});
it('prefers a function candidate over a non-function for `decorates` refs', () => {
const variable: Node = {
id: 'var:config.ts:Inject:5', kind: 'variable', name: 'Inject',
+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.
+26 -13
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);
@@ -813,7 +813,10 @@ impl<'t> Walker<'t> {
self.extract_class(node);
skip_children = true;
} else if kind == "struct_specifier" {
self.extract_struct(node);
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);
@@ -925,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 {
@@ -971,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 {
@@ -980,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);
@@ -1045,21 +1048,27 @@ impl<'t> Walker<'t> {
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"));
.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);
@@ -1557,7 +1566,11 @@ impl<'t> Walker<'t> {
return;
}
if kind == "struct_specifier" {
self.extract_struct(node);
self.extract_aggregate(node, "struct");
return;
}
if kind == "union_specifier" {
self.extract_aggregate(node, "union");
return;
}
if kind == "enum_specifier" {
+27 -9
View File
@@ -432,8 +432,19 @@ struct InlineScan {
fn scan_inline_structs(s: &[u8]) -> InlineScan {
let mut out = InlineScan { ptr: false, types: Vec::new(), tags: Vec::new() };
let mut last = 0;
while let Some(t) = find_word(s, b"struct", last) {
let after_kw = t + 6;
loop {
let next_struct = find_word(s, b"struct", last);
let next_union = find_word(s, b"union", last);
let Some((t, keyword_len)) = (match (next_struct, next_union) {
(Some(st), Some(un)) if st < un => Some((st, 6)),
(Some(_), Some(un)) => Some((un, 5)),
(Some(st), None) => Some((st, 6)),
(None, Some(un)) => Some((un, 5)),
(None, None) => None,
}) else {
break;
};
let after_kw = t + keyword_len;
let ws = skip_jsws(s, after_kw);
if ws == after_kw || !is_word_at(s, ws) {
last = t + 1;
@@ -569,10 +580,10 @@ fn init_body(s: &[u8], p: usize) -> Option<(String, usize)> {
let i = skip_jsws(s, p);
let mods = modifier_positions(s, i);
for &pos in mods.iter().rev() {
for with_struct in [true, false] {
let q = if with_struct {
if s.len() >= pos + 6 && &s[pos..pos + 6] == b"struct" {
let e = pos + 6;
for keyword in [Some(b"struct".as_slice()), Some(b"union".as_slice()), None] {
let q = if let Some(keyword) = keyword {
if s.len() >= pos + keyword.len() && &s[pos..pos + keyword.len()] == keyword {
let e = pos + keyword.len();
let w = skip_jsws(s, e);
if w == e {
continue;
@@ -729,12 +740,19 @@ fn alias_line(line: &[u8]) -> Option<&[u8]> {
if v0 == name_end {
return None; // [ \t]+ before the value
}
// (?:struct[ \t]+)* greedy, k-descending on value failure.
// (?:(?:struct|union)[ \t]+)* greedy, k-descending on value failure.
let mut stack = vec![v0];
loop {
let cur = *stack.last().unwrap();
if line.len() >= cur + 6 && &line[cur..cur + 6] == b"struct" {
let e = cur + 6;
let keyword_len = if line.len() >= cur + 6 && &line[cur..cur + 6] == b"struct" {
Some(6)
} else if line.len() >= cur + 5 && &line[cur..cur + 5] == b"union" {
Some(5)
} else {
None
};
if let Some(keyword_len) = keyword_len {
let e = cur + keyword_len;
let w2 = skip_sp_tab(line, e);
if w2 > e {
stack.push(w2);
+17 -11
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);
@@ -447,7 +447,10 @@ impl<'t> Walker<'t> {
self.extract_interface(node);
skip_children = true;
} else if kind == "struct_item" {
self.extract_struct(node);
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);
@@ -529,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 {
@@ -579,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 {
@@ -589,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);
@@ -1057,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);
@@ -1131,7 +1133,11 @@ impl<'t> Walker<'t> {
// Structural nodes inside bodies.
if kind == "struct_item" {
self.extract_struct(node);
self.extract_aggregate(node, "struct");
return;
}
if kind == "union_item" {
self.extract_aggregate(node, "union");
return;
}
if kind == "enum_item" {
@@ -140,6 +140,8 @@ walker mirrors, with file:line anchors (as of `705e501`). Read WITH
**cExtractor (line 180):** functionTypes=[function_definition]; NO
class/method/interface types; structTypes=[struct_specifier];
unionTypes=[union_specifier] (a named `union U { … };` is a definition, and
`typedef union { … } N;` resolves to a first-class `union` node);
enumTypes=[enum_specifier]; enumMemberTypes=[enumerator];
typeAliasTypes=[type_definition]; importTypes=[preproc_include];
callTypes=[call_expression]; variableTypes=[declaration];
@@ -54,7 +54,9 @@ Types: functionTypes=[`function_item`, **`function_signature_item`**] (the
latter = a trait method DECLARATION `fn render(&self);` — extracted so a
trait's method set is first-class); classTypes=[] (impl blocks instead);
methodTypes = same two; interfaceTypes=[`trait_item`] with
**interfaceKind:'trait'**; structTypes=[`struct_item`]; enumTypes=[`enum_item`];
**interfaceKind:'trait'**; structTypes=[`struct_item`];
unionTypes=[`union_item`] (same body walk, distinct `union` node kind);
enumTypes=[`enum_item`];
enumMemberTypes=[`enum_variant`]; typeAliasTypes=[`type_item`];
importTypes=[`use_declaration`]; callTypes=[`call_expression`];
variableTypes=[`let_declaration`, `const_item`, `static_item`].
+5 -5
View File
@@ -157,7 +157,7 @@ const DEFAULT_BUILD_OPTIONS: Required<BuildContextOptions> = {
* they tell you something exists, not how it works.
*/
const HIGH_VALUE_NODE_KINDS: NodeKind[] = [
'function', 'method', 'class', 'interface', 'type_alias', 'struct', 'trait',
'function', 'method', 'class', 'interface', 'type_alias', 'struct', 'union', 'trait',
'component', 'route', 'variable', 'constant', 'enum', 'module', 'namespace',
];
@@ -503,7 +503,7 @@ export class ContextBuilder {
// like RestController, BulkRequest, AllocationService — not nodes named exactly that.
// Also tries stem variants: "caching" → "cache" finds Cache, CacheBuilder.
if (symbolsFromQuery.length > 0) {
const definitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'trait',
const definitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'union', 'trait',
'protocol', 'enum', 'type_alias'];
// Expand symbols with stem variants for broader definition matching
const expandedSymbols = new Set(symbolsFromQuery);
@@ -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[];
@@ -754,7 +754,7 @@ export class ContextBuilder {
// LIKE reliably finds these substring matches. Results are appended with
// guaranteed slots so they don't compete with higher-scoring prefix matches.
if (symbolsFromQuery.length > 0) {
const camelDefinitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'trait',
const camelDefinitionKinds: NodeKind[] = ['class', 'interface', 'struct', 'union', 'trait',
'protocol', 'enum', 'type_alias'];
// Callable kinds participate too: in service-layer codebases the
// camel-infix definers of a queried FIELD are methods/functions
@@ -977,7 +977,7 @@ export class ContextBuilder {
// before reaching extends/implements neighbors. This dedicated step
// ensures subclasses and superclasses always appear in results.
// Budget: up to maxNodes/4 hierarchy nodes to avoid flooding.
const typeHierarchyKinds = new Set<string>(['class', 'interface', 'struct', 'trait', 'protocol']);
const typeHierarchyKinds = new Set<string>(['class', 'interface', 'struct', 'union', 'trait', 'protocol']);
const maxHierarchyNodes = Math.ceil(opts.maxNodes / 4);
let hierarchyNodesAdded = 0;
for (const result of filteredResults) {
+23 -4
View File
@@ -187,6 +187,9 @@ export const cExtractor: LanguageExtractor = {
methodTypes: [],
interfaceTypes: [],
structTypes: ['struct_specifier'],
// A bodiless `union U;` is a forward declaration; the aggregate extractor
// applies the same body requirement it uses for C structs.
unionTypes: ['union_specifier'],
enumTypes: ['enum_specifier'],
enumMemberTypes: ['enumerator'],
typeAliasTypes: ['type_definition'], // typedef
@@ -207,12 +210,19 @@ 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, `<anonymous>` 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' &&
getChildByField(child, 'body')
)
return 'struct';
if (child.type === 'union_specifier' && getChildByField(child, 'body')) return 'union';
}
return undefined;
},
@@ -1552,6 +1562,9 @@ export const cppExtractor: LanguageExtractor = {
methodTypes: ['function_definition'],
interfaceTypes: [],
structTypes: ['struct_specifier'],
// C++ unions additionally carry member functions, which extract through the
// same aggregate-body walk as structs while preserving their distinct kind.
unionTypes: ['union_specifier'],
enumTypes: ['enum_specifier'],
enumMemberTypes: ['enumerator'],
typeAliasTypes: ['type_definition', 'alias_declaration'], // typedef and using
@@ -1581,12 +1594,18 @@ 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' &&
getChildByField(child, 'body')
)
return 'struct';
if (child.type === 'union_specifier' && getChildByField(child, 'body')) return 'union';
}
return undefined;
},
+5 -1
View File
@@ -103,6 +103,8 @@ export const objcExtractor: LanguageExtractor = {
interfaceTypes: ['protocol_declaration'],
interfaceKind: 'protocol',
structTypes: ['struct_specifier'],
// Objective-C is a C superset: union declarations preserve their own kind.
unionTypes: ['union_specifier'],
enumTypes: ['enum_specifier'],
enumMemberTypes: ['enumerator'],
typeAliasTypes: ['type_definition'],
@@ -128,7 +130,9 @@ 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';
if (child.type === 'struct_specifier' && getChildByField(child, 'body'))
return 'struct';
if (child.type === 'union_specifier' && getChildByField(child, 'body')) return 'union';
}
return undefined;
},
+3
View File
@@ -42,6 +42,9 @@ export const rustExtractor: LanguageExtractor = {
methodTypes: ['function_item', 'function_signature_item'],
interfaceTypes: ['trait_item'],
structTypes: ['struct_item'],
// Unions share struct member syntax and impl attachment, but retain their
// distinct semantic kind in the graph.
unionTypes: ['union_item'],
enumTypes: ['enum_item'],
enumMemberTypes: ['enum_variant'],
typeAliasTypes: ['type_item'], // Rust type aliases
+2
View File
@@ -102,6 +102,8 @@ export interface LanguageExtractor {
interfaceTypes: string[];
/** Node types that represent structs */
structTypes: string[];
/** Node types that represent unions */
unionTypes?: string[];
/** Node types that represent enums */
enumTypes: string[];
/** Node types that represent enum members/cases (e.g. Swift: 'enum_entry', Rust: 'enum_variant') */
+38 -15
View File
@@ -1066,6 +1066,11 @@ export class TreeSitterExtractor {
this.extractStruct(node);
skipChildren = true; // extractStruct visits body children
}
// Check for union declarations
else if (this.extractor.unionTypes?.includes(nodeType)) {
this.extractUnion(node);
skipChildren = true; // extractUnion visits body children
}
// Check for enum declarations
else if (this.extractor.enumTypes.includes(nodeType)) {
this.extractEnum(node);
@@ -1487,7 +1492,7 @@ export class TreeSitterExtractor {
/**
* Check if the current node stack indicates we are inside a class-like node
* (class, struct, interface, trait). File nodes do not count as class-like.
* (class, struct, union, interface, trait). File nodes do not count as class-like.
*/
private isInsideClassLikeNode(): boolean {
if (this.nodeStack.length === 0) return false;
@@ -1498,6 +1503,7 @@ export class TreeSitterExtractor {
return (
parentNode.kind === 'class' ||
parentNode.kind === 'struct' ||
parentNode.kind === 'union' ||
parentNode.kind === 'interface' ||
parentNode.kind === 'trait' ||
parentNode.kind === 'enum' ||
@@ -1807,7 +1813,7 @@ export class TreeSitterExtractor {
(n) =>
n.name === receiverType &&
n.filePath === this.filePath &&
(n.kind === 'struct' || n.kind === 'class' || n.kind === 'enum' || n.kind === 'trait')
(n.kind === 'struct' || n.kind === 'union' || n.kind === 'class' || n.kind === 'enum' || n.kind === 'trait')
);
if (ownerNode) {
this.edges.push({
@@ -1873,6 +1879,16 @@ export class TreeSitterExtractor {
* Extract a struct
*/
private extractStruct(node: SyntaxNode): void {
this.extractAggregate(node, 'struct');
}
/** Extract a union while sharing the member-walk behavior of aggregate types. */
private extractUnion(node: SyntaxNode): void {
this.extractAggregate(node, 'union');
}
/** Extract a struct-like declaration without conflating its semantic kind. */
private extractAggregate(node: SyntaxNode, kind: 'struct' | 'union'): void {
if (!this.extractor) return;
// Skip forward declarations and type references (no body = not a definition)
@@ -1886,24 +1902,24 @@ export class TreeSitterExtractor {
const visibility = this.extractor.getVisibility?.(node);
const isExported = this.extractor.isExported?.(node, this.source);
const structNode = this.createNode('struct', name, node, {
const aggregateNode = this.createNode(kind, name, node, {
docstring,
visibility,
isExported,
});
if (!structNode) return;
if (!aggregateNode) return;
// Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable)
this.extractInheritance(node, structNode.id);
this.extractInheritance(node, aggregateNode.id);
// C# primary-constructor parameter dependencies (`struct P(int x)`, and
// `record struct M(decimal Amount)` which the grammar nests here).
this.extractCsharpPrimaryCtorParamRefs(node, structNode.id);
this.extractCsharpPrimaryCtorParamRefs(node, aggregateNode.id);
// Push to stack for field extraction (bodiless positional records have
// no members to visit)
if (body) {
this.nodeStack.push(structNode.id);
this.nodeStack.push(aggregateNode.id);
for (let i = 0; i < body.namedChildCount; i++) {
const child = body.namedChild(i);
if (child) {
@@ -2905,17 +2921,20 @@ export class TreeSitterExtractor {
// (e.g. Go: `type Foo struct { ... }` is a type_spec wrapping struct_type)
const resolvedKind = this.extractor.resolveTypeAliasKind?.(node, this.source);
if (resolvedKind === 'struct') {
const structNode = this.createNode('struct', name, node, { docstring, isExported });
if (!structNode) return true;
if (resolvedKind === 'struct' || resolvedKind === 'union') {
const aggregateNode = this.createNode(resolvedKind, name, node, { docstring, isExported });
if (!aggregateNode) return true;
// Visit body children for field extraction
this.nodeStack.push(structNode.id);
// Try Go-style 'type' field first, then find inner struct child (C typedef struct)
this.nodeStack.push(aggregateNode.id);
// Try Go-style 'type' field first, then find the matching inner aggregate child.
const typeChild = getChildByField(node, 'type')
|| this.findChildByTypes(node, this.extractor.structTypes);
|| this.findChildByTypes(
node,
resolvedKind === 'union' ? (this.extractor.unionTypes ?? []) : this.extractor.structTypes
);
if (typeChild) {
// Extract struct embedding (e.g. Go: `type DB struct { *Head; Queryable }`)
this.extractInheritance(typeChild, structNode.id);
this.extractInheritance(typeChild, aggregateNode.id);
const body = getChildByField(typeChild, this.extractor.bodyField) || typeChild;
for (let i = 0; i < body.namedChildCount; i++) {
const child = body.namedChild(i);
@@ -5271,6 +5290,10 @@ export class TreeSitterExtractor {
this.extractStruct(node);
return;
}
if (this.extractor!.unionTypes?.includes(nodeType)) {
this.extractUnion(node);
return;
}
if (this.extractor!.enumTypes.includes(nodeType)) {
this.extractEnum(node);
return;
@@ -5745,7 +5768,7 @@ export class TreeSitterExtractor {
*/
private findNodeByName(name: string): string | undefined {
for (const node of this.nodes) {
if (node.name === name && (node.kind === 'struct' || node.kind === 'enum' || node.kind === 'class')) {
if (node.name === name && (node.kind === 'struct' || node.kind === 'union' || node.kind === 'enum' || node.kind === 'class')) {
return node.id;
}
}
+2
View File
@@ -173,6 +173,7 @@ export class GraphQueryManager {
const allNodes: Node[] = [];
const kinds: Node['kind'][] = [
'class',
'union',
'function',
'method',
'interface',
@@ -347,6 +348,7 @@ export class GraphQueryManager {
'module',
'class',
'struct',
'union',
'interface',
'trait',
'function',
+1 -1
View File
@@ -564,7 +564,7 @@ export class GraphTraverser {
// into their children so that callers of contained methods appear in impact
const focalNode = this.queries.getNodeById(nodeId);
if (focalNode) {
const containerKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol', 'module', 'enum']);
const containerKinds = new Set(['class', 'interface', 'struct', 'union', 'trait', 'protocol', 'module', 'enum']);
if (containerKinds.has(focalNode.kind)) {
const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
if (containsEdges.length > 0) {
+6 -6
View File
@@ -118,7 +118,7 @@ const RUST_PATH_PREFIXES = new Set(['crate', 'super', 'self']);
* multi-thousand-character wall of source that bloats the agent's context.
*/
const CONTAINER_NODE_KINDS = new Set<NodeKind>([
'class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'namespace', 'module',
'class', 'struct', 'union', 'interface', 'trait', 'protocol', 'enum', 'namespace', 'module',
]);
/** Last `::` / `.` / `/`-separated segment of a qualified symbol. */
@@ -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.
@@ -3025,7 +3025,7 @@ export class ToolHandler {
const ROOT_CAP = 5; // only the symbols the query actually targeted
const FILE_CAP = 4; // caller files listed per symbol before "+N more"
const MEANINGFUL = new Set<string>([
'function', 'method', 'class', 'interface', 'struct', 'trait', 'protocol',
'function', 'method', 'class', 'interface', 'struct', 'union', 'trait', 'protocol',
'enum', 'type_alias', 'component', 'constant', 'variable', 'property', 'field',
]);
const rel = (p: string) => p.replace(/\\/g, '/');
@@ -3568,7 +3568,7 @@ export class ToolHandler {
// displaces a flow-central file. Bounded: only the few named seeds, only the
// types in their signatures.
const CALLABLE_KINDS = new Set(['method', 'function', 'component', 'constructor']);
const TYPE_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
const TYPE_KINDS = new Set(['class', 'struct', 'union', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
const SIG_EDGE = new Set(['references', 'type_of', 'returns']);
const changeSurfaceCandidates: Node[] = [];
const seenChangeSurface = new Set<string>();
@@ -4103,7 +4103,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) {
@@ -4854,7 +4854,7 @@ export class ToolHandler {
// query actually asked about (#185 follow-up — Session.swift in
// Alamofire is the canonical case: the `Session` class spans ~1,400
// lines). We want the granular symbols inside, not the envelope.
const ENVELOPE_KINDS = new Set(['file', 'module', 'class', 'struct', 'interface', 'enum', 'namespace', 'protocol', 'trait', 'component']);
const ENVELOPE_KINDS = new Set(['file', 'module', 'class', 'struct', 'union', 'interface', 'enum', 'namespace', 'protocol', 'trait', 'component']);
// Cluster from this file's gathered nodes PLUS any callable the agent NAMED that
// lives here. Explore's relevance gather can miss a named method def in a huge
// non-sibling file — Django's query.py is 3,040 lines and `_fetch_all` (L2237)
+16 -14
View File
@@ -296,7 +296,7 @@ function resolveTypeName(name: string, objEnv: Map<string, string> | undefined):
let n = name;
for (let i = 0; objEnv && i < 5; i++) {
const v = objEnv.get(n);
const t = v?.trim().match(/^(?:struct\s+)?(\w+)$/);
const t = v?.trim().match(/^(?:(?:struct|union)\s+)?(\w+)$/);
if (!t) break;
n = t[1]!;
}
@@ -370,20 +370,20 @@ const INCLUDABLE_EXT = /\.(def|inc|h|hh|hpp|hxx|c|cc|cpp|cxx|ipp|tcc|tbl)$/i;
* are excluded: `resolveTypeName` would rewrite to a dead-end token that can
* never name a struct, so skipping them is exact, and it drops the register
* flood. */
const OBJ_ALIAS_RE = /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(?:struct[ \t]+)*[A-Za-z_]\w*[ \t\r]*$/gm;
const OBJ_ALIAS_RE = /^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(?:(?:struct|union)[ \t]+)*[A-Za-z_]\w*[ \t\r]*$/gm;
/** `(?:struct )?TYPE name[opt] = {` initializers, where TYPE is a struct that
* has ≥1 fn-pointer field. Handles both single (`= {…}`) and array
* (`[] = { {…}, {…} }`) forms. Macro calls inside an element are expanded first. */
const INIT_RE =
/(?:^|[;{}])\s*(?:(?:static|const|extern|register|volatile)\s+)*(?:struct\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{/g;
/(?:^|[;{}])\s*(?:(?:static|const|extern|register|volatile)\s+)*(?:(?:struct|union)\s+)?(\w+)\s+(\w+)\s*(\[[^\]]*\])?\s*=\s*\{/g;
/** `struct TAG { … } var[opt] [= {…}]` — the struct is defined INLINE with the
* table (vim's `cmdname`/`nv_cmd`); its layout never became a node, so parse it
* here and register it before reading the entries. No leading anchor: a
* `struct TAG {` with a brace body is always a definition (it may be preceded
* by a `#define …` line ending in a digit, as in vim), and the trailing
* `var … = {` check below is what distinguishes a TABLE from a plain type. */
const INLINE_STRUCT_RE = /\bstruct\s+(\w+)\s*\{/g;
const INLINE_STRUCT_RE = /\b(?:struct|union)\s+(\w+)\s*\{/g;
/** `(?:static …)* ELEMTYPE [*] name[…] = { … }` — a bare array of function
* pointers (no struct wrapper). The optional `*` covers a function-TYPE
* typedef element (`opcode_t *opcodes[]`); a function-pointer typedef element
@@ -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('{');
@@ -854,12 +854,14 @@ export async function cFnPointerDispatchEdges(
if (fields.some((f) => f.isFnPtr)) structLayout.set(name, fields);
};
for (const st of (ctx.iterateNodesByKind?.('struct') ?? ctx.getNodesByKind('struct'))) {
if ((++scannedFiles & 255) === 0) await onYield();
if (!C_CPP_EXT.test(st.filePath)) continue;
const rawFields = rawFieldsByNode.get(st.id);
if (!rawFields) continue; // file unreadable or body unparsable at sweep time — the old pass skipped it too
registerStructLayout(st.name, classifyFields(rawFields));
for (const kind of ['struct', 'union'] as const) {
for (const st of (ctx.iterateNodesByKind?.(kind) ?? ctx.getNodesByKind(kind))) {
if ((++scannedFiles & 255) === 0) await onYield();
if (!C_CPP_EXT.test(st.filePath)) continue;
const rawFields = rawFieldsByNode.get(st.id);
if (!rawFields) continue; // file unreadable or body unparsable at sweep time — the old pass skipped it too
registerStructLayout(st.name, classifyFields(rawFields));
}
}
rawFieldsByNode.clear();
if (prof) { prof.B = Date.now() - tPass; tPass = Date.now(); }
@@ -1211,7 +1213,7 @@ export async function cFnPointerDispatchEdges(
const recvTypeIn = (fnSrc: string, recv: string): string | null => {
let re = recvReCache.get(recv);
if (!re) {
re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${recv}\\b\\s*(?:[,)=;]|\\[)`, 'g');
re = new RegExp(`(?:(?:struct|union)\\s+)?(\\w+)\\s*\\*?\\s*\\b${recv}\\b\\s*(?:[,)=;]|\\[)`, 'g');
recvReCache.set(recv, re);
}
re.lastIndex = 0;
@@ -1230,7 +1232,7 @@ export async function cFnPointerDispatchEdges(
const varTypeIn = (fnSrc: string, v: string): string | null => {
let re = varReCache.get(v);
if (!re) {
re = new RegExp(`(?:struct\\s+)?(\\w+)\\s*\\*?\\s*\\b${escapeRe(v)}\\b\\s*(?:[,)=;]|\\[)`, 'g');
re = new RegExp(`(?:(?:struct|union)\\s+)?(\\w+)\\s*\\*?\\s*\\b${escapeRe(v)}\\b\\s*(?:[,)=;]|\\[)`, 'g');
varReCache.set(v, re);
}
re.lastIndex = 0;
+1 -1
View File
@@ -1061,7 +1061,7 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield
// Concrete-side kinds vary by language: `class` covers Java / Kotlin /
// C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value
// types that conform to protocols. Iterate both.
const concreteKinds = ['class', 'struct'] as const;
const concreteKinds = ['class', 'struct', 'union'] as const;
for (const kind of concreteKinds) {
for (const cls of queries.iterateNodesByKind(kind)) {
if ((++scanned255 & 63) === 0) await onYield();
+2 -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' ||
@@ -2191,7 +2192,7 @@ function findExportedSymbolWalk(
/** Node kinds that own static members reachable as `Container.member`. */
const STATIC_MEMBER_CONTAINERS = new Set<Node['kind']>([
'class', 'struct', 'interface', 'enum', 'trait', 'protocol',
'class', 'struct', 'union', 'interface', 'enum', 'trait', 'protocol',
]);
/**
+5 -2
View File
@@ -1079,13 +1079,16 @@ export class ReferenceResolver {
}
// Promote "calls" to "instantiates" when the resolved target is a
// class/struct. Languages without a `new` keyword (Python, Ruby)
// class/struct/union. Languages without a `new` keyword (Python, Ruby)
// express instantiation as `Foo()` — extraction can't tell that
// apart from a function call without symbol info, but resolution
// can: if `Foo` resolves to a class, the call IS an instantiation.
if (kind === 'calls') {
const targetNode = this.queries.getNodeById(ref.targetNodeId);
if (targetNode && (targetNode.kind === 'class' || targetNode.kind === 'struct')) {
if (
targetNode &&
(targetNode.kind === 'class' || targetNode.kind === 'struct' || targetNode.kind === 'union')
) {
kind = 'instantiates';
}
}
+5 -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;
@@ -2107,6 +2107,7 @@ function findBestMatch(
if (
candidate.kind === 'class' ||
candidate.kind === 'struct' ||
candidate.kind === 'union' ||
candidate.kind === 'interface'
) {
score += 25;
+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,
+1
View File
@@ -42,6 +42,7 @@ export const NODE_KINDS = [
'export',
'route',
'component',
'union',
] as const;
export type NodeKind = (typeof NODE_KINDS)[number];