feat(languages): add Zig language provider

Wires Zig (.zig) into the ingestion pipeline as a first-class language.
Adds a tree-sitter-zig parser binding, language config, tree-sitter
queries, and dedicated extractor configs (class/method/call/field/type/
variable/import/named-binding).

Highlights of the Zig modeling:
- Container types (struct/enum/union) are bound via `variable_declaration`
  in the Zig grammar; the class extractor walks the RHS to disambiguate
  the label (Struct / Enum / Class — Union handling lands separately).
- @import("./path.zig") relative imports resolve against the importing
  file; stdlib/builtin/root names are intentionally returned as null.
- Bare-name @import("pkg") for build.zig.zon dependencies is left as a
  TODO (resolved in a follow-up commit).
- Top-level const/var declarations whose RHS is a struct/enum/union are
  filtered out of the Const set so they don't double-emit alongside the
  type node.

Validated end-to-end on FuryForged (~9,400 symbols, 700+ Zig functions):
function-level resolution, cross-file impact walks, and @import-driven
module edges all behave correctly.

Includes a fixture + integration test (test/integration/resolvers/
zig.test.ts) covering struct/enum detection, method extraction, and
relative-import resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garrett Griffin-Morales 2026-04-26 18:32:07 -04:00
parent 247b1bd556
commit ededebf012
25 changed files with 872 additions and 3 deletions

View file

@ -42,6 +42,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
[SupportedLanguages.Swift]: ['.swift'],
[SupportedLanguages.Dart]: ['.dart'],
[SupportedLanguages.Vue]: ['.vue'],
[SupportedLanguages.Zig]: ['.zig'],
[SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'],
} satisfies Record<SupportedLanguages, readonly string[]>; // Ensure exhaustiveness
@ -100,6 +101,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
[SupportedLanguages.Swift]: 'swift',
[SupportedLanguages.Dart]: 'dart',
[SupportedLanguages.Vue]: 'typescript',
[SupportedLanguages.Zig]: 'zig',
[SupportedLanguages.Cobol]: 'cobol',
} satisfies Record<SupportedLanguages, string>; // Ensure exhaustiveness

View file

@ -20,6 +20,7 @@ export enum SupportedLanguages {
Swift = 'swift',
Dart = 'dart',
Vue = 'vue',
Zig = 'zig',
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
Cobol = 'cobol',
}

View file

@ -40,6 +40,7 @@ export const LanguageClassifications: Readonly<Record<SupportedLanguages, Langua
[SupportedLanguages.Swift]: 'production',
[SupportedLanguages.Dart]: 'production',
[SupportedLanguages.Vue]: 'experimental',
[SupportedLanguages.Zig]: 'experimental',
[SupportedLanguages.Cobol]: 'experimental',
};

View file

@ -14,6 +14,7 @@
"@ladybugdb/core": "^0.15.2",
"@modelcontextprotocol/sdk": "^1.0.0",
"@scarf/scarf": "^1.4.0",
"@tree-sitter-grammars/tree-sitter-zig": "^1.1.2",
"cli-progress": "^3.12.0",
"commander": "^14.0.3",
"cors": "^2.8.5",
@ -55,6 +56,7 @@
"@types/uuid": "^11.0.0",
"@vitest/coverage-v8": "^4.0.18",
"gitnexus-shared": "file:../gitnexus-shared",
"graphology-types": "^0.24.8",
"tsx": "^4.0.0",
"typescript": "^5.4.5",
"vitest": "^4.0.18"
@ -75,7 +77,7 @@
"version": "1.0.0",
"dev": true,
"devDependencies": {
"typescript": "^6.0.2"
"typescript": "^6.0.3"
}
},
"node_modules/@babel/helper-string-parser": {
@ -1917,6 +1919,25 @@
"dev": true,
"license": "MIT"
},
"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",
"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/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@ -3358,8 +3379,8 @@
"version": "0.24.8",
"resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
"integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
"license": "MIT",
"peer": true
"dev": true,
"license": "MIT"
},
"node_modules/graphology-utils": {
"version": "2.5.2",

View file

@ -55,6 +55,7 @@
"@ladybugdb/core": "^0.15.2",
"@modelcontextprotocol/sdk": "^1.0.0",
"@scarf/scarf": "^1.4.0",
"@tree-sitter-grammars/tree-sitter-zig": "^1.1.2",
"cli-progress": "^3.12.0",
"commander": "^14.0.3",
"cors": "^2.8.5",
@ -101,6 +102,7 @@
"@types/uuid": "^11.0.0",
"@vitest/coverage-v8": "^4.0.18",
"gitnexus-shared": "file:../gitnexus-shared",
"graphology-types": "^0.24.8",
"tsx": "^4.0.0",
"typescript": "^5.4.5",
"vitest": "^4.0.18"

View file

@ -0,0 +1,8 @@
// gitnexus/src/core/ingestion/call-extractors/configs/zig.ts
import { SupportedLanguages } from 'gitnexus-shared';
import type { CallExtractionConfig } from '../../call-types.js';
export const zigCallConfig: CallExtractionConfig = {
language: SupportedLanguages.Zig,
};

View file

@ -0,0 +1,77 @@
// gitnexus/src/core/ingestion/class-extractors/configs/zig.ts
import { SupportedLanguages } from 'gitnexus-shared';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { ClassExtractionConfig, ClassLikeNodeLabel } from '../../class-types.js';
/**
* Find the "type expression" child of a Zig variable_declaration.
* Zig binds anonymous container types to variables:
* const Pioneer = struct { ... };
* const State = enum { ... };
* const Tag = union(enum) { ... };
* The container declaration is a child of the variable_declaration node,
* not a top-level node like Rust's struct_item.
*/
function findContainerTypeChild(node: SyntaxNode): SyntaxNode | null {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
if (
child.type === 'struct_declaration' ||
child.type === 'enum_declaration' ||
child.type === 'union_declaration'
) {
return child;
}
}
return null;
}
/**
* Zig class extraction config.
*
* `variable_declaration` is the type-bearing node when its RHS is a
* struct/enum/union. The variable's `identifier` provides the type name.
*/
export const zigClassConfig: ClassExtractionConfig = {
language: SupportedLanguages.Zig,
// Only variable_declaration is treated as a type declaration — the actual
// shape (Struct/Enum/Class) is decided by extractType.
typeDeclarationNodes: ['variable_declaration'],
ancestorScopeNodeTypes: ['variable_declaration'],
extractName(node: SyntaxNode): string | undefined {
if (node.type !== 'variable_declaration') return undefined;
// Only emit a name if this variable_declaration actually wraps a container type.
if (!findContainerTypeChild(node)) return undefined;
// First named identifier child holds the bound name.
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type === 'identifier') return c.text;
}
return undefined;
},
extractType(node: SyntaxNode): ClassLikeNodeLabel | undefined {
if (node.type !== 'variable_declaration') return undefined;
const container = findContainerTypeChild(node);
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';
return undefined;
},
// For nested containers, the enclosing variable_declaration's bound name
// contributes the scope segment. We extract that identifier here.
extractScopeSegments(scopeNode: SyntaxNode): string[] | undefined {
if (scopeNode.type !== 'variable_declaration') return undefined;
if (!findContainerTypeChild(scopeNode)) return [];
for (let i = 0; i < scopeNode.namedChildCount; i++) {
const c = scopeNode.namedChild(i);
if (c?.type === 'identifier') return [c.text];
}
return [];
},
};

View file

@ -227,6 +227,10 @@ export const ENTRY_POINT_PATTERNS = {
/^mapEventToState$/, // Legacy BLoC pattern
],
[SupportedLanguages.Vue]: [], // Vue uses TypeScript queries — entry points handled via TS patterns
[SupportedLanguages.Zig]: [
/^main$/, // standard executable entry point
/^build$/, // build.zig entry point
],
[SupportedLanguages.Cobol]: [], // Standalone regex processor — no tree-sitter entry points
} satisfies Record<SupportedLanguages, RegExp[]>;

View file

@ -246,3 +246,32 @@ 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: a definition is exported when the enclosing declaration carries the
* `pub` keyword. `pub` is an anonymous (unnamed) child of the declaration
* node function_declaration, variable_declaration, etc.
*/
const ZIG_DECL_TYPES: ReadonlySet<string> = new Set([
'function_declaration',
'variable_declaration',
'struct_declaration',
'enum_declaration',
'union_declaration',
'container_field',
]);
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 && !child.isNamed && child.text === 'pub') return true;
}
return false;
}
current = current.parent;
}
return false;
};

View file

@ -0,0 +1,106 @@
// gitnexus/src/core/ingestion/field-extractors/zig.ts
import { SupportedLanguages } from 'gitnexus-shared';
import { BaseFieldExtractor } from '../field-extractor.js';
import type {
ExtractedFields,
FieldExtractorContext,
FieldInfo,
FieldVisibility,
} from '../field-types.js';
import { extractSimpleTypeName } from '../type-extractors/shared.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
/**
* Zig field extractor.
*
* Zig containers are anonymous values bound to a `variable_declaration`:
* const Pioneer = struct { state: State, energy: u32 };
* const State = enum { idle, working };
*
* The class extractor identifies a variable_declaration as a "type
* declaration" when its RHS is struct_declaration / enum_declaration /
* union_declaration. This field extractor mirrors that decision and
* walks into the container to enumerate its `container_field` members.
*
* Visibility: Zig has no per-field modifier; container fields are part of
* the type's public surface, so we report all as 'public'.
*/
export class ZigFieldExtractor extends BaseFieldExtractor {
language = SupportedLanguages.Zig;
isTypeDeclaration(node: SyntaxNode): boolean {
if (node.type !== 'variable_declaration') return false;
return findContainerChild(node) !== null;
}
protected extractVisibility(_node: SyntaxNode): FieldVisibility {
return 'public';
}
extract(node: SyntaxNode, context: FieldExtractorContext): ExtractedFields | null {
if (!this.isTypeDeclaration(node)) return null;
// Owner name = the bound identifier, not a 'name' field.
let ownerFqn: string | undefined;
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type === 'identifier') {
ownerFqn = c.text;
break;
}
}
if (!ownerFqn) return null;
const container = findContainerChild(node);
if (!container) return null;
const fields: FieldInfo[] = [];
for (let i = 0; i < container.namedChildCount; i++) {
const child = container.namedChild(i);
if (child?.type !== 'container_field') continue;
const field = this.buildField(child, context);
if (field) fields.push(field);
}
return { ownerFqn, fields, nestedTypes: [] };
}
private buildField(node: SyntaxNode, context: FieldExtractorContext): FieldInfo | null {
const nameNode = node.childForFieldName?.('name');
const name = nameNode?.text;
if (!name) return null;
const typeNode = node.childForFieldName?.('type');
const rawType = typeNode
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
: null;
const type = this.normalizeType(rawType);
return {
name,
type,
visibility: 'public',
isStatic: false,
isReadonly: false,
sourceFile: context.filePath,
line: node.startPosition.row + 1,
};
}
}
function findContainerChild(node: SyntaxNode): SyntaxNode | null {
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (
c?.type === 'struct_declaration' ||
c?.type === 'enum_declaration' ||
c?.type === 'union_declaration'
) {
return c;
}
}
return null;
}
export const zigFieldExtractor = new ZigFieldExtractor();

View file

@ -896,6 +896,7 @@ export const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE = {
},
],
[SupportedLanguages.Vue]: [], // Vue uses TypeScript AST framework detection
[SupportedLanguages.Zig]: [], // No mainstream Zig frameworks tracked yet
[SupportedLanguages.Cobol]: [], // Standalone regex processor — no AST framework patterns
} satisfies Record<SupportedLanguages, AstFrameworkPatternConfig[]>;

View file

