feat: add Zig language support

Adds a Zig LanguageProvider grounded in @tree-sitter-grammars/tree-sitter-zig 1.1.2.
The grammar's published peerOptional `tree-sitter@^0.22.1` is suppressed via an
npm `overrides` entry that aliases the peer to the bundled `tree-sitter@0.21.x`;
load-time smoke testing confirmed ABI compatibility.

v1 capabilities:
  - .zig file detection + Prism syntax mapping
  - Top-level + nested function_declaration as Function/Method
  - struct/enum/union (anonymous in the grammar — owner name resolved from the
    enclosing variable_declaration in class/field/method extractors)
  - container_field as struct/union fields and enum variants
  - top-level const/var as Variable nodes
  - free + member call_expression as @call edges
  - @import("./foo.zig") local-file resolution; std and external packages
    return empty (no ghost edges)
  - pub keyword detection for export checking
  - no heritage hooks (Zig has no inheritance; queries never emit @heritage.*)

Generic extractor changes (backward-compatible):
  - field-extractors/generic.ts and method-extractors/generic.ts: empty
    `bodyNodeTypes` falls back to the type declaration node itself as its own
    body container — needed because Zig's struct_declaration directly contains
    its container_field children. (The `extractOwnerName` hook this commit
    originally introduced now exists upstream; Zig just configures it.)

Out of scope (deferred):
  - usingnamespace, build.zig.zon package graph, comptime/anytype
  - scope-resolution hooks (emitScopeCaptures, interpretImport, …): Zig is
    classified `experimental` and uses the generic fallback resolution path
  - cross-package imports (std, deps)

Tests:
  - new fixture test/fixtures/sample-code/simple.zig
  - Zig describe block in tree-sitter-languages integration test
  - simple.zig added to parsing.test.ts fixture-existence list
  - Zig added to ingestion-utils detection unit test
  - Zig smoke case in parser-loader-abi.test.ts
  - Zig grammar registered in the grammar-literal validation gate
This commit is contained in:
Navid EMAD 2026-05-08 12:02:07 +02:00
parent 94d53eda8e
commit 8c8eec7cf8
No known key found for this signature in database
29 changed files with 642 additions and 4 deletions

View file

@ -580,6 +580,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
| C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ |
| C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Dart | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Zig | ✓ | — | ✓ | — | — | — | — | — | ✓ |
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics

View file

@ -53,6 +53,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
[SupportedLanguages.Dart]: ['.dart'],
[SupportedLanguages.Vue]: ['.vue'],
[SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'],
[SupportedLanguages.Zig]: ['.zig'],
} satisfies Record<SupportedLanguages, readonly string[]>; // Ensure exhaustiveness
/** Pre-built reverse lookup: extension → language (built once at module load). */
@ -121,6 +122,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: 'dart',
[SupportedLanguages.Vue]: 'typescript',
[SupportedLanguages.Cobol]: 'cobol',
[SupportedLanguages.Zig]: 'zig',
} satisfies Record<SupportedLanguages, string>; // Ensure exhaustiveness
/** Non-code file extensions → Prism-compatible syntax identifiers */

View file

@ -22,4 +22,5 @@ export enum SupportedLanguages {
Vue = 'vue',
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
Cobol = 'cobol',
Zig = 'zig',
}

View file

@ -41,6 +41,7 @@ export const LanguageClassifications: Readonly<Record<SupportedLanguages, Langua
[SupportedLanguages.Dart]: 'production',
[SupportedLanguages.Vue]: 'experimental',
[SupportedLanguages.Cobol]: 'experimental',
[SupportedLanguages.Zig]: 'experimental',
};
/** Convenience predicate: is this language gating Ring 4 retirement? */

View file

@ -342,7 +342,7 @@ GitNexus supports indexing multiple repositories. Each `gitnexus analyze` regist
## Supported Languages
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby, Dart
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby, Dart, Zig
### Language Feature Matrix
@ -362,6 +362,7 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift,
| C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ |
| C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Dart | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Zig | ✓ | — | ✓ | — | — | — | — | — | ✓ |
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics
@ -520,7 +521,7 @@ results until you re-run `gitnexus analyze --repair-fts` from a shell where the
### Installation fails with native module errors
Some optional language grammars (Dart, Proto, Swift, Kotlin) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing.
Some optional language grammars (Dart, Proto, Swift, Kotlin) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing. Zig (`@tree-sitter-grammars/tree-sitter-zig`, an npm `optionalDependency`) behaves the same way: if its native binding fails to install, `.zig` files are skipped.
If `npm install -g gitnexus` fails on native modules:

View file

@ -72,6 +72,7 @@
},
"optionalDependencies": {
"@huggingface/transformers": "^4.1.0",
"@tree-sitter-grammars/tree-sitter-zig": "1.1.2",
"onnxruntime-node": "^1.24.0"
}
},
@ -1811,6 +1812,26 @@
"tslib": "^2.8.0"
}
},
"node_modules/@tree-sitter-grammars/tree-sitter-zig": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tree-sitter-grammars/tree-sitter-zig/-/tree-sitter-zig-1.1.2.tgz",
"integrity": "sha512-J0L31HZ2isy3F5zb2g5QWQOv2r/pbruQNL9ADhuQv2pn5BQOzxt80WcEJaYXBeuJ8GHxVT42slpCna8k1c8LOw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"peerDependencies": {
"tree-sitter": "^0.22.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",

View file

@ -95,6 +95,7 @@
},
"optionalDependencies": {
"@huggingface/transformers": "^4.1.0",
"@tree-sitter-grammars/tree-sitter-zig": "1.1.2",
"onnxruntime-node": "^1.24.0"
},
"trustedDependencies": [
@ -124,6 +125,9 @@
"sharp": ">=0.35.0",
"@huggingface/transformers": {
"onnxruntime-node": "$onnxruntime-node"
},
"@tree-sitter-grammars/tree-sitter-zig": {
"tree-sitter": "$tree-sitter"
}
},
"engines": {

View file

@ -0,0 +1,6 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';
export const zigCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Zig,
};

View file

@ -0,0 +1,35 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { ClassExtractionConfig, ClassLikeNodeLabel } from '../../class-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Zig containers (struct/enum/union) are anonymous in the grammar:
*
* const Point = struct { ... };
*
* The binding name is the first identifier child of the parent
* variable_declaration. Walk up one level to find it.
*/
const extractZigContainerName = (node: SyntaxNode): string | undefined => {
const parent = node.parent;
if (!parent || parent.type !== 'variable_declaration') return undefined;
for (let i = 0; i < parent.namedChildCount; i++) {
const child = parent.namedChild(i);
if (child?.type === 'identifier') return child.text;
}
return undefined;
};
const extractZigContainerType = (node: SyntaxNode): ClassLikeNodeLabel | undefined => {
if (node.type === 'struct_declaration') return 'Struct';
if (node.type === 'enum_declaration') return 'Enum';
if (node.type === 'union_declaration') return 'Struct';
return undefined;
};
export const zigClassConfig: ClassExtractionConfig = {
language: SupportedLanguages.Zig,
typeDeclarationNodes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
extractName: extractZigContainerName,
extractType: extractZigContainerType,
};

View file

@ -242,3 +242,34 @@ export const rubyExportChecker: ExportChecker = (_node, _name) => true;
/** Dart: public if no leading underscore (convention, same as Python). */
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');
/** Zig declaration node types whose `pub` keyword child marks the symbol public. */
const ZIG_DECL_TYPES = new Set(['function_declaration', 'variable_declaration']);
/**
* Zig: walk to the enclosing decl, scan its direct children for an unnamed `pub`
* keyword token (tree-sitter-zig models `pub` as an anonymous keyword child of
* function_declaration / variable_declaration). Container fields (struct/enum
* variants) are public if their enclosing variable_declaration is public.
*/
export const zigExportChecker: ExportChecker = (node, _name) => {
let current: SyntaxNode | null = node;
while (current) {
if (ZIG_DECL_TYPES.has(current.type)) {
for (let i = 0; i < current.childCount; i++) {
const child = current.child(i);
if (child?.type === 'pub') return true;
}
// For nested function_declaration (a method inside a struct), keep walking
// up to the outer variable_declaration — it's the binding that carries
// module-level visibility.
if (current.type === 'function_declaration') {
current = current.parent;
continue;
}
return false;
}
current = current.parent;
}
return false;
};

View file

@ -0,0 +1,57 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { FieldExtractionConfig } from '../generic.js';
/**
* Zig containers (struct/enum/union) are anonymous in tree-sitter-zig; the
* binding name is the first identifier child of the parent variable_declaration.
*/
const extractZigOwnerName = (node: SyntaxNode): string | undefined => {
const parent = node.parent;
if (!parent || parent.type !== 'variable_declaration') return undefined;
for (let i = 0; i < parent.namedChildCount; i++) {
const child = parent.namedChild(i);
if (child?.type === 'identifier') return child.text;
}
return undefined;
};
/**
* Container fields appear as direct children of struct_declaration /
* enum_declaration / union_declaration — there is no separate body wrapper
* in this grammar, so `bodyNodeTypes` is empty and the generic factory's
* "iterate immediate children" pass picks them up.
*/
export const zigFieldConfig: FieldExtractionConfig = {
language: SupportedLanguages.Zig,
typeDeclarationNodes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
fieldNodeTypes: ['container_field'],
bodyNodeTypes: [],
defaultVisibility: 'public',
extractOwnerName: extractZigOwnerName,
extractName(node) {
const name = node.childForFieldName('name');
return name?.text;
},
extractType(node) {
const typeNode = node.childForFieldName('type');
return typeNode?.text?.trim();
},
extractVisibility() {
// Zig has no per-field visibility — fields inherit the container's
// module-level visibility. Treat as public; the export checker decides
// what the *container* exposes.
return 'public';
},
isStatic() {
return false;
},
isReadonly() {
return false;
},
};

View file

@ -148,6 +148,13 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
if (result.length === 0 && bodyField) {
result.push(bodyField);
}
// Last resort: when no body wrapper exists (e.g. tree-sitter-zig's
// struct_declaration directly contains its container_field children),
// use the type-declaration node itself as the body. The downstream
// walk filters by `fieldNodeTypes`, so unrelated children are ignored.
if (result.length === 0 && bodyNodeSet.size === 0) {
result.push(node);
}
return result;
}

View file

@ -0,0 +1,34 @@
/**
* Zig import resolution.
*
* v1 scope: only `@import("./foo.zig")`-style local-file imports. Standard
* library and external packages (`@import("std")`, `@import("mod")`) are
* treated as external — return an empty result so they don't produce ghost
* import edges.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
import { resolveStandard } from '../standard.js';
const stripQuotes = (s: string): string => s.replace(/^['"]|['"]$/g, '');
export const zigImportStrategy: ImportResolverStrategy = (rawImportPath, filePath, ctx) => {
const stripped = stripQuotes(rawImportPath);
// Local-file imports always reference a `.zig` path. Anything else
// (`std`, `builtin`, package names) is external — stop the chain.
if (!stripped.endsWith('.zig')) {
return { kind: 'files', files: [] };
}
// Treat as relative to the importing file. tree-sitter-zig captures the
// string with surrounding quotes; resolveStandard handles `./` and `../`.
const relPath = stripped.startsWith('.') ? stripped : './' + stripped;
return resolveStandard(relPath, filePath, ctx, SupportedLanguages.Zig);
};
export const zigImportConfig: ImportResolutionConfig = {
language: SupportedLanguages.Zig,
strategies: [zigImportStrategy],
};

View file

@ -25,6 +25,7 @@ import { swiftProvider } from './swift.js';
import { dartProvider } from './dart.js';
import { vueProvider } from './vue.js';
import { cobolProvider } from './cobol.js';
import { zigProvider } from './zig.js';
export const providers = {
[SupportedLanguages.JavaScript]: javascriptProvider,
@ -43,6 +44,7 @@ export const providers = {
[SupportedLanguages.Dart]: dartProvider,
[SupportedLanguages.Vue]: vueProvider,
[SupportedLanguages.Cobol]: cobolProvider,
[SupportedLanguages.Zig]: zigProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;
/** Get provider by language enum (always succeeds for SupportedLanguages). */

View file

@ -0,0 +1,51 @@
/**
* Zig Language Provider.
*
* Key Zig traits:
* - mroStrategy: default 'first-wins' is irrelevant — Zig has no inheritance,
* and no heritage hooks are provided (Zig queries never produce
* `@heritage.*` captures).
* - exportChecker: walks to the enclosing variable_declaration /
* function_declaration and looks for a `pub` keyword child.
* - importResolver: only resolves local `@import("./foo.zig")` paths;
* `@import("std")` and external packages are deliberately external.
* - namedBindingExtractor: omitted — `const Foo = @import("x").Foo` is a
* const declaration, not import-statement syntax.
* - scope-resolution hooks (emitScopeCaptures, interpretImport, …) are
* omitted — Zig is classified `experimental` and uses the generic
* fallback resolution path.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import { ZIG_QUERIES } from '../tree-sitter-queries.js';
import { zigExportChecker } from '../export-detection.js';
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
import { zigImportConfig } from '../import-resolvers/configs/zig.js';
import { createCallExtractor } from '../call-extractors/generic.js';
import { zigCallConfig } from '../call-extractors/configs/zig.js';
import { createClassExtractor } from '../class-extractors/generic.js';
import { zigClassConfig } from '../class-extractors/configs/zig.js';
import { createFieldExtractor } from '../field-extractors/generic.js';
import { zigFieldConfig } from '../field-extractors/configs/zig.js';
import { createMethodExtractor } from '../method-extractors/generic.js';
import { zigMethodConfig } from '../method-extractors/configs/zig.js';
import { createVariableExtractor } from '../variable-extractors/generic.js';
import { zigVariableConfig } from '../variable-extractors/configs/zig.js';
import { zigTypeConfig } from '../type-extractors/zig.js';
export const zigProvider = defineLanguage({
id: SupportedLanguages.Zig,
extensions: ['.zig'],
entryPointPatterns: [/^main$/],
astFrameworkPatterns: [],
treeSitterQueries: ZIG_QUERIES,
typeConfig: zigTypeConfig,
exportChecker: zigExportChecker,
importResolver: createImportResolver(zigImportConfig),
callExtractor: createCallExtractor(zigCallConfig),
classExtractor: createClassExtractor(zigClassConfig),
fieldExtractor: createFieldExtractor(zigFieldConfig),
methodExtractor: createMethodExtractor(zigMethodConfig),
variableExtractor: createVariableExtractor(zigVariableConfig),
});

View file

@ -0,0 +1,106 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { MethodExtractionConfig, ParameterInfo } from '../../method-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Zig method extraction.
*
* tree-sitter-zig containers (struct/enum/union) are anonymous; the binding
* name lives on the parent variable_declaration. Methods inside a container
* appear as plain `function_declaration` children of the container node.
*
* The first parameter is the receiver iff it is named `self` (convention) —
* unlike Rust, Zig has no dedicated `self_parameter` node type.
*/
const extractZigOwnerName = (node: SyntaxNode): string | undefined => {
const parent = node.parent;
if (!parent || parent.type !== 'variable_declaration') return undefined;
for (let i = 0; i < parent.namedChildCount; i++) {
const child = parent.namedChild(i);
if (child?.type === 'identifier') return child.text;
}
return undefined;
};
const extractZigName = (node: SyntaxNode): string | undefined => {
const nameNode = node.childForFieldName('name');
return nameNode?.text;
};
const extractZigReturnType = (node: SyntaxNode): string | undefined => {
// tree-sitter-zig labels the return type as the `type` field on
// function_declaration (the same field name used for parameter types).
const typeNode = node.childForFieldName('type');
return typeNode?.text?.trim();
};
const extractZigParameters = (node: SyntaxNode): ParameterInfo[] => {
const paramList = node.childForFieldName('parameters');
if (!paramList) return [];
const params: ParameterInfo[] = [];
for (let i = 0; i < paramList.namedChildCount; i++) {
const param = paramList.namedChild(i);
if (!param || param.type !== 'parameter') continue;
const nameNode = param.childForFieldName('name');
const typeNode = param.childForFieldName('type');
params.push({
name: nameNode?.text ?? '?',
type: typeNode?.text?.trim() ?? null,
rawType: typeNode?.text?.trim() ?? null,
isOptional: false,
isVariadic: false,
});
}
return params;
};
const hasPubKeyword = (node: SyntaxNode): boolean => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'pub') return true;
}
return false;
};
const extractZigReceiverType = (node: SyntaxNode): string | undefined => {
const paramList = node.childForFieldName('parameters');
if (!paramList) return undefined;
const first = paramList.namedChild(0);
if (!first || first.type !== 'parameter') return undefined;
const nameNode = first.childForFieldName('name');
if (nameNode?.text !== 'self') return undefined;
const typeNode = first.childForFieldName('type');
return typeNode?.text?.trim();
};
export const zigMethodConfig: MethodExtractionConfig = {
language: SupportedLanguages.Zig,
typeDeclarationNodes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
methodNodeTypes: ['function_declaration'],
bodyNodeTypes: [],
extractOwnerName: extractZigOwnerName,
extractName: extractZigName,
extractReturnType: extractZigReturnType,
extractParameters: extractZigParameters,
extractVisibility: (node) => (hasPubKeyword(node) ? 'public' : 'private'),
extractReceiverType: extractZigReceiverType,
isStatic(node) {
// A Zig "method" is effectively static if its first parameter is not `self`.
const paramList = node.childForFieldName('parameters');
if (!paramList) return true;
const first = paramList.namedChild(0);
if (!first || first.type !== 'parameter') return true;
const nameNode = first.childForFieldName('name');
return nameNode?.text !== 'self';
},
isAbstract() {
return false;
},
isFinal() {
return false;
},
};

View file

@ -166,6 +166,13 @@ function findBodies(node: SyntaxNode, bodyNodeSet: Set<string>): SyntaxNode[] {
result.push(bodyField);
addNestedBodies(bodyField, bodyNodeSet, result);
}
// Last resort: when no body wrapper exists (e.g. tree-sitter-zig's
// struct_declaration directly contains its function_declaration children),
// use the type-declaration node itself as the body. The downstream walk
// filters by `methodNodeTypes`, so unrelated children are ignored.
if (result.length === 0 && bodyNodeSet.size === 0) {
result.push(node);
}
return result;
}

View file

@ -2447,6 +2447,58 @@ export const DART_QUERIES = `
right: (_)) @assignment
`;
// ── Zig ──────────────────────────────────────────────────────────────────────
// Verified against @tree-sitter-grammars/tree-sitter-zig 1.1.2.
// Container declarations (struct/enum/union) are anonymous in the grammar; the
// binding name lives on the parent variable_declaration's first identifier
// child. Heritage queries are intentionally absent — Zig has no inheritance.
export const ZIG_QUERIES = `
; Functions (top-level + methods inside struct/enum/union containers)
(function_declaration
name: (identifier) @name) @definition.function
; Struct: const Foo = struct { ... }
(variable_declaration
(identifier) @name
(struct_declaration)) @definition.struct
; Enum: const Foo = enum { ... }
(variable_declaration
(identifier) @name
(enum_declaration)) @definition.enum
; Union: const Foo = union { ... } (and tagged-union union(enum) { ... })
; Labeled as @definition.struct because CONTAINER_TYPE_TO_LABEL has no Union
; label by default; the CONTAINER_TYPE_TO_LABEL update for union_declaration
; lives in ast-helpers.ts.
(variable_declaration
(identifier) @name
(union_declaration)) @definition.struct
; Container fields (struct fields, enum variants, union variants).
(container_field
name: (identifier) @name) @definition.property
; @import("path") — capture the string argument as @import.source.
; The #eq? predicate restricts the match to the @import builtin (other
; builtins like @sizeOf, @TypeOf, @as are not import statements).
(variable_declaration
(builtin_function
(builtin_identifier) @builtin
(arguments
(string) @import.source))
(#eq? @builtin "@import")) @import
; Free calls: foo(...)
(call_expression
function: (identifier) @call.name) @call
; Member calls: obj.method(...) and namespace.fn(...) (e.g. std.debug.print).
(call_expression
function: (field_expression
member: (identifier) @call.name)) @call
`;
import { SupportedLanguages } from 'gitnexus-shared';
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
@ -2466,4 +2518,5 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: DART_QUERIES,
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
[SupportedLanguages.Zig]: ZIG_QUERIES,
};

View file

@ -0,0 +1,21 @@
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
/**
* Zig type extraction — minimal v1 stub.
*
* Type-flow inference (constructor binding, for-loop element types, pattern
* binding, etc.) is intentionally out of scope for v1. The provider supplies
* an empty `declarationNodeTypes` set and no-op extractors to satisfy the
* `LanguageTypeConfig` contract; the central type-env builder will simply
* record nothing for Zig files. Receiver resolution today operates without
* type-env signal for languages that omit it.
*/
const noopDeclaration: TypeBindingExtractor = () => {};
const noopParameter: ParameterExtractor = () => {};
export const zigTypeConfig: LanguageTypeConfig = {
declarationNodeTypes: new Set(),
extractDeclaration: noopDeclaration,
extractParameter: noopParameter,
};

View file

@ -370,6 +370,8 @@ export const CLASS_CONTAINER_TYPES = new Set([
// Go
'struct_type',
'interface_type',
// Zig
'union_declaration',
]);
/**
@ -438,6 +440,11 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
companion_object: 'Class',
struct_type: 'Struct',
interface_type: 'Interface',
// Zig: tagged and untagged unions are class-like containers; map to Struct
// since CONTAINER_TYPE_TO_LABEL has no dedicated `Union` label and the
// graph schema doesn't model unions distinctly. `struct_declaration` and
// `enum_declaration` are already present (Dart / generic).
union_declaration: 'Struct',
};
/**

View file

@ -0,0 +1,90 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { VariableExtractionConfig, VariableVisibility } from '../../variable-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Zig variable extraction.
*
* tree-sitter-zig uses a single `variable_declaration` node for both `const`
* and `var` bindings. The keyword is exposed as an unnamed token child
* (`type === 'const'` or `type === 'var'`).
*
* Excludes variable_declarations whose value is a struct/enum/union or an
* `@import(...)` builtin — those are handled by the class extractor / import
* pipeline. The exclusion is by-effect rather than by-config: when one of
* those values is present, the surrounding pipeline labels the node as
* Struct/Enum/Import-edge and the variable record is redundant.
*/
const hasPubKeyword = (node: SyntaxNode): boolean => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'pub') return true;
}
return false;
};
const isVarKeyword = (node: SyntaxNode): boolean => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'var') return true;
}
return false;
};
export const zigVariableConfig: VariableExtractionConfig = {
language: SupportedLanguages.Zig,
constNodeTypes: ['variable_declaration'],
staticNodeTypes: [],
variableNodeTypes: [],
extractName(node) {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'identifier') return child.text;
}
return undefined;
},
extractType(node) {
// type annotation appears as a `builtin_type` / `identifier` after the `:`.
// tree-sitter-zig models the annotation as an unnamed-token-prefixed child;
// the simplest portable accessor is childForFieldName('type'), which the
// grammar exposes for parameters and function returns. Variable
// declarations don't expose a `type` field in 1.1.2 — fall back to
// scanning for the first builtin_type child after the binding identifier.
const typeNode = node.childForFieldName('type');
if (typeNode) return typeNode.text?.trim();
let seenIdent = false;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
if (!seenIdent) {
if (child.type === 'identifier') seenIdent = true;
continue;
}
if (child.type === 'builtin_type' || child.type === 'identifier') {
return child.text?.trim();
}
// Non-type expression (initializer) reached — no type annotation.
return undefined;
}
return undefined;
},
extractVisibility(node): VariableVisibility {
return hasPubKeyword(node) ? 'public' : 'private';
},
isConst(node) {
return !isVarKeyword(node);
},
isStatic() {
return false;
},
isMutable(node) {
return isVarKeyword(node);
},
};

View file

@ -1,4 +1,5 @@
import { parentPort, threadId, workerData } from 'node:worker_threads';
import { createRequire } from 'node:module';
import {
boundCallableStartRow,
localIdentity,
@ -81,6 +82,13 @@ let C: TreeSitterLanguage | null = null;
try {
C = requireVendoredGrammar('tree-sitter-c') as TreeSitterLanguage;
} catch {}
// @tree-sitter-grammars/tree-sitter-zig is an optionalDependency — may not be installed
const _require = createRequire(import.meta.url);
let Zig: TreeSitterLanguage | null = null;
try {
Zig = _require('@tree-sitter-grammars/tree-sitter-zig');
} catch {}
import { getLanguageFromFilename } from 'gitnexus-shared';
import {
buildDefinitionPreScan,
@ -548,6 +556,7 @@ const languageMap: Record<string, TreeSitterLanguage> = {
[SupportedLanguages.Vue]: TypeScript.typescript,
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
...(Zig ? { [SupportedLanguages.Zig]: Zig } : {}),
};
/**

View file

@ -177,6 +177,20 @@ const SOURCES: Record<string, GrammarSource> = {
'Likely cause: no prebuilt `.node` for this platform/architecture. ' +
`See ${ISSUES_URL}/2107.`,
},
// Zig grammar declares peerOptional `tree-sitter@^0.22.1` but its native
// binding is ABI-compatible with the bundled `tree-sitter@0.21.x` runtime
// (verified by load-time smoke test). The peer-dep mismatch is suppressed
// via the `overrides` block in package.json. Listed as `optional: true`
// because the package is in `optionalDependencies` — users on platforms
// without a prebuild may not have it.
[SupportedLanguages.Zig]: {
load: () => _require('@tree-sitter-grammars/tree-sitter-zig'),
optional: true,
unavailableNote:
'Zig parsing disabled: `@tree-sitter-grammars/tree-sitter-zig` is an ' +
'optionalDependency and is not installed (or its native binding failed ' +
'to load on this platform).',
},
};
/**

View file

@ -0,0 +1,30 @@
const std = @import("std");
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
fn private_helper() i32 {
return 0;
}
pub const Point = struct {
x: i32,
y: i32,
pub fn init(x: i32, y: i32) Point {
return .{ .x = x, .y = y };
}
pub fn distance(self: Point, other: Point) i32 {
return (self.x - other.x) + (self.y - other.y);
}
};
const Color = enum { red, green, blue };
pub fn main() void {
const p = Point.init(1, 2);
_ = p.distance(Point{ .x = 3, .y = 4 });
_ = add(1, 2);
}

View file

@ -70,6 +70,12 @@ const GRAMMAR_PACKAGES: Partial<Record<SupportedLanguages, { pkg: string; subpat
pkg: 'tree-sitter-typescript',
subpaths: ['typescript/src/node-types.json', 'tsx/src/node-types.json'],
},
// optionalDependency — loadGrammarModel returns null when absent, so the
// gate skips Zig literals ('unavailable', R9) instead of failing.
[SupportedLanguages.Zig]: {
pkg: '@tree-sitter-grammars/tree-sitter-zig',
subpaths: ['src/node-types.json'],
},
};
/** Languages the gate validates (everything with a grammar package). */

View file

@ -882,6 +882,7 @@ describe('parsing', () => {
'simple.c',
'simple.cpp',
'simple.cs',
'simple.zig',
];
for (const fixture of fixtures) {

View file

@ -680,6 +680,35 @@ describe('Tree-sitter multi-language parsing', () => {
});
});
describe('Zig', () => {
it('parses functions, structs, enums, and imports if tree-sitter-zig is available', async () => {
try {
await loadLanguage(SupportedLanguages.Zig);
} catch {
// @tree-sitter-grammars/tree-sitter-zig not installed — skip
return;
}
const content = readFixture('simple.zig');
const provider = getProvider(SupportedLanguages.Zig);
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
const defs = extractDefinitions(matches);
const defTypes = defs.map((d) => d.type);
expect(defTypes).toContain('definition.function');
expect(defTypes).toContain('definition.struct');
expect(defTypes).toContain('definition.enum');
});
it('gracefully handles missing tree-sitter-zig', async () => {
try {
await loadLanguage(SupportedLanguages.Zig);
} catch (e: any) {
expect(e.message).toContain('Unsupported language');
}
});
});
describe('unhappy path', () => {
it('returns null/undefined for unsupported file extensions', () => {
expect(getLanguageFromFilename('archive.xyz')).toBeNull();
@ -891,7 +920,7 @@ describe('Tree-sitter multi-language parsing', () => {
[SupportedLanguages.CSharp, 'simple.cs'],
[SupportedLanguages.Rust, 'simple.rs'],
[SupportedLanguages.PHP, 'simple.php'],
// Dart and Swift are excluded — they are optionalDependencies that may not be installed
// Dart, Swift, and Zig are excluded — they are optionalDependencies that may not be installed
];
for (const [lang, fixture, filePath] of langFixtures) {

View file

@ -141,8 +141,14 @@ describe('getLanguageFromFilename', () => {
});
});
describe('Zig', () => {
it('detects .zig files', () => {
expect(getLanguageFromFilename('main.zig')).toBe(SupportedLanguages.Zig);
});
});
describe('unsupported', () => {
it.each(['.scala', '.r', '.lua', '.zig', '.txt', '.md', '.json', '.yaml'])(
it.each(['.scala', '.r', '.lua', '.txt', '.md', '.json', '.yaml'])(
'returns null for %s files',
(ext) => {
expect(getLanguageFromFilename(`file${ext}`)).toBeNull();

View file

@ -117,6 +117,11 @@ const SMOKE_CASES: Record<string, SmokeCase> = {
snippet: 'fun main() {}\n',
rootType: 'source_file',
},
[SupportedLanguages.Zig]: {
language: SupportedLanguages.Zig,
snippet: 'pub fn main() void {}\n',
rootType: 'source_file',
},
};
describe('parser-loader ABI load-smoke (#1922)', () => {