Skip to content
Merged
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 @@ -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)
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 {}
Loading