From 81eccec36e1a0e46cd7be0f95099fc4d92e66f97 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Fri, 12 Jun 2026 19:42:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(zig):=20scope-resolution=20hooks=20?= =?UTF-8?q?=E2=80=94=20IMPORTS=20and=20CALLS=20edges=20(Ring=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the registry-primary scope-resolution path for Zig, the prerequisite for cross-file edges since the legacy DAG removal. Adds the standard per-language stack under languages/zig/: - query.ts: scope query (containers as Class scopes, blocks, functions), declarations (container anchors placed on the container node itself so the def lands in its own Class scope and the name binding auto-hoists to the parent — populateClassOwnedMembers needs the class-like def among the class scope's ownedDefs), @import statements (#eq?-gated builtin), parameter/constructor type bindings, and call/constructor reference sites. The grammar is required lazily (optionalDependency). - captures.ts: emitZigScopeCaptures — groups query matches, drops the plain-variable group for container/import bindings (their dedicated rules bind the name), and relabels container-nested fns @declaration.function → @declaration.method (labelOverride parity). - interpret.ts: namespace-kind imports (const x = @import("…")) and type bindings — self-parameter convention marks the receiver, Zig sigils (*, ?, [], error unions, const) stripped from type names while dotted qualifiers (mod.T) are preserved for Case-3 namespace-prefix receiver dispatch. - simple-hooks.ts: parameter bindings stay function-local (Go rationale), local-over-import merge precedence, bounds-check arity (always 'unknown' today — no synthesized arity metadata). - scope-resolver.ts: emit-side wiring; build.zig.zon threads through loadResolutionConfig into the same resolveZigImportInternal the legacy resolver config wraps. fieldFallbackOnMethodLookup off (statically typed). Registered in SCOPE_RESOLVERS. The resolvers integration test un-skips the import-edge case and gains CALLS assertions: free call (main → helper) and receiver-bound method dispatch through a namespace-qualified constructor (var p = pioneer.Pioneer{…}; p.tick() → main → tick). --- .../core/ingestion/import-resolvers/zig.ts | 7 +- gitnexus/src/core/ingestion/languages/zig.ts | 28 +++- .../core/ingestion/languages/zig/captures.ts | 88 ++++++++++++ .../src/core/ingestion/languages/zig/index.ts | 8 ++ .../core/ingestion/languages/zig/interpret.ts | 55 ++++++++ .../src/core/ingestion/languages/zig/query.ts | 126 ++++++++++++++++++ .../ingestion/languages/zig/scope-resolver.ts | 50 +++++++ .../ingestion/languages/zig/simple-hooks.ts | 77 +++++++++++ .../scope-resolution/pipeline/registry.ts | 2 + .../test/integration/resolvers/zig.test.ts | 22 +-- 10 files changed, 447 insertions(+), 16 deletions(-) create mode 100644 gitnexus/src/core/ingestion/languages/zig/captures.ts create mode 100644 gitnexus/src/core/ingestion/languages/zig/index.ts create mode 100644 gitnexus/src/core/ingestion/languages/zig/interpret.ts create mode 100644 gitnexus/src/core/ingestion/languages/zig/query.ts create mode 100644 gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts create mode 100644 gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts diff --git a/gitnexus/src/core/ingestion/import-resolvers/zig.ts b/gitnexus/src/core/ingestion/import-resolvers/zig.ts index aa5dc139c..9c1200be3 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/zig.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/zig.ts @@ -31,7 +31,7 @@ const ZIG_STDLIB_NAMES = new Set(['std', 'builtin', 'root']); export function resolveZigImportInternal( currentFile: string, importPath: string, - allFiles: Set, + allFiles: ReadonlySet, buildZon?: ZigBuildZonConfig | null, ): string | null { // Stdlib / compiler builtin / root — not resolvable from source files alone. @@ -68,10 +68,7 @@ export function resolveZigImportInternal( if (normalized !== null) { // Conventional Zig layout: /src/.zig (matches the // package's primary module name) or /src/main.zig. - const candidates = [ - `${normalized}/src/${importPath}.zig`, - `${normalized}/src/main.zig`, - ]; + const candidates = [`${normalized}/src/${importPath}.zig`, `${normalized}/src/main.zig`]; for (const c of candidates) { if (allFiles.has(c)) return c; } diff --git a/gitnexus/src/core/ingestion/languages/zig.ts b/gitnexus/src/core/ingestion/languages/zig.ts index 1b60620fc..3a15ccf4d 100644 --- a/gitnexus/src/core/ingestion/languages/zig.ts +++ b/gitnexus/src/core/ingestion/languages/zig.ts @@ -11,9 +11,13 @@ * `@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. + * - scope-resolution hooks (Ring 3): `emitScopeCaptures` walks the file via + * `zig/query.ts` (containers as Class scopes, container-nested fns + * relabeled @declaration.method, plain-variable groups filtered for + * container/import bindings); `interpretImport` maps + * `const x = @import("…")` to a namespace import; receiver dispatch + * rides the `self`-parameter convention. The emit-side wiring lives in + * `zig/scope-resolver.ts` (SCOPE_RESOLVERS registry). */ import { SupportedLanguages } from 'gitnexus-shared'; @@ -34,6 +38,14 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { zigVariableConfig } from '../variable-extractors/configs/zig.js'; import { zigTypeConfig } from '../type-extractors/zig.js'; import type { SyntaxNode } from '../utils/ast-helpers.js'; +import { + emitZigScopeCaptures, + interpretZigImport, + interpretZigTypeBinding, + zigArityCompatibility, + zigBindingScopeFor, + zigReceiverBinding, +} from './zig/index.js'; const ZIG_CONTAINER_TYPES = new Set(['struct_declaration', 'enum_declaration', 'union_declaration']); @@ -75,4 +87,14 @@ export const zigProvider = defineLanguage({ if (isZigContainerMethod(functionNode)) return 'Method'; return defaultLabel; }, + + // ── RFC #909 Ring 3: scope-based resolution hooks ── + emitScopeCaptures: emitZigScopeCaptures, + interpretImport: interpretZigImport, + interpretTypeBinding: interpretZigTypeBinding, + bindingScopeFor: zigBindingScopeFor, + receiverBinding: zigReceiverBinding, + // Provider contract is (def, callsite); the ScopeResolver contract is + // (callsite, def) — same function, adapted argument order. + arityCompatibility: (def, callsite) => zigArityCompatibility(callsite, def), }); diff --git a/gitnexus/src/core/ingestion/languages/zig/captures.ts b/gitnexus/src/core/ingestion/languages/zig/captures.ts new file mode 100644 index 000000000..70e096e22 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/zig/captures.ts @@ -0,0 +1,88 @@ +import type { Capture, CaptureMatch } from 'gitnexus-shared'; +import { nodeToCapture, type SyntaxNode } from '../../utils/ast-helpers.js'; +import { getZigParser, getZigScopeQuery } from './query.js'; +import { getTreeSitterBufferSize } from '../../constants.js'; +import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; + +const ZIG_CONTAINER_TYPES = new Set([ + 'struct_declaration', + 'enum_declaration', + 'union_declaration', +]); + +/** Is this variable_declaration a container binding (`const T = struct {…}`) + * or an import binding (`const x = @import("…")`)? Those groups are emitted + * by their dedicated query rules; the plain @declaration.variable match for + * the same node must be dropped so the name binds exactly once. */ +function isContainerOrImportBinding(declNode: SyntaxNode): boolean { + for (let i = 0; i < declNode.namedChildCount; i++) { + const child = declNode.namedChild(i); + if (child === null) continue; + if (ZIG_CONTAINER_TYPES.has(child.type)) return true; + if (child.type === 'builtin_function') { + const builtin = child.namedChild(0); + if (builtin?.type === 'builtin_identifier' && builtin.text === '@import') return true; + } + } + return false; +} + +/** A `fn` nested in a struct/enum/union container is a method — mirror the + * provider's `labelOverride` so scope-side defs carry the same label the + * worker gives the graph node. */ +function isContainerMethod(fnNode: SyntaxNode): boolean { + let ancestor = fnNode.parent; + while (ancestor) { + if (ZIG_CONTAINER_TYPES.has(ancestor.type)) return true; + ancestor = ancestor.parent; + } + return false; +} + +export function emitZigScopeCaptures( + sourceText: string, + _filePath: string, + cachedTree?: unknown, +): readonly CaptureMatch[] { + let tree = cachedTree as ReturnType['parse']> | undefined; + if (tree === undefined) { + tree = parseSourceSafe(getZigParser(), sourceText, undefined, { + bufferSize: getTreeSitterBufferSize(sourceText), + }); + } + + const rawMatches = getZigScopeQuery().matches(tree.rootNode); + const out: CaptureMatch[] = []; + + for (const m of rawMatches) { + const grouped: Record = {}; + const nodeMap: Record = {}; + for (const c of m.captures) { + const tag = '@' + c.name; + if (tag.startsWith('@_')) continue; // skip anonymous predicate captures + grouped[tag] = nodeToCapture(tag, c.node); + nodeMap[tag] = c.node; + } + if (Object.keys(grouped).length === 0) continue; + + // Drop the plain-variable group for container/import bindings — their + // dedicated rules already bind the name (as Struct/Enum/Union or import). + const variableAnchor = nodeMap['@declaration.variable']; + if (variableAnchor !== undefined && isContainerOrImportBinding(variableAnchor)) { + continue; + } + + // Relabel container-nested fns Function → Method (provider labelOverride + // parity). The anchor capture name carries the kind, so rebuild it. + const fnAnchor = nodeMap['@declaration.function']; + if (fnAnchor !== undefined && isContainerMethod(fnAnchor)) { + const fnCapture = grouped['@declaration.function']!; + delete grouped['@declaration.function']; + grouped['@declaration.method'] = { ...fnCapture, name: '@declaration.method' }; + } + + out.push(grouped); + } + + return out; +} diff --git a/gitnexus/src/core/ingestion/languages/zig/index.ts b/gitnexus/src/core/ingestion/languages/zig/index.ts new file mode 100644 index 000000000..9049ffc2a --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/zig/index.ts @@ -0,0 +1,8 @@ +export { emitZigScopeCaptures } from './captures.js'; +export { interpretZigImport, interpretZigTypeBinding, normalizeZigTypeName } from './interpret.js'; +export { + zigArityCompatibility, + zigBindingScopeFor, + zigMergeBindings, + zigReceiverBinding, +} from './simple-hooks.js'; diff --git a/gitnexus/src/core/ingestion/languages/zig/interpret.ts b/gitnexus/src/core/ingestion/languages/zig/interpret.ts new file mode 100644 index 000000000..db3e192ff --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/zig/interpret.ts @@ -0,0 +1,55 @@ +import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared'; + +const stripQuotes = (s: string): string => s.replace(/^["']|["']$/g, ''); + +/** + * `const std = @import("std");` binds the imported module to a const handle + * accessed via qualified syntax — a namespace import (closest peers: Python + * `import numpy`, Go `import "pkg/bar"`). The local name and imported name + * are always the same identifier; Zig has no rename syntax at the import + * site (renames are ordinary const aliases handled as variable bindings). + */ +export function interpretZigImport(captures: CaptureMatch): ParsedImport | null { + const name = captures['@import.name']?.text; + const source = captures['@import.source']?.text; + if (name === undefined || source === undefined) return null; + const targetRaw = stripQuotes(source); + if (targetRaw.length === 0) return null; + return { kind: 'namespace', localName: name, importedName: name, targetRaw }; +} + +/** + * Strip Zig type sigils that wrap the nominal type: pointers (`*T`, `[*]T`), + * optionals (`?T`), error unions (`!T` / `E!T`), slices (`[]T`), arrays + * (`[N]T`), and `const` qualifiers. Keeps the bare type name so registry + * lookup matches the container declaration. + */ +export function normalizeZigTypeName(text: string): string { + let t = text.trim(); + let previous: string; + do { + previous = t; + t = t.replace(/^(\*|\?|\[\*?c?\]|\[[^\]]*\])\s*/, ''); + t = t.replace(/^const\s+/, ''); + } while (t !== previous); + const bang = t.lastIndexOf('!'); + if (bang !== -1) t = t.slice(bang + 1).trim(); + return t; +} + +export function interpretZigTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null { + const name = captures['@type-binding.name']?.text; + const type = captures['@type-binding.type']?.text; + if (name === undefined || type === undefined) return null; + + let source: TypeRef['source'] = 'annotation'; + if (captures['@type-binding.parameter'] !== undefined) { + // Zig has no implicit receiver keyword; the convention is a first + // parameter named `self`. Mark it so `receiverBinding` finds it. + source = name === 'self' ? 'self' : 'parameter-annotation'; + } else if (captures['@type-binding.constructor'] !== undefined) { + source = 'constructor-inferred'; + } + + return { boundName: name, rawTypeName: normalizeZigTypeName(type), source }; +} diff --git a/gitnexus/src/core/ingestion/languages/zig/query.ts b/gitnexus/src/core/ingestion/languages/zig/query.ts new file mode 100644 index 000000000..fb16a3432 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/zig/query.ts @@ -0,0 +1,126 @@ +import Parser from 'tree-sitter'; +import { createRequire } from 'node:module'; + +const _require = createRequire(import.meta.url); + +/** + * Zig scope-resolution query (RFC #909 Ring 3). + * + * The grammar is an optionalDependency (`@tree-sitter-grammars/tree-sitter-zig`), + * so the language module is required lazily and `getZigParser` / + * `getZigScopeQuery` throw only when actually invoked without the grammar + * installed. That is safe: the parse pipeline filters `.zig` files through + * `parser-loader.isLanguageAvailable` before any scope extraction runs. + * + * Zig specifics encoded here: + * - Containers (struct/enum/union) are anonymous nodes bound by the + * enclosing `variable_declaration`; declarations capture the binding + * identifier from the wrapper. + * - `@import` is a builtin call, not import-statement syntax; the + * `#eq?` predicate keeps other builtins (@sizeOf, @as, …) out. + * - A plain `(variable_declaration (identifier))` rule would also match + * container and import bindings — `emitZigScopeCaptures` filters those + * groups out so a name binds exactly once. + */ +const ZIG_SCOPE_QUERY = ` +;; Scopes +(source_file) @scope.module +(struct_declaration) @scope.class +(enum_declaration) @scope.class +(union_declaration) @scope.class +(function_declaration) @scope.function +(block) @scope.block + +;; Declarations — functions (relabeled @declaration.method inside containers +;; by emitZigScopeCaptures, mirroring the provider's labelOverride) +(function_declaration + name: (identifier) @declaration.name) @declaration.function + +;; Declarations — containers. The binding name lives on the wrapper +;; variable_declaration, but the ANCHOR is the container node itself so its +;; range equals the @scope.class range: the extractor then attaches the def +;; to the class scope (walkers.populateClassOwnedMembers expects the +;; class-like def among the class scope's ownedDefs) and auto-hoists the +;; name binding to the parent scope. +(variable_declaration + (identifier) @declaration.name + (struct_declaration) @declaration.struct) +(variable_declaration + (identifier) @declaration.name + (enum_declaration) @declaration.enum) +(variable_declaration + (identifier) @declaration.name + (union_declaration) @declaration.union) + +;; Declarations — container fields (struct fields, enum/union variants) +(container_field + name: (identifier) @declaration.name) @declaration.field + +;; Declarations — const/var bindings (import/container groups filtered in TS) +(variable_declaration + (identifier) @declaration.name) @declaration.variable + +;; Imports — const x = @import("...") +(variable_declaration + (identifier) @import.name + (builtin_function + (builtin_identifier) @_builtin + (arguments (string) @import.source)) + (#eq? @_builtin "@import")) @import.statement + +;; Type bindings — parameter annotations (incl. self: *T receivers) +(parameter + name: (identifier) @type-binding.name + type: (_) @type-binding.type) @type-binding.parameter + +;; Type bindings — constructor inference: const p = T{ ... } +(variable_declaration + (identifier) @type-binding.name + (struct_initializer + (identifier) @type-binding.type)) @type-binding.constructor + +;; Type bindings — qualified constructor: const p = mod.T{ ... }. The whole +;; field_expression is captured so the dotted text "mod.T" survives — +;; receiver dispatch resolves the namespace prefix through the import +;; binding (emitReceiverBoundCalls Case 3). +(variable_declaration + (identifier) @type-binding.name + (struct_initializer + (field_expression) @type-binding.type)) @type-binding.constructor + +;; References — free calls: foo(...) +(call_expression + function: (identifier) @reference.name) @reference.call.free + +;; References — member calls: obj.method(...) / mod.fn(...) +(call_expression + function: (field_expression + object: (_) @reference.receiver + member: (identifier) @reference.name)) @reference.call.member + +;; References — constructor uses: T{ ... } +(struct_initializer + (identifier) @reference.name) @reference.call.constructor +`; + +let _parser: Parser | null = null; +let _query: Parser.Query | null = null; + +function getZigLanguage(): Parameters[0] { + return _require('@tree-sitter-grammars/tree-sitter-zig'); +} + +export function getZigParser(): Parser { + if (_parser === null) { + _parser = new Parser(); + _parser.setLanguage(getZigLanguage()); + } + return _parser; +} + +export function getZigScopeQuery(): Parser.Query { + if (_query === null) { + _query = new Parser.Query(getZigLanguage(), ZIG_SCOPE_QUERY); + } + return _query; +} diff --git a/gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts new file mode 100644 index 000000000..780a4fafa --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts @@ -0,0 +1,50 @@ +/** + * Zig `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by the + * generic `runScopeResolution` orchestrator. + * + * Thin wiring: Zig has no inheritance (default MRO linearization over an + * empty heritage set), no `super`, and is statically typed (field-fallback + * heuristic off per the contract guidance). Import resolution reuses the + * same `resolveZigImportInternal` the legacy import-resolver config wraps, + * with `build.zig.zon` `.path` deps threaded through `loadResolutionConfig`. + */ + +import type { ParsedFile } from 'gitnexus-shared'; +import { SupportedLanguages } from 'gitnexus-shared'; +import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js'; +import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js'; +import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js'; +import { loadZigBuildZon, type ZigBuildZonConfig } from '../../language-config.js'; +import { resolveZigImportInternal } from '../../import-resolvers/zig.js'; +import { zigProvider } from '../zig.js'; +import { zigArityCompatibility, zigMergeBindings } from './index.js'; + +export const zigScopeResolver: ScopeResolver = { + language: SupportedLanguages.Zig, + languageProvider: zigProvider, + importEdgeReason: 'zig-scope: import', + + loadResolutionConfig: (repoPath: string) => loadZigBuildZon(repoPath), + + resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => + resolveZigImportInternal( + fromFile, + targetRaw, + allFilePaths, + (resolutionConfig as ZigBuildZonConfig | null | undefined) ?? null, + ), + + mergeBindings: zigMergeBindings, + arityCompatibility: zigArityCompatibility, + + buildMro: (graph, parsedFiles, nodeLookup) => + buildMro(graph, parsedFiles, nodeLookup, defaultLinearize), + + populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed), + + // Zig has no `super`. + isSuperReceiver: () => false, + + // Statically typed — the field-fallback heuristic over-connects. + fieldFallbackOnMethodLookup: false, +}; diff --git a/gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts new file mode 100644 index 000000000..25b08f7a1 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts @@ -0,0 +1,77 @@ +import type { + BindingRef, + Callsite, + CaptureMatch, + Scope, + ScopeId, + ScopeTree, + SymbolDefinition, + TypeRef, +} from 'gitnexus-shared'; + +/** Keep parameter (incl. `self`) typeBindings in the function scope — + * hoisting them to Module would pollute other functions' receiver + * resolution (same rationale as `goBindingScopeFor`). */ +export function zigBindingScopeFor( + decl: CaptureMatch, + innermost: Scope, + _tree: ScopeTree, +): ScopeId | null { + if (decl['@type-binding.parameter'] !== undefined) { + return innermost.id; + } + return null; // default auto-hoist for other bindings +} + +/** Zig's receiver convention is a first parameter named `self`; the + * `self`-sourced typeBinding on the function scope carries its type. */ +export function zigReceiverBinding(functionScope: Scope): TypeRef | null { + if (functionScope.kind !== 'Function') return null; + for (const binding of functionScope.typeBindings.values()) { + if (binding.source === 'self') return binding; + } + return null; +} + +const TIER: Record = { + local: 0, + namespace: 1, + import: 2, + reexport: 3, + wildcard: 4, +}; + +/** Local declarations shadow imports; deterministic order within a tier. */ +export function zigMergeBindings( + existing: readonly BindingRef[], + incoming: readonly BindingRef[], + _scopeId: string, +): BindingRef[] { + const seen = new Set(); + return [...existing, ...incoming] + .sort( + (a, b) => + (TIER[a.origin] ?? 99) - (TIER[b.origin] ?? 99) || a.def.nodeId.localeCompare(b.def.nodeId), + ) + .filter((binding) => { + if (seen.has(binding.def.nodeId)) return false; + seen.add(binding.def.nodeId); + return true; + }); +} + +/** Zig has no overloading; without synthesized arity metadata on + * declarations the comparison is always 'unknown' — kept as a real + * bounds check so it turns on if arity captures are added later. */ +export function zigArityCompatibility( + callsite: Callsite, + def: SymbolDefinition, +): 'compatible' | 'unknown' | 'incompatible' { + const max = def.parameterCount; + const min = def.requiredParameterCount; + if (max === undefined && min === undefined) return 'unknown'; + if (!Number.isFinite(callsite.arity) || callsite.arity < 0) return 'unknown'; + if (min !== undefined && callsite.arity < min) return 'incompatible'; + if (max !== undefined && callsite.arity > max) return 'incompatible'; + return 'compatible'; +} diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts index cd225d9a2..bcfe5a983 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts @@ -27,6 +27,7 @@ import { cobolScopeResolver } from '../../languages/cobol/scope-resolver.js'; import { swiftScopeResolver } from '../../languages/swift/scope-resolver.js'; import { dartScopeResolver } from '../../languages/dart/scope-resolver.js'; import { vueScopeResolver } from '../../languages/vue/scope-resolver.js'; +import { zigScopeResolver } from '../../languages/zig/scope-resolver.js'; /** Map of `SupportedLanguages` → `ScopeResolver`. The scope-resolution phase * iterates this map directly — every registered resolver runs. This is the @@ -51,4 +52,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap = n [SupportedLanguages.Swift, swiftScopeResolver], [SupportedLanguages.Dart, dartScopeResolver], [SupportedLanguages.Vue, vueScopeResolver], + [SupportedLanguages.Zig, zigScopeResolver], ]); diff --git a/gitnexus/test/integration/resolvers/zig.test.ts b/gitnexus/test/integration/resolvers/zig.test.ts index d2528d893..cf891f809 100644 --- a/gitnexus/test/integration/resolvers/zig.test.ts +++ b/gitnexus/test/integration/resolvers/zig.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'path'; import { + edgeSet, FIXTURES, getNodesByLabel, getRelationships, @@ -41,17 +42,22 @@ describe('Zig basic resolution', () => { expect(methods).toContain('reset'); }); - // IMPORTS (and CALLS) edges are produced by the scope-resolution pipeline, - // which requires the provider to implement `emitScopeCaptures` / - // `interpretImport`. Zig is classified `experimental` and does not provide - // those hooks yet — the import RESOLVER itself (relative paths + - // build.zig.zon, see test/unit/zig-import-resolver.test.ts) is wired into - // the resolver factory and becomes live the moment the hooks land. - // Un-skip when Zig gains scope-resolution hooks. - it.skip('resolves the relative @import("./pioneer.zig") to pioneer.zig', () => { + 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'); }); + + it('emits a CALLS edge for the free call main → helper', () => { + const calls = getRelationships(result, 'CALLS'); + expect(edgeSet(calls)).toContain('main → helper'); + }); + + it('emits a CALLS edge for the receiver-bound method call main → tick', () => { + const calls = getRelationships(result, 'CALLS'); + // `var p = pioneer.Pioneer{…}; p.tick()` — constructor-inferred receiver + // type through the namespace import, dispatched onto Pioneer.tick. + expect(edgeSet(calls)).toContain('main → tick'); + }); });