From 11acc504b53ff7a12e6a31eaeef2fce0981918a9 Mon Sep 17 00:00:00 2001 From: ctype_lab Date: Thu, 6 Aug 2026 15:47:09 +0900 Subject: [PATCH] test(union): regression cases + kernel-parity torture coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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 --- __tests__/extraction.test.ts | 110 ++++++++++++++++++++ __tests__/fixtures/kernel-parity/torture.c | 16 +++ __tests__/fixtures/kernel-parity/torture.rs | 7 ++ 3 files changed, 133 insertions(+) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 7840239..d8a4315 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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 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', () => { @@ -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 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 === '')).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', () => { let tempDir: string; let cg: CodeGraph; diff --git a/__tests__/fixtures/kernel-parity/torture.c b/__tests__/fixtures/kernel-parity/torture.c index 4072637..57e04a9 100644 --- a/__tests__/fixtures/kernel-parity/torture.c +++ b/__tests__/fixtures/kernel-parity/torture.c @@ -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; } diff --git a/__tests__/fixtures/kernel-parity/torture.rs b/__tests__/fixtures/kernel-parity/torture.rs index 0f7e80c..1e14b7b 100644 --- a/__tests__/fixtures/kernel-parity/torture.rs +++ b/__tests__/fixtures/kernel-parity/torture.rs @@ -209,3 +209,10 @@ fn mount() { } routes![top_level_h]; + +pub union Reg { + pub raw: u32, + pub halves: [u16; 2], +} + +impl Base for Reg {}