Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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, and the methods from that impl were left pointing at a type the graph did not contain. A `typedef union { … } Name;` in C now carries the typedef's name and remains distinguishable from a struct. Re-index after upgrading to replace the earlier struct-shaped union nodes.

- `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)
- A generated CRUD or protobuf layer no longer crowds out the hand-written code sitting beside it: generated files are now recognized by the `// Code generated by … DO NOT EDIT.` style banner written at the top of the file, not just by a filename that looks generated. Re-index after upgrading to pick up the new detection. (#1500)
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions __tests__/c-fnptr-synthesizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 16 additions & 1 deletion __tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
});
Expand Down Expand Up @@ -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,
Expand Down
126 changes: 126 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
16 changes: 16 additions & 0 deletions __tests__/fixtures/kernel-parity/torture.c
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
7 changes: 7 additions & 0 deletions __tests__/fixtures/kernel-parity/torture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,10 @@ fn mount() {
}

routes![top_level_h];

pub union Reg {
pub raw: u32,
pub halves: [u16; 2],
}

impl Base for Reg {}
75 changes: 75 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,59 @@ 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('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
Expand Down Expand Up @@ -2169,6 +2222,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',
Expand Down
3 changes: 2 additions & 1 deletion codegraph-kernel/src/buffers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.
Expand Down
Loading