test(union): regression cases + kernel-parity torture coverage

Four cases in extraction.test.ts: a Rust union carrying an
`impl Trait for` edge and owning the impl's method; a named C union
alongside a forward declaration that must NOT mint a node; a
`typedef union` taking the typedef name with no `<anonymous>` twin; a
C++ union with a member function.

Verified they fail without the fix on BOTH extraction paths — the wasm
walker via CODEGRAPH_KERNEL=0 and the kernel with a staged build.

torture.c / torture.rs gain the same shapes. The parity gate compares
the two walkers rather than a snapshot, so the fixtures do not detect
the bug on their own — they pin that the fix stays SYMMETRIC. The
regression tests above are what pin that it is present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ctype_lab
2026-08-06 15:47:09 +09:00
co-authored by Claude Opus 5
parent 85e9ac6762
commit 11acc504b5
3 changed files with 133 additions and 0 deletions
+110
View File
@@ -1174,6 +1174,51 @@ impl Counter {
); );
expect(implRefs).toHaveLength(0); 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 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('struct');
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', () => { 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('struct');
// 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('struct');
// 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('struct');
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', () => { describe('Dart mixins and type references', () => {
let tempDir: string; let tempDir: string;
let cg: CodeGraph; let cg: CodeGraph;
@@ -152,3 +152,19 @@ static void ratelimited_warn(void) {
static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5); static DEFINE_RATELIMIT_STATE(ratelimit, 5 * HZ, 5);
use_ptr(&ratelimit, 0); 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]; routes![top_level_h];
pub union Reg {
pub raw: u32,
pub halves: [u16; 2],
}
impl Base for Reg {}