mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
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:
parent
94d53eda8e
commit
8c8eec7cf8
29 changed files with 642 additions and 4 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ export enum SupportedLanguages {
|
|||
Vue = 'vue',
|
||||
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
|
||||
Cobol = 'cobol',
|
||||
Zig = 'zig',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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? */
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
21
gitnexus/package-lock.json
generated
21
gitnexus/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { CallExtractionConfig } from '../../call-types.js';
|
||||
|
||||
export const zigCallConfig: CallExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
};
|
||||
35
gitnexus/src/core/ingestion/class-extractors/configs/zig.ts
Normal file
35
gitnexus/src/core/ingestion/class-extractors/configs/zig.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
57
gitnexus/src/core/ingestion/field-extractors/configs/zig.ts
Normal file
57
gitnexus/src/core/ingestion/field-extractors/configs/zig.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
34
gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts
Normal file
34
gitnexus/src/core/ingestion/import-resolvers/configs/zig.ts
Normal 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],
|
||||
};
|
||||
|
|
@ -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). */
|
||||
|
|
|
|||
51
gitnexus/src/core/ingestion/languages/zig.ts
Normal file
51
gitnexus/src/core/ingestion/languages/zig.ts
Normal 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),
|
||||
});
|
||||
106
gitnexus/src/core/ingestion/method-extractors/configs/zig.ts
Normal file
106
gitnexus/src/core/ingestion/method-extractors/configs/zig.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
21
gitnexus/src/core/ingestion/type-extractors/zig.ts
Normal file
21
gitnexus/src/core/ingestion/type-extractors/zig.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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 } : {}),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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).',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
30
gitnexus/test/fixtures/sample-code/simple.zig
vendored
Normal file
30
gitnexus/test/fixtures/sample-code/simple.zig
vendored
Normal 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);
|
||||
}
|
||||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -882,6 +882,7 @@ describe('parsing', () => {
|
|||
'simple.c',
|
||||
'simple.cpp',
|
||||
'simple.cs',
|
||||
'simple.zig',
|
||||
];
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue