Skip to content

Commit d55962e

Browse files
committed
feat(zig): add Zig 0.16+ language support
Tree-sitter-zig WASM from tree-sitter-wasms, no native build required. Extracts: top-level functions, structs with fields/methods, enums, error sets (as enums), @import statements, test declarations, and function calls. Pub/private visibility is detected from the anonymous `pub` token. Known limitation: comptime-generated types (e.g. `fn Foo(comptime T: type) type { return struct {...}; }`) are not extractable at the AST level and are documented in zig.ts. https://claude.ai/code/session_017AQjdPam3enwct9vFi4G1S
1 parent 7e617d8 commit d55962e

5 files changed

Lines changed: 453 additions & 0 deletions

File tree

__tests__/extraction.test.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ describe('Language Detection', () => {
9494
expect(detectLanguage('main.dart')).toBe('dart');
9595
});
9696

97+
it('should detect Zig files', () => {
98+
expect(detectLanguage('main.zig')).toBe('zig');
99+
expect(detectLanguage('build.zon')).toBe('zig');
100+
});
101+
97102
it('should return unknown for unsupported extensions', () => {
98103
expect(detectLanguage('styles.css')).toBe('unknown');
99104
expect(detectLanguage('data.json')).toBe('unknown');
@@ -122,6 +127,7 @@ describe('Language Support', () => {
122127
expect(languages).toContain('swift');
123128
expect(languages).toContain('kotlin');
124129
expect(languages).toContain('dart');
130+
expect(languages).toContain('zig');
125131
});
126132
});
127133

@@ -3649,3 +3655,202 @@ class Svc {
36493655
expect(decoratedNode?.name).toBe('method');
36503656
});
36513657
});
3658+
3659+
describe('Zig Extraction', () => {
3660+
it('should extract top-level function declarations', () => {
3661+
const code = `
3662+
pub fn add(a: i32, b: i32) i32 {
3663+
return a + b;
3664+
}
3665+
3666+
fn internal(x: u8) void {
3667+
_ = x;
3668+
}
3669+
`;
3670+
const result = extractFromSource('math.zig', code);
3671+
3672+
const pubFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'add');
3673+
expect(pubFn).toBeDefined();
3674+
expect(pubFn?.visibility).toBe('public');
3675+
3676+
const privFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'internal');
3677+
expect(privFn).toBeDefined();
3678+
expect(privFn?.visibility).toBe('private');
3679+
});
3680+
3681+
it('should extract function signatures', () => {
3682+
const code = `
3683+
pub fn add(a: i32, b: i32) i32 {
3684+
return a + b;
3685+
}
3686+
`;
3687+
const result = extractFromSource('math.zig', code);
3688+
const fn = result.nodes.find((n) => n.name === 'add');
3689+
expect(fn?.signature).toContain('(a: i32, b: i32)');
3690+
});
3691+
3692+
it('should extract struct declarations with fields', () => {
3693+
const code = `
3694+
pub const Vec2 = struct {
3695+
x: f32,
3696+
y: f32,
3697+
};
3698+
`;
3699+
const result = extractFromSource('vec.zig', code);
3700+
3701+
const struct = result.nodes.find((n) => n.kind === 'struct');
3702+
expect(struct).toBeDefined();
3703+
expect(struct?.name).toBe('Vec2');
3704+
expect(struct?.visibility).toBe('public');
3705+
3706+
const fields = result.nodes.filter((n) => n.kind === 'field');
3707+
const fieldNames = fields.map((f) => f.name);
3708+
expect(fieldNames).toContain('x');
3709+
expect(fieldNames).toContain('y');
3710+
});
3711+
3712+
it('should extract struct methods', () => {
3713+
const code = `
3714+
pub const Vec2 = struct {
3715+
x: f32,
3716+
y: f32,
3717+
3718+
pub fn length(self: Vec2) f32 {
3719+
return @sqrt(self.x * self.x + self.y * self.y);
3720+
}
3721+
3722+
fn dot(self: Vec2, other: Vec2) f32 {
3723+
return self.x * other.x + self.y * other.y;
3724+
}
3725+
};
3726+
`;
3727+
const result = extractFromSource('vec.zig', code);
3728+
3729+
const pubMethod = result.nodes.find((n) => n.kind === 'method' && n.name === 'length');
3730+
expect(pubMethod).toBeDefined();
3731+
expect(pubMethod?.visibility).toBe('public');
3732+
3733+
const privMethod = result.nodes.find((n) => n.kind === 'method' && n.name === 'dot');
3734+
expect(privMethod).toBeDefined();
3735+
expect(privMethod?.visibility).toBe('private');
3736+
3737+
// Methods should be contained by the struct
3738+
const containsEdges = result.edges.filter((e) => e.kind === 'contains');
3739+
const struct = result.nodes.find((n) => n.kind === 'struct');
3740+
expect(containsEdges.some((e) => e.source === struct?.id && e.target === pubMethod?.id)).toBe(true);
3741+
});
3742+
3743+
it('should extract enum declarations with members', () => {
3744+
const code = `
3745+
pub const Color = enum {
3746+
red,
3747+
green,
3748+
blue,
3749+
};
3750+
`;
3751+
const result = extractFromSource('color.zig', code);
3752+
3753+
const enumNode = result.nodes.find((n) => n.kind === 'enum');
3754+
expect(enumNode).toBeDefined();
3755+
expect(enumNode?.name).toBe('Color');
3756+
3757+
const members = result.nodes.filter((n) => n.kind === 'enum_member');
3758+
const memberNames = members.map((m) => m.name);
3759+
expect(memberNames).toContain('red');
3760+
expect(memberNames).toContain('green');
3761+
expect(memberNames).toContain('blue');
3762+
});
3763+
3764+
it('should extract error sets as enums', () => {
3765+
const code = `
3766+
pub const AppError = error {
3767+
OutOfMemory,
3768+
InvalidInput,
3769+
};
3770+
`;
3771+
const result = extractFromSource('errors.zig', code);
3772+
3773+
const enumNode = result.nodes.find((n) => n.kind === 'enum' && n.name === 'AppError');
3774+
expect(enumNode).toBeDefined();
3775+
3776+
const members = result.nodes.filter((n) => n.kind === 'enum_member');
3777+
const memberNames = members.map((m) => m.name);
3778+
expect(memberNames).toContain('OutOfMemory');
3779+
expect(memberNames).toContain('InvalidInput');
3780+
});
3781+
3782+
it('should extract @import as import node', () => {
3783+
const code = `
3784+
const std = @import("std");
3785+
const math = @import("./math.zig");
3786+
`;
3787+
const result = extractFromSource('main.zig', code);
3788+
3789+
const importStd = result.nodes.find((n) => n.kind === 'import' && n.name === 'std');
3790+
expect(importStd).toBeDefined();
3791+
3792+
const importMath = result.nodes.find((n) => n.kind === 'import' && n.name === 'math');
3793+
expect(importMath).toBeDefined();
3794+
3795+
// Should produce unresolved references for the module paths
3796+
const stdRef = result.unresolvedReferences.find((r) => r.referenceName === 'std');
3797+
expect(stdRef).toBeDefined();
3798+
expect(stdRef?.referenceKind).toBe('imports');
3799+
});
3800+
3801+
it('should extract chained @import as import node', () => {
3802+
const code = `
3803+
const io = @import("std").io;
3804+
`;
3805+
const result = extractFromSource('main.zig', code);
3806+
3807+
const importIo = result.nodes.find((n) => n.kind === 'import' && n.name === 'io');
3808+
expect(importIo).toBeDefined();
3809+
});
3810+
3811+
it('should extract plain constants and variables', () => {
3812+
const code = `
3813+
pub const PI: f64 = 3.14159;
3814+
var global_count: i32 = 0;
3815+
`;
3816+
const result = extractFromSource('consts.zig', code);
3817+
3818+
const pi = result.nodes.find((n) => n.name === 'PI');
3819+
expect(pi).toBeDefined();
3820+
expect(pi?.kind).toBe('constant');
3821+
expect(pi?.visibility).toBe('public');
3822+
3823+
const count = result.nodes.find((n) => n.name === 'global_count');
3824+
expect(count).toBeDefined();
3825+
expect(count?.kind).toBe('variable');
3826+
expect(count?.visibility).toBe('private');
3827+
});
3828+
3829+
it('should extract test declarations as functions', () => {
3830+
const code = `
3831+
test "addition works" {
3832+
const x = 1 + 2;
3833+
_ = x;
3834+
}
3835+
`;
3836+
const result = extractFromSource('math_test.zig', code);
3837+
3838+
const testFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'addition works');
3839+
expect(testFn).toBeDefined();
3840+
});
3841+
3842+
it('should extract function calls', () => {
3843+
const code = `
3844+
fn helper() void {}
3845+
3846+
pub fn main() void {
3847+
helper();
3848+
}
3849+
`;
3850+
const result = extractFromSource('main.zig', code);
3851+
3852+
const callRef = result.unresolvedReferences.find((r) => r.referenceKind === 'calls');
3853+
expect(callRef).toBeDefined();
3854+
expect(callRef?.referenceName).toBe('helper');
3855+
});
3856+
});

src/extraction/grammars.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
3535
dart: 'tree-sitter-dart.wasm',
3636
pascal: 'tree-sitter-pascal.wasm',
3737
scala: 'tree-sitter-scala.wasm',
38+
zig: 'tree-sitter-zig.wasm',
3839
};
3940

4041
/**
@@ -78,6 +79,8 @@ export const EXTENSION_MAP: Record<string, Language> = {
7879
'.fmx': 'pascal',
7980
'.scala': 'scala',
8081
'.sc': 'scala',
82+
'.zig': 'zig',
83+
'.zon': 'zig',
8184
};
8285

8386
/**
@@ -291,6 +294,7 @@ export function getLanguageDisplayName(language: Language): string {
291294
liquid: 'Liquid',
292295
pascal: 'Pascal / Delphi',
293296
scala: 'Scala',
297+
zig: 'Zig',
294298
unknown: 'Unknown',
295299
};
296300
return names[language] || language;

src/extraction/languages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { kotlinExtractor } from './kotlin';
2323
import { dartExtractor } from './dart';
2424
import { pascalExtractor } from './pascal';
2525
import { scalaExtractor } from './scala';
26+
import { zigExtractor } from './zig';
2627

2728
export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
2829
typescript: typescriptExtractor,
@@ -43,4 +44,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
4344
dart: dartExtractor,
4445
pascal: pascalExtractor,
4546
scala: scalaExtractor,
47+
zig: zigExtractor,
4648
};

0 commit comments

Comments
 (0)