@ -0,0 +1,19 @@
/**
* Zig import resolution config.
* Per-file @import("...") strings, then standard fallback.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js';
import { createStandardStrategy } from '../standard.js';
import { resolveZigImportInternal } from '../zig.js';
export const zigModuleStrategy: ImportResolverStrategy = (rawImportPath, filePath, ctx) => {
const resolved = resolveZigImportInternal(filePath, rawImportPath, ctx.allFilePaths);
return resolved ? { kind: 'files', files: [resolved] } : null;
};
export const zigImportConfig: ImportResolutionConfig = {
language: SupportedLanguages.Zig,
strategies: [zigModuleStrategy, createStandardStrategy(SupportedLanguages.Zig)],
};

View file

@ -0,0 +1,59 @@
/**
* Zig module import resolution internal helpers.
*
* Zig imports take three shapes:
* const std = @import("std"); stdlib, unresolvable
* const builtin = @import("builtin"); compiler builtin, unresolvable
* const root = @import("root"); user's main module, unresolvable here
* const foo = @import("./foo.zig"); relative path
* const foo = @import("foo.zig"); also relative (Zig treats unprefixed
* paths with a `.zig` extension as
* filesystem-relative to the importer)
* const bar = @import("bar"); package dep declared in build.zig.zon
* (TODO: resolve via build.zig.zon)
*
* Only the relative-path cases are resolved here. Stdlib / builtin / root
* names and unrecognised package names return null so the standard fallback
* can attempt suffix matching.
*/
const ZIG_STDLIB_NAMES = new Set(['std', 'builtin', 'root']);
/** Resolve a Zig @import argument to a file path in the repository.
* Returns null when the import is a stdlib / builtin / root reference,
* a build.zig.zon package dep, or genuinely unresolvable. */
export function resolveZigImportInternal(
currentFile: string,
importPath: string,
allFiles: Set<string>,
): string | null {
// Stdlib / compiler builtin / root — not resolvable from source files alone.
if (ZIG_STDLIB_NAMES.has(importPath)) return null;
// Strip any explicit `.zig` extension for path arithmetic; we re-add it below.
const trimmed = importPath.replace(/\\/g, '/');
// Path-bearing import: resolve relative to the current file's directory.
// Zig allows both "./foo.zig" and "foo.zig" — both are filesystem-relative.
if (trimmed.endsWith('.zig') || trimmed.includes('/')) {
const currentDir = currentFile.split('/').slice(0, -1);
const parts = trimmed.split('/');
for (const part of parts) {
if (part === '' || part === '.') continue;
if (part === '..') {
currentDir.pop();
} else {
currentDir.push(part);
}
}
const candidate = currentDir.join('/');
if (allFiles.has(candidate)) return candidate;
if (allFiles.has(candidate + '.zig')) return candidate + '.zig';
return null;
}
// Bare name without extension or slashes (e.g. @import("bar")).
// TODO: resolve via build.zig.zon package map. For now, return null and
// let the standard suffix matcher try its luck.
return null;
}

View file

@ -24,6 +24,7 @@ import { rubyProvider } from './ruby.js';
import { swiftProvider } from './swift.js';
import { dartProvider } from './dart.js';
import { vueProvider } from './vue.js';
import { zigProvider } from './zig.js';
import { cobolProvider } from './cobol.js';
export const providers = {
@ -42,6 +43,7 @@ export const providers = {
[SupportedLanguages.Swift]: swiftProvider,
[SupportedLanguages.Dart]: dartProvider,
[SupportedLanguages.Vue]: vueProvider,
[SupportedLanguages.Zig]: zigProvider,
[SupportedLanguages.Cobol]: cobolProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;

View file

@ -0,0 +1,99 @@
/**
* Zig Language Provider
*
* Mirrors the Rust provider Zig is the closest analog (systems language,
* per-symbol named imports via `const X = @import("...")`, no inheritance,
* no MRO).
*
* Key Zig traits:
* - importSemantics: 'named' (each `@import` binds to one local name)
* - mroStrategy: 'first-wins' (no inheritance, MRO is irrelevant)
* - namedBindingExtractor: returns the local-name exported-name pair
* for the enclosing variable_declaration of the @import call.
*
* Container types (struct / enum / union) are anonymous values bound to
* a variable_declaration. The class extractor disambiguates these from
* ordinary variable declarations by inspecting the RHS.
*/
import { SupportedLanguages } from 'gitnexus-shared';
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 { zigExportChecker } from '../export-detection.js';
import { zigFieldExtractor } from '../field-extractors/zig.js';
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
import { zigImportConfig } from '../import-resolvers/configs/zig.js';
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
import { defineLanguage } from '../language-provider.js';
import { createMethodExtractor } from '../method-extractors/generic.js';
import { zigMethodConfig } from '../method-extractors/configs/zig.js';
import { extractZigNamedBindings } from '../named-bindings/zig.js';
import { ZIG_QUERIES } from '../tree-sitter-queries.js';
import { typeConfig as zigConfig } from '../type-extractors/zig.js';
import { createVariableExtractor } from '../variable-extractors/generic.js';
import { zigVariableConfig } from '../variable-extractors/configs/zig.js';
// Zig builtins that should never be treated as user-defined call targets.
// All `@`-prefixed names plus a handful of conventionally noisy stdlib helpers.
const BUILT_INS: ReadonlySet<string> = new Set([
// Core builtins (compile-time intrinsics)
'@import',
'@intCast',
'@as',
'@ptrCast',
'@sizeOf',
'@alignOf',
'@TypeOf',
'@typeInfo',
'@typeName',
'@field',
'@hasField',
'@hasDecl',
'@compileError',
'@compileLog',
'@panic',
'@truncate',
'@bitCast',
'@floatCast',
'@floatFromInt',
'@intFromFloat',
'@intFromBool',
'@boolFromInt',
'@enumFromInt',
'@intFromEnum',
'@errorName',
'@embedFile',
'@max',
'@min',
'@memcpy',
'@memset',
'@addWithOverflow',
'@subWithOverflow',
'@mulWithOverflow',
'@shlWithOverflow',
// Common stdlib helpers that would otherwise overwhelm the call graph.
'panic',
'assert',
'print',
'debugPrint',
]);
export const zigProvider = defineLanguage({
id: SupportedLanguages.Zig,
extensions: ['.zig'],
treeSitterQueries: ZIG_QUERIES,
typeConfig: zigConfig,
exportChecker: zigExportChecker,
importResolver: createImportResolver(zigImportConfig),
namedBindingExtractor: extractZigNamedBindings,
// 'first-wins' is the default; Zig has no inheritance so MRO is irrelevant.
callExtractor: createCallExtractor(zigCallConfig),
fieldExtractor: zigFieldExtractor,
methodExtractor: createMethodExtractor(zigMethodConfig),
variableExtractor: createVariableExtractor(zigVariableConfig),
classExtractor: createClassExtractor(zigClassConfig),
heritageExtractor: createHeritageExtractor(SupportedLanguages.Zig),
builtInNames: BUILT_INS,
});

View file

@ -0,0 +1,138 @@
// gitnexus/src/core/ingestion/method-extractors/configs/zig.ts
// Verified against @tree-sitter-grammars/tree-sitter-zig 1.1.2
import { SupportedLanguages } from 'gitnexus-shared';
import type {
MethodExtractionConfig,
MethodVisibility,
ParameterInfo,
} from '../../method-types.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
// Anonymous keyword children of a function_declaration node.
function hasKeywordChild(node: SyntaxNode, keyword: string): boolean {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c && !c.isNamed && c.text === keyword) return true;
}
return false;
}
function extractZigMethodName(node: SyntaxNode): string | undefined {
const nameNode = node.childForFieldName?.('name');
return nameNode?.text;
}
function extractZigReturnType(node: SyntaxNode): string | undefined {
const typeNode = node.childForFieldName?.('type');
if (!typeNode) return undefined;
return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
}
function 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 ? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null) : null,
rawType: typeNode?.text?.trim() ?? null,
isOptional: false,
isVariadic: false,
});
}
return params;
}
function extractZigVisibility(node: SyntaxNode): MethodVisibility {
return hasKeywordChild(node, 'pub') ? 'public' : 'private';
}
/** A Zig "method" is detected as a function_declaration whose first
* parameter is named `self` purely conventional, the language has no
* receiver syntax. We expose the convention via extractReceiverType so
* the call-resolution pipeline can route receiver.method() calls. */
function 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();
}
/**
* Owner resolution for a function_declaration nested inside a container.
* The container is a struct_declaration / enum_declaration / union_declaration,
* which is itself the value child of a variable_declaration. The owner name
* is the bound identifier on that variable_declaration.
*/
function extractZigOwnerName(node: SyntaxNode): string | undefined {
if (
node.type !== 'struct_declaration' &&
node.type !== 'enum_declaration' &&
node.type !== 'union_declaration'
) {
return undefined;
}
const parent = node.parent;
if (parent?.type !== 'variable_declaration') return undefined;
for (let i = 0; i < parent.namedChildCount; i++) {
const c = parent.namedChild(i);
if (c?.type === 'identifier') return c.text;
}
return undefined;
}
// ---------------------------------------------------------------------------
// Zig config
// ---------------------------------------------------------------------------
export const zigMethodConfig: MethodExtractionConfig = {
language: SupportedLanguages.Zig,
// Methods live inside container declarations (struct/enum/union).
typeDeclarationNodes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
methodNodeTypes: ['function_declaration'],
// The container declaration itself is the body — function_declaration nodes
// are direct named children.
bodyNodeTypes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
extractOwnerName: extractZigOwnerName,
extractName: extractZigMethodName,
extractReturnType: extractZigReturnType,
extractParameters: extractZigParameters,
extractVisibility: extractZigVisibility,
isStatic(node: SyntaxNode): boolean {
// Static = no `self` first parameter.
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(_node: SyntaxNode, _ownerNode: SyntaxNode): boolean {
// Zig has no abstract method concept.
return false;
},
isFinal(): boolean {
return false;
},
extractReceiverType: extractZigReceiverType,
extractAnnotations(_node: SyntaxNode): string[] {
return [];
},
};

View file

@ -0,0 +1,36 @@
import type { SyntaxNode } from '../utils/ast-helpers.js';
import type { NamedBinding } from './types.js';
/**
* Zig named-binding extraction.
*
* The capture in tree-sitter-queries.ts pins `@import` as the import node
* (the builtin_function call). The enclosing variable_declaration gives us
* the local name:
*
* const std = @import("std"); local "std", exported = "std"
* const ArrayList = std.ArrayList; field access alias chain (local
* "ArrayList", exported "ArrayList")
*
* We treat the bound variable name as a per-symbol binding this matches
* `importSemantics: 'named'` and ensures cross-file references through
* an aliased identifier resolve to the right module.
*/
export function extractZigNamedBindings(importNode: SyntaxNode): NamedBinding[] | undefined {
// The query captures the builtin_function (@import call). Walk up to the
// enclosing variable_declaration to find the bound local name.
let current: SyntaxNode | null = importNode;
while (current && current.type !== 'variable_declaration') {
current = current.parent;
}
if (!current) return undefined;
// First named identifier child is the bound name.
for (let i = 0; i < current.namedChildCount; i++) {
const c = current.namedChild(i);
if (c?.type === 'identifier') {
return [{ local: c.text, exported: c.text }];
}
}
return undefined;
}

View file

@ -1332,6 +1332,66 @@ export const DART_QUERIES = `
(type_identifier) @heritage.trait))) @heritage
`;
// Zig queries — works with @tree-sitter-grammars/tree-sitter-zig.
// Zig has no top-level struct/enum/union node types; instead these are anonymous
// values bound to a `variable_declaration`. We capture the variable_declaration
// as the type-bearing node and let the class extractor disambiguate.
//
// Top-level const/var queries use a #not-match? predicate to exclude
// declarations whose RHS is a struct/enum/union — those would otherwise
// double-emit alongside @definition.struct/@definition.enum/@definition.class.
export const ZIG_QUERIES = `
; Functions (top-level and FFI)
(source_file
(function_declaration name: (identifier) @name) @definition.function)
; Container types bound to a variable_declaration
; pub const Pioneer = struct { ... };
(variable_declaration
(identifier) @name
(struct_declaration)) @definition.struct
; pub const State = enum { ... };
(variable_declaration
(identifier) @name
(enum_declaration)) @definition.enum
; const Tag = union(enum) { ... }; labelled as Class (no Union NodeLabel)
(variable_declaration
(identifier) @name
(union_declaration)) @definition.class
; Methods inside container bodies
(struct_declaration
(function_declaration name: (identifier) @name) @definition.method)
(enum_declaration
(function_declaration name: (identifier) @name) @definition.method)
(union_declaration
(function_declaration name: (identifier) @name) @definition.method)
; Container fields (struct fields, enum members, union variants)
(struct_declaration
(container_field name: (identifier) @name) @definition.property)
(union_declaration
(container_field name: (identifier) @name) @definition.property)
(enum_declaration
(container_field name: (identifier) @name) @definition.property)
; Imports: @import("path") builtin
(builtin_function
(builtin_identifier) @_b
(arguments (string (string_content) @import.source))
(#eq? @_b "@import")) @import
; Calls
(call_expression function: (identifier) @call.name) @call
(call_expression
function: (field_expression member: (identifier) @call.name)) @call
; Top-level const/var (excluding type declarations)
(source_file
(variable_declaration (identifier) @name) @definition.const
(#not-match? @definition.const "(struct|enum|union)\\\\s*[({]"))
`;
import { SupportedLanguages } from 'gitnexus-shared';
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
@ -1350,5 +1410,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Swift]: SWIFT_QUERIES,
[SupportedLanguages.Dart]: DART_QUERIES,
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
[SupportedLanguages.Zig]: ZIG_QUERIES,
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
};

View file

@ -0,0 +1,61 @@
// gitnexus/src/core/ingestion/type-extractors/zig.ts
//
// Zig type-environment extraction.
//
// Zig is a typed language, but type inference is performed by the
// compiler at compile time and is not always reflected in the surface
// syntax. For call-graph purposes we extract only the high-confidence
// patterns — explicit type annotations and parameter types. Anything
// that requires type inference (e.g., `const x = makeFoo()`) is left
// as an unresolved binding for now.
import type {
LanguageTypeConfig,
ParameterExtractor,
TypeBindingExtractor,
} from './types.js';
import { extractSimpleTypeName } from './shared.js';
import type { SyntaxNode } from '../utils/ast-helpers.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set(['variable_declaration']);
/** Zig: const x: Foo = ... | var x: Foo = ... */
const extractDeclaration: TypeBindingExtractor = (
node: SyntaxNode,
env: Map<string, string>,
): void => {
if (node.type !== 'variable_declaration') return;
// Find the bound identifier (first named identifier child).
let nameText: string | undefined;
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type === 'identifier') {
nameText = c.text;
break;
}
}
if (!nameText) return;
const typeNode = node.childForFieldName?.('type');
if (!typeNode) return;
const typeName = extractSimpleTypeName(typeNode);
if (typeName) env.set(nameText, typeName);
};
/** Zig: parameter → name + type */
const extractParameter: ParameterExtractor = (
node: SyntaxNode,
env: Map<string, string>,
): void => {
if (node.type !== 'parameter') return;
const nameNode = node.childForFieldName?.('name');
const typeNode = node.childForFieldName?.('type');
if (!nameNode || !typeNode) return;
const typeName = extractSimpleTypeName(typeNode);
if (typeName) env.set(nameNode.text, typeName);
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
};

View file

@ -0,0 +1,69 @@
// gitnexus/src/core/ingestion/variable-extractors/configs/zig.ts
import { SupportedLanguages } from 'gitnexus-shared';
import type { VariableExtractionConfig, VariableVisibility } from '../../variable-types.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Zig variable extraction config.
*
* Zig has a single `variable_declaration` node type that covers both
* `const` and `var`. The const/var distinction is an anonymous keyword
* child of the declaration. There is no separate `static` concept at
* the declaration level (statics are achieved via comptime initialization).
*
* Type-bearing declarations whose RHS is struct/enum/union are treated
* as type declarations by the class extractor, so they should not also
* surface as ordinary variables. The query in tree-sitter-queries.ts
* already filters those out with a #not-match? predicate.
*/
function hasKeyword(node: SyntaxNode, keyword: string): boolean {
for (let i = 0; i < node.childCount; i++) {
const c = node.child(i);
if (c && !c.isNamed && c.text === keyword) return true;
}
return false;
}
export const zigVariableConfig: VariableExtractionConfig = {
language: SupportedLanguages.Zig,
// Zig's variable_declaration covers both const and var; the factory uses
// these sets primarily for routing — overlap is fine because isConst() is
// the source of truth at extraction time.
constNodeTypes: ['variable_declaration'],
staticNodeTypes: [],
variableNodeTypes: ['variable_declaration'],
extractName(node: SyntaxNode): string | undefined {
if (node.type !== 'variable_declaration') return undefined;
for (let i = 0; i < node.namedChildCount; i++) {
const c = node.namedChild(i);
if (c?.type === 'identifier') return c.text;
}
return undefined;
},
extractType(node: SyntaxNode): string | undefined {
const typeNode = node.childForFieldName?.('type');
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text?.trim();
return undefined;
},
extractVisibility(node: SyntaxNode): VariableVisibility {
return hasKeyword(node, 'pub') ? 'public' : 'private';
},
isConst(node: SyntaxNode): boolean {
return hasKeyword(node, 'const');
},
isStatic(_node: SyntaxNode): boolean {
return false;
},
isMutable(node: SyntaxNode): boolean {
return hasKeyword(node, 'var');
},
};

