Просмотр исходного кода

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>
ctype_lab 1 месяц назад
Родитель
Сommit
11acc504b5

+ 110 - 0
__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 <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', () => {
   let tempDir: string;
   let cg: CodeGraph;

+ 16 - 0
__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; }

+ 7 - 0
__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 {}