mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -42,6 +42,7 @@ export const NODE_KINDS = [
|
||||
'export',
|
||||
'route',
|
||||
'component',
|
||||
'union',
|
||||
] as const;
|
||||
|
||||
export type NodeKind = (typeof NODE_KINDS)[number];
|
||||
|
||||
Reference in New Issue
Block a user