From ededebf01291abf8421989a692d4aa526a5a4fc4 Mon Sep 17 00:00:00 2001 From: Garrett Griffin-Morales Date: Sun, 26 Apr 2026 18:32:07 -0400 Subject: [PATCH] feat(languages): add Zig language provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- gitnexus-shared/src/language-detection.ts | 2 + gitnexus-shared/src/languages.ts | 1 + .../language-classification.ts | 1 + gitnexus/package-lock.json | 27 +++- gitnexus/package.json | 2 + .../ingestion/call-extractors/configs/zig.ts | 8 + .../ingestion/class-extractors/configs/zig.ts | 77 ++++++++++ .../src/core/ingestion/entry-point-scoring.ts | 4 + .../src/core/ingestion/export-detection.ts | 29 ++++ .../core/ingestion/field-extractors/zig.ts | 106 ++++++++++++++ .../src/core/ingestion/framework-detection.ts | 1 + .../ingestion/import-resolvers/configs/zig.ts | 19 +++ .../core/ingestion/import-resolvers/zig.ts | 59 ++++++++ .../src/core/ingestion/languages/index.ts | 2 + gitnexus/src/core/ingestion/languages/zig.ts | 99 +++++++++++++ .../method-extractors/configs/zig.ts | 138 ++++++++++++++++++ .../src/core/ingestion/named-bindings/zig.ts | 36 +++++ .../src/core/ingestion/tree-sitter-queries.ts | 61 ++++++++ .../src/core/ingestion/type-extractors/zig.ts | 61 ++++++++ .../variable-extractors/configs/zig.ts | 69 +++++++++ .../core/ingestion/workers/parse-worker.ts | 2 + .../src/core/tree-sitter/parser-loader.ts | 2 + .../lang-resolution/zig-basic/src/main.zig | 12 ++ .../lang-resolution/zig-basic/src/pioneer.zig | 13 ++ .../test/integration/resolvers/zig.test.ts | 44 ++++++ 25 files changed, 872 insertions(+), 3 deletions(-) create mode 100644 gitnexus/src/core/ingestion/call-extractors/configs/zig.ts create mode 100644 gitnexus/src/core/ingestion/class-extractors/configs/zig.ts create mode 100644 gitnexus/src/core/ingestion/field-extractors/zig.ts create mode 100644 gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts create mode 100644 gitnexus/src/core/ingestion/import-resolvers/zig.ts create mode 100644 gitnexus/src/core/ingestion/languages/zig.ts create mode 100644 gitnexus/src/core/ingestion/method-extractors/configs/zig.ts create mode 100644 gitnexus/src/core/ingestion/named-bindings/zig.ts create mode 100644 gitnexus/src/core/ingestion/type-extractors/zig.ts create mode 100644 gitnexus/src/core/ingestion/variable-extractors/configs/zig.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/zig-basic/src/main.zig create mode 100644 gitnexus/test/fixtures/lang-resolution/zig-basic/src/pioneer.zig create mode 100644 gitnexus/test/integration/resolvers/zig.test.ts diff --git a/gitnexus-shared/src/language-detection.ts b/gitnexus-shared/src/language-detection.ts index d31f9d58d..02046a67f 100644 --- a/gitnexus-shared/src/language-detection.ts +++ b/gitnexus-shared/src/language-detection.ts @@ -42,6 +42,7 @@ const EXTENSION_MAP: Record = { [SupportedLanguages.Swift]: ['.swift'], [SupportedLanguages.Dart]: ['.dart'], [SupportedLanguages.Vue]: ['.vue'], + [SupportedLanguages.Zig]: ['.zig'], [SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'], } satisfies Record; // Ensure exhaustiveness @@ -100,6 +101,7 @@ const SYNTAX_MAP: Record = { [SupportedLanguages.Swift]: 'swift', [SupportedLanguages.Dart]: 'dart', [SupportedLanguages.Vue]: 'typescript', + [SupportedLanguages.Zig]: 'zig', [SupportedLanguages.Cobol]: 'cobol', } satisfies Record; // Ensure exhaustiveness diff --git a/gitnexus-shared/src/languages.ts b/gitnexus-shared/src/languages.ts index 29edb8d1e..26fcbf87b 100644 --- a/gitnexus-shared/src/languages.ts +++ b/gitnexus-shared/src/languages.ts @@ -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', } diff --git a/gitnexus-shared/src/scope-resolution/language-classification.ts b/gitnexus-shared/src/scope-resolution/language-classification.ts index 10c556cda..f86ccc685 100644 --- a/gitnexus-shared/src/scope-resolution/language-classification.ts +++ b/gitnexus-shared/src/scope-resolution/language-classification.ts @@ -40,6 +40,7 @@ export const LanguageClassifications: Readonly; diff --git a/gitnexus/src/core/ingestion/export-detection.ts b/gitnexus/src/core/ingestion/export-detection.ts index 3eb1b4f07..db089a2ac 100644 --- a/gitnexus/src/core/ingestion/export-detection.ts +++ b/gitnexus/src/core/ingestion/export-detection.ts @@ -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 = 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; +}; diff --git a/gitnexus/src/core/ingestion/field-extractors/zig.ts b/gitnexus/src/core/ingestion/field-extractors/zig.ts new file mode 100644 index 000000000..3c2068748 --- /dev/null +++ b/gitnexus/src/core/ingestion/field-extractors/zig.ts @@ -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(); diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 739f22967..8d44b210f 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -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; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts new file mode 100644 index 000000000..448f1d625 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts @@ -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)], +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/zig.ts b/gitnexus/src/core/ingestion/import-resolvers/zig.ts new file mode 100644 index 000000000..29819ff66 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/zig.ts @@ -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 | 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; +} diff --git a/gitnexus/src/core/ingestion/languages/index.ts b/gitnexus/src/core/ingestion/languages/index.ts index 041af775d..772fa652d 100644 --- a/gitnexus/src/core/ingestion/languages/index.ts +++ b/gitnexus/src/core/ingestion/languages/index.ts @@ -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; diff --git a/gitnexus/src/core/ingestion/languages/zig.ts b/gitnexus/src/core/ingestion/languages/zig.ts new file mode 100644 index 000000000..3e6fa1c23 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/zig.ts @@ -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 = 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, +}); diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/zig.ts b/gitnexus/src/core/ingestion/method-extractors/configs/zig.ts new file mode 100644 index 000000000..45c70af38 --- /dev/null +++ b/gitnexus/src/core/ingestion/method-extractors/configs/zig.ts @@ -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 []; + }, +}; diff --git a/gitnexus/src/core/ingestion/named-bindings/zig.ts b/gitnexus/src/core/ingestion/named-bindings/zig.ts new file mode 100644 index 000000000..37b4a15eb --- /dev/null +++ b/gitnexus/src/core/ingestion/named-bindings/zig.ts @@ -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; +} diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 18b163777..fa7a3c32b 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -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 = { @@ -1350,5 +1410,6 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.Swift]: SWIFT_QUERIES, [SupportedLanguages.Dart]: DART_QUERIES, [SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue