From cd3e9a91ddfe38468518d586e8e01c1417bb0558 Mon Sep 17 00:00:00 2001 From: Garrett Griffin-Morales Date: Sun, 26 Apr 2026 18:42:24 -0400 Subject: [PATCH] feat(types): add 'Union' label for tagged-union types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote tagged unions to a first-class graph label rather than folding them into 'Class'. The `'Union'` member already existed in `NodeLabel`; this change adds it to `ClassLikeNodeLabel` so type extractors can return it directly, and updates the Zig pipeline end-to-end: - `ClassLikeNodeLabel` gains `'Union'`. - `CLASS_LIKE_LABELS` (the runtime guard set used by the generic class extractor) gains `'Union'`. - The Zig class extractor's `extractType` now returns `'Union'` for `union_declaration` instead of `'Class'`. - The Zig tree-sitter query tags `union(enum)` capture as `@definition.union` instead of `@definition.class`. The `ast-helpers.ts` capture→label map already mapped this to `'Union'`, so no extractor-side changes were needed. Cross-language note: this lifts a hard-coded TS limitation that was also relevant to C/C++ (their queries already emit `@definition.union` and so will benefit transparently in cases where the generic extractor's CLASS_LIKE_LABELS check gated the label through). No grammar or query changes for those languages here — their existing unions just stop being filtered out by the ClassLikeNodeLabel narrowing. Tests: - Zig fixture gains a `union(enum)` declaration (`Tag`). - Integration test asserts `Tag` appears as `Union` and NOT as `Class` (negative-side guard against double-emission). - Full rust + cpp test suites pass unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- gitnexus/src/core/ingestion/class-extractors/configs/zig.ts | 2 +- gitnexus/src/core/ingestion/class-extractors/generic.ts | 1 + gitnexus/src/core/ingestion/class-types.ts | 2 +- gitnexus/src/core/ingestion/tree-sitter-queries.ts | 4 ++-- .../test/fixtures/lang-resolution/zig-basic/src/pioneer.zig | 5 +++++ gitnexus/test/integration/resolvers/zig.test.ts | 6 ++++++ 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/gitnexus/src/core/ingestion/class-extractors/configs/zig.ts b/gitnexus/src/core/ingestion/class-extractors/configs/zig.ts index 1d2c5f890..82c833ac6 100644 --- a/gitnexus/src/core/ingestion/class-extractors/configs/zig.ts +++ b/gitnexus/src/core/ingestion/class-extractors/configs/zig.ts @@ -59,7 +59,7 @@ export const zigClassConfig: ClassExtractionConfig = { if (!container) return undefined; if (container.type === 'struct_declaration') return 'Struct'; if (container.type === 'enum_declaration') return 'Enum'; - if (container.type === 'union_declaration') return 'Class'; + if (container.type === 'union_declaration') return 'Union'; return undefined; }, diff --git a/gitnexus/src/core/ingestion/class-extractors/generic.ts b/gitnexus/src/core/ingestion/class-extractors/generic.ts index 303eb80c0..df30f1329 100644 --- a/gitnexus/src/core/ingestion/class-extractors/generic.ts +++ b/gitnexus/src/core/ingestion/class-extractors/generic.ts @@ -56,6 +56,7 @@ const CLASS_LIKE_LABELS = new Set([ 'Interface', 'Enum', 'Record', + 'Union', ]); const normalizeQualifiedName = (value: string): string => diff --git a/gitnexus/src/core/ingestion/class-types.ts b/gitnexus/src/core/ingestion/class-types.ts index 858d4c2eb..a45999f79 100644 --- a/gitnexus/src/core/ingestion/class-types.ts +++ b/gitnexus/src/core/ingestion/class-types.ts @@ -3,7 +3,7 @@ import type { SyntaxNode } from './utils/ast-helpers.js'; export type ClassLikeNodeLabel = Extract< NodeLabel, - 'Class' | 'Struct' | 'Interface' | 'Enum' | 'Record' + 'Class' | 'Struct' | 'Interface' | 'Enum' | 'Record' | 'Union' >; export interface ExtractedClassSymbol { diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index fa7a3c32b..6cf96dd93 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -1354,10 +1354,10 @@ export const ZIG_QUERIES = ` (variable_declaration (identifier) @name (enum_declaration)) @definition.enum -; const Tag = union(enum) { ... }; — labelled as Class (no Union NodeLabel) +; const Tag = union(enum) { ... }; (variable_declaration (identifier) @name - (union_declaration)) @definition.class + (union_declaration)) @definition.union ; ── Methods inside container bodies ────────────────────────────────────────── (struct_declaration diff --git a/gitnexus/test/fixtures/lang-resolution/zig-basic/src/pioneer.zig b/gitnexus/test/fixtures/lang-resolution/zig-basic/src/pioneer.zig index 4269bed84..2b6674c41 100644 --- a/gitnexus/test/fixtures/lang-resolution/zig-basic/src/pioneer.zig +++ b/gitnexus/test/fixtures/lang-resolution/zig-basic/src/pioneer.zig @@ -1,5 +1,10 @@ pub const State = enum { idle, working }; +pub const Tag = union(enum) { + none, + energy: u32, +}; + pub const Pioneer = struct { energy: u32, diff --git a/gitnexus/test/integration/resolvers/zig.test.ts b/gitnexus/test/integration/resolvers/zig.test.ts index d93451af0..0c875a2b7 100644 --- a/gitnexus/test/integration/resolvers/zig.test.ts +++ b/gitnexus/test/integration/resolvers/zig.test.ts @@ -23,6 +23,12 @@ describe('Zig basic resolution', () => { expect(getNodesByLabel(result, 'Enum')).toContain('State'); }); + it('labels `union(enum)` declarations as Union (not Class)', () => { + expect(getNodesByLabel(result, 'Union')).toContain('Tag'); + // Negative-side check: Tag must NOT also appear under Class. + expect(getNodesByLabel(result, 'Class')).not.toContain('Tag'); + }); + it('extracts top-level functions from main.zig', () => { const fns = getNodesByLabel(result, 'Function'); expect(fns).toContain('main');