feat(types): add 'Union' label for tagged-union types

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) <noreply@anthropic.com>
This commit is contained in:
Garrett Griffin-Morales 2026-04-26 18:42:24 -04:00
parent 91f680ec8e
commit cd3e9a91dd
6 changed files with 16 additions and 4 deletions

View file

@ -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;
},

View file

@ -56,6 +56,7 @@ const CLASS_LIKE_LABELS = new Set<ClassLikeNodeLabel>([
'Interface',
'Enum',
'Record',
'Union',
]);
const normalizeQualifiedName = (value: string): string =>

View file

@ -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 {

View file

@ -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

View file

@ -1,5 +1,10 @@
pub const State = enum { idle, working };
pub const Tag = union(enum) {
none,
energy: u32,
};
pub const Pioneer = struct {
energy: u32,

View file

@ -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');