View file

@ -10,6 +10,7 @@ import CPP from 'tree-sitter-cpp';
import CSharp from 'tree-sitter-c-sharp/bindings/node/index.js';
import Go from 'tree-sitter-go';
import Rust from 'tree-sitter-rust';
import Zig from '@tree-sitter-grammars/tree-sitter-zig';
import PHP from 'tree-sitter-php';
import Ruby from 'tree-sitter-ruby';
import { createRequire } from 'node:module';
@ -314,6 +315,7 @@ const languageMap: Record<string, TreeSitterLanguage> = {
...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
[SupportedLanguages.PHP]: PHP.php_only,
[SupportedLanguages.Ruby]: Ruby,
[SupportedLanguages.Zig]: Zig,
[SupportedLanguages.Vue]: TypeScript.typescript,
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),

View file

@ -15,6 +15,7 @@ import Go from 'tree-sitter-go';
import Rust from 'tree-sitter-rust';
import PHP from 'tree-sitter-php';
import Ruby from 'tree-sitter-ruby';
import Zig from '@tree-sitter-grammars/tree-sitter-zig';
import { createRequire } from 'node:module';
import { SupportedLanguages } from 'gitnexus-shared';
@ -51,6 +52,7 @@ const languageMap: Record<string, any> = {
...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
[SupportedLanguages.PHP]: PHP.php_only,
[SupportedLanguages.Ruby]: Ruby,
[SupportedLanguages.Zig]: Zig,
[SupportedLanguages.Vue]: TypeScript.typescript,
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),

View file

@ -0,0 +1,12 @@
const std = @import("std");
const pioneer = @import("./pioneer.zig");
pub fn main() void {
var p = pioneer.Pioneer{ .energy = 0 };
p.tick();
helper();
}
fn helper() void {
_ = 1;
}

View file

@ -0,0 +1,13 @@
pub const State = enum { idle, working };
pub const Pioneer = struct {
energy: u32,
pub fn tick(self: *Pioneer) void {
self.energy += 1;
}
pub fn reset(self: *Pioneer) void {
self.energy = 0;
}
};

View file

@ -0,0 +1,44 @@
/**
* Zig: container types, methods, calls, and @import resolution.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES,
getNodesByLabel,
getRelationships,
runPipelineFromRepo,
type PipelineResult,
} from './helpers.js';
describe('Zig basic resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'zig-basic'), () => {});
}, 60000);
it('detects the Pioneer struct and State enum', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('Pioneer');
expect(getNodesByLabel(result, 'Enum')).toContain('State');
});
it('extracts top-level functions from main.zig', () => {
const fns = getNodesByLabel(result, 'Function');
expect(fns).toContain('main');
expect(fns).toContain('helper');
});
it('extracts struct methods (tick, reset) as Methods', () => {
const methods = getNodesByLabel(result, 'Method');
expect(methods).toContain('tick');
expect(methods).toContain('reset');
});
it('resolves the relative @import("./pioneer.zig") to pioneer.zig', () => {
const imports = getRelationships(result, 'IMPORTS');
const internal = imports.filter((e) => e.targetFilePath.endsWith('pioneer.zig'));
expect(internal.length).toBeGreaterThan(0);
expect(internal[0].sourceFilePath).toContain('main.zig');
});
});