diff --git a/__tests__/zig-extraction.test.ts b/__tests__/zig-extraction.test.ts new file mode 100644 index 000000000..66e370250 --- /dev/null +++ b/__tests__/zig-extraction.test.ts @@ -0,0 +1,156 @@ +/** + * Zig Extraction Tests + * + * Zig has no classes — types are values bound to a const (`const X = struct{}`) + * and modules are `const m = @import(...)`. These tests pin the idioms the + * extractor must get right: container types, scope-based methods, fields, enum + * members, constants, imports, test blocks, and the import-namespace mappings + * that make cross-file `callers`/`callees` resolve. + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import type { ExtractionResult } from '../src/types'; +import { extractFromSource } from '../src/extraction'; +import { + detectLanguage, + isLanguageSupported, + getSupportedLanguages, + initGrammars, + loadAllGrammars, +} from '../src/extraction/grammars'; +import { extractImportMappings } from '../src/resolution/import-resolver'; + +beforeAll(async () => { + await initGrammars(); + await loadAllGrammars(); +}); + +const SAMPLE = ` +const std = @import("std"); +const helper = @import("./util/helper.zig"); + +pub const max_items: u32 = 8; + +pub const Color = enum { red, green, blue }; + +pub const Point = struct { + x: f32, + y: f32, + + pub fn add(self: Point, other: Point) Point { + return .{ .x = self.x + other.x, .y = self.y + other.y }; + } +}; + +pub fn translate(p: Point) Point { + return helper.shift(p); +} + +test "point add" { + _ = Point.add; +} +`; + +describe('Zig language wiring', () => { + it('detects .zig as zig and reports it supported', () => { + expect(detectLanguage('src/main.zig')).toBe('zig'); + expect(isLanguageSupported('zig')).toBe(true); + expect(getSupportedLanguages()).toContain('zig'); + }); +}); + +describe('Zig extraction', () => { + // Computed in beforeAll, not at collection time — the file-level beforeAll + // must load the grammar first. + let result: ExtractionResult; + beforeAll(() => { + result = extractFromSource('shapes.zig', SAMPLE, 'zig'); + }); + + const byKind = (kind: string, name: string) => + result.nodes.find((n) => n.kind === kind && n.name === name); + + it('extracts a const-bound struct as a struct, not a constant', () => { + expect(byKind('struct', 'Point')).toBeDefined(); + expect(byKind('constant', 'Point')).toBeUndefined(); + }); + + it('extracts a struct method as a method (scope-based, no receiver syntax)', () => { + const add = byKind('method', 'add'); + expect(add).toBeDefined(); + expect(add!.qualifiedName).toContain('Point'); + }); + + it('extracts struct fields', () => { + expect(byKind('field', 'x')).toBeDefined(); + expect(byKind('field', 'y')).toBeDefined(); + }); + + it('extracts a const-bound enum and its members', () => { + expect(byKind('enum', 'Color')).toBeDefined(); + expect(byKind('enum_member', 'red')).toBeDefined(); + expect(byKind('enum_member', 'blue')).toBeDefined(); + }); + + it('extracts a top-level fn as a function and a plain const as a constant', () => { + expect(byKind('function', 'translate')).toBeDefined(); + expect(byKind('constant', 'max_items')).toBeDefined(); + }); + + it('extracts @import as import nodes', () => { + expect(byKind('import', 'std')).toBeDefined(); + expect(byKind('import', './util/helper.zig')).toBeDefined(); + }); + + it('extracts a test block as a callable function node', () => { + expect(byKind('function', 'point add')).toBeDefined(); + }); + + it('emits a calls reference for a namespaced member call', () => { + // `helper.shift(p)` — the dotted ref the resolver maps through the import. + const ref = result.unresolvedReferences.find( + (r) => r.referenceName === 'helper.shift' && r.referenceKind === 'calls' + ); + expect(ref).toBeDefined(); + }); +}); + +describe('Zig import mappings (cross-file resolution)', () => { + it('maps @import bindings to namespace imports', () => { + const maps = extractImportMappings('shapes.zig', SAMPLE, 'zig'); + const helper = maps.find((m) => m.localName === 'helper'); + expect(helper).toBeDefined(); + expect(helper!.source).toBe('./util/helper.zig'); + expect(helper!.isNamespace).toBe(true); + expect(maps.find((m) => m.localName === 'std')).toBeDefined(); + }); +}); + +describe('Zig generic-type factories', () => { + // `fn List(T) type { return struct {...} }` — Zig's generic types are + // functions returning an anonymous container. + const FACTORY = ` +pub fn List(comptime T: type) type { + return struct { + items: []T, + pub fn append(self: *@This(), x: T) void { _ = self; _ = x; } + pub fn clear(self: *@This()) void { _ = self; } + }; +}`; + let nodes: { kind: string; name: string; qualifiedName?: string }[]; + beforeAll(() => { + nodes = extractFromSource('list.zig', FACTORY, 'zig').nodes; + }); + + it('indexes the factory as a struct named for the function', () => { + expect(nodes.find((n) => n.kind === 'struct' && n.name === 'List')).toBeDefined(); + }); + + it('indexes the returned container declarations as methods of that type', () => { + const append = nodes.find((n) => n.kind === 'method' && n.name === 'append'); + expect(append).toBeDefined(); + expect(append!.qualifiedName).toContain('List'); + expect(nodes.find((n) => n.kind === 'method' && n.name === 'clear')).toBeDefined(); + expect(nodes.find((n) => n.kind === 'field' && n.name === 'items')).toBeDefined(); + }); +}); diff --git a/src/extraction/function-ref.ts b/src/extraction/function-ref.ts index 1bae970a4..49930d19f 100644 --- a/src/extraction/function-ref.ts +++ b/src/extraction/function-ref.ts @@ -226,6 +226,21 @@ const RUST_SPEC: FnRefSpec = { ]), }; +// Zig has no closures, so a callback is an inline anonymous-struct method passed +// by value: `std.sort.sort(items, ctx, struct { fn cmp(_,a,b) bool {…} }.cmp)`. +// The value is a `field_expression` (.cmp); comptime dispatch tables use +// `.{ .add = addKernel }` (field_initializer) and `.{ a, b }` (initializer_list). +const ZIG_SPEC: FnRefSpec = { + idTypes: new Set(['identifier']), + dispatch: new Map([ + ['arguments', { mode: 'args' }], + ['assignment_expression', { mode: 'rhs', field: 'right' }], + ['field_initializer', { mode: 'value' }], + ['initializer_list', { mode: 'list' }], + ]), + special: new Set(['field_expression']), +}; + const JAVA_SPEC: FnRefSpec = { // No bare-identifier function values in Java — only method references. idTypes: new Set(), @@ -384,6 +399,7 @@ export const FN_REF_SPECS: Record = { python: PYTHON_SPEC, go: GO_SPEC, rust: RUST_SPEC, + zig: ZIG_SPEC, java: JAVA_SPEC, kotlin: KOTLIN_SPEC, csharp: CSHARP_SPEC, @@ -615,6 +631,17 @@ function normalizeSpecial( source: string ): NormalizedRef[] { switch (type) { + // Zig `Container.member` used as a value — the inline-anonymous-struct + // callback idiom `struct { fn cmp(…) {} }.cmp` (Zig's closure substitute). + // Capture the member; the same-file gate keeps only members naming a + // function defined here, so ordinary `obj.field` data reads drop out. + case 'field_expression': { + const member = getChildByField(node, 'member'); + return member && member.type === 'identifier' + ? [{ name: getNodeText(member, source), node: member }] + : []; + } + // Java method references. Receiver decides the resolution route (#808): // `this::run0` / `super::close` → `this.` (class-scoped resolver; // super rides the inherited-member supertype pass) diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 1b15996c0..26c7635f5 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -24,6 +24,12 @@ const WASM_GRAMMAR_FILES: Record = { python: 'tree-sitter-python.wasm', go: 'tree-sitter-go.wasm', rust: 'tree-sitter-rust.wasm', + // Vendored: built from tree-sitter-zig patched for Zig 0.16 (asm-clobbers + // struct, de-keyworded async/await/suspend/resume, error-set field types, + // reserved-word block labels, conditional slice/pointer element types) — not + // in tree-sitter-wasms. 100% parse across the Zig 0.16 standard library, + // Ghostty, TigerBeetle, libxev and http.zig. + zig: 'tree-sitter-zig.wasm', java: 'tree-sitter-java.wasm', c: 'tree-sitter-c.wasm', cpp: 'tree-sitter-cpp.wasm', @@ -61,6 +67,7 @@ export const EXTENSION_MAP: Record = { '.pyw': 'python', '.go': 'go', '.rs': 'rust', + '.zig': 'zig', '.java': 'java', '.c': 'c', '.h': 'c', // Could also be C++, defaulting to C @@ -221,7 +228,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise> = { python: pythonExtractor, go: goExtractor, rust: rustExtractor, + zig: zigExtractor, java: javaExtractor, c: cExtractor, cpp: cppExtractor, diff --git a/src/extraction/languages/zig.ts b/src/extraction/languages/zig.ts new file mode 100644 index 000000000..1ea804f43 --- /dev/null +++ b/src/extraction/languages/zig.ts @@ -0,0 +1,314 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import { getNodeText, getChildByField, getPrecedingDocstring } from '../tree-sitter-helpers'; +import type { LanguageExtractor, ExtractorContext } from '../tree-sitter-types'; + +/** + * Zig extraction. + * + * Zig has no classes and no top-level named type declarations: a type is a + * VALUE produced by a container expression (`struct`/`enum`/`union`/`opaque`) + * and bound to a `const`, and a module is a `const` bound to `@import(...)`. + * So one grammar node — `variable_declaration` — fans out to several CodeGraph + * kinds depending on its right-hand side: + * + * const Point = struct { ... }; // → struct + * const Color = enum { ... }; // → enum + * const Token = union { ... }; // → struct (no `union` NodeKind) + * const std = @import("std"); // → import + `imports` ref + * const max = 8; // → constant + * var count: usize = 0; // → variable + * + * The node-type dispatch ladder in tree-sitter.ts keys on a single node type, + * so this fan-out is done in the `visitNode` hook (the documented escape hatch + * for languages whose AST shape doesn't fit the ladder — Pascal uses it too). + * Everything UNAMBIGUOUS is left to the ladder: `function_declaration` (a free + * function at file scope, a method when nested in a container scope the hook + * pushed), and `call_expression`. Methods are detected purely by scope — + * `isInsideClassLikeNode()` — because Zig has no receiver syntax; the `self` + * parameter is an ordinary parameter, so `getReceiverType` is deliberately unset. + * + * Container members live as DIRECT children of the container node (there is no + * `body` field), so `extractStruct`/`extractEnum` — which require one — can't be + * reused; the hook walks members itself and routes each back through + * `ctx.visitNode`, so methods/nested types/calls still flow through the core. + */ + +/** Container-expression node types whose members are walked as a scope. */ +const CONTAINER_KINDS = new Set([ + 'struct_declaration', + 'union_declaration', + 'opaque_declaration', + 'enum_declaration', +]); + +/** `@import`/`@embedFile`/`@cImport` builtins that introduce a module dependency. */ +const IMPORT_BUILTINS = new Set(['@import', '@embedFile', '@cImport']); + +/** Scope kinds under which a `const`/`var` is a real symbol, not a function local. */ +const CONTAINER_SCOPE_KINDS = new Set([ + 'file', 'module', 'namespace', 'struct', 'enum', 'class', 'interface', 'trait', +]); + +/** Whether a node has the `pub` visibility modifier as a direct child token. */ +function hasPub(node: SyntaxNode): boolean { + for (let i = 0; i < node.childCount; i++) { + if (node.child(i)?.type === 'pub') return true; + } + return false; +} + +/** `const` vs `var` — read the leading keyword token; default to const. */ +function isConstDecl(node: SyntaxNode): boolean { + for (let i = 0; i < node.childCount; i++) { + const t = node.child(i)?.type; + if (t === 'const') return true; + if (t === 'var') return false; + } + return true; +} + +/** The bound name of a `variable_declaration` — its first `identifier` child + * (the type annotation's identifiers are nested under the `type` field). */ +function declName(node: SyntaxNode, source: string): string | null { + const id = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier'); + return id ? getNodeText(id, source) : null; +} + +/** The right-hand side of a `variable_declaration`: the first named child that + * begins after the `=` token. Returns null for a bare `var x: T;` (no value). */ +function rhsValue(node: SyntaxNode): SyntaxNode | null { + let eqEnd = -1; + for (let i = 0; i < node.childCount; i++) { + const c = node.child(i); + if (c && c.type === '=') { eqEnd = c.endIndex; break; } + } + if (eqEnd < 0) return null; + return node.namedChildren.find((c: SyntaxNode) => c.startIndex >= eqEnd) ?? null; +} + +/** The imported module/file for an `@import`-family builtin, or null. The string + * argument is returned verbatim ("std", "builtin", "./foo.zig"); a `@cImport` + * with no string argument resolves to the conventional "c" module. */ +function importTarget(builtin: SyntaxNode, source: string): string | null { + const id = builtin.namedChildren.find((c: SyntaxNode) => c.type === 'builtin_identifier'); + if (!id) return null; + const name = getNodeText(id, source); + if (!IMPORT_BUILTINS.has(name)) return null; + const args = builtin.namedChildren.find((c: SyntaxNode) => c.type === 'arguments'); + const str = args?.namedChildren.find((c: SyntaxNode) => c.type === 'string'); + if (!str) return name === '@cImport' ? 'c' : null; + return getNodeText(str, source).replace(/^"/, '').replace(/"$/, ''); +} + +/** The CodeGraph kind of the innermost scope on the stack ('file' when empty). */ +function scopeKind(ctx: ExtractorContext): string { + if (ctx.nodeStack.length === 0) return 'file'; + const top = ctx.nodeStack[ctx.nodeStack.length - 1]; + return ctx.nodes.find((n) => n.id === top)?.kind ?? 'file'; +} + +/** + * Create the type node for a container-valued declaration and walk its members + * under that scope, so nested `function_declaration`s become methods (via the + * core ladder's `isInsideClassLikeNode()` check) and nested types recurse here. + * An `enum`'s `container_field`s are its members (→ `enum_member`); a struct's + * are fields (→ `field`). + */ +function extractContainer( + decl: SyntaxNode, + value: SyntaxNode, + name: string, + ctx: ExtractorContext, +): void { + const isEnum = value.type === 'enum_declaration'; + const owner = ctx.createNode(isEnum ? 'enum' : 'struct', name, decl, { + docstring: getPrecedingDocstring(decl, ctx.source), + visibility: hasPub(decl) ? 'public' : 'private', + isExported: hasPub(decl), + }); + if (!owner) return; + + ctx.pushScope(owner.id); + for (const child of value.namedChildren) { + if (child.type === 'container_field') { + const nameNode = getChildByField(child, 'name') ?? child; + const member = getNodeText(nameNode, ctx.source); + if (isEnum) { + ctx.createNode('enum_member', member, child); + } else { + const typeNode = getChildByField(child, 'type'); + ctx.createNode('field', member, child, { + signature: typeNode ? `: ${getNodeText(typeNode, ctx.source)}` : undefined, + }); + } + } else { + // function_declaration → method, nested variable_declaration → back here, + // test_declaration → test, comptime_declaration → descend for calls. + ctx.visitNode(child); + } + } + ctx.popScope(); +} + +/** An `error { A, B }` set bound to a const → an enum whose members are the + * error names, so `MyError.A` navigation and impact analysis resolve. */ +function extractErrorSet(decl: SyntaxNode, value: SyntaxNode, name: string, ctx: ExtractorContext): void { + const owner = ctx.createNode('enum', name, decl, { + docstring: getPrecedingDocstring(decl, ctx.source), + visibility: hasPub(decl) ? 'public' : 'private', + isExported: hasPub(decl), + }); + if (!owner) return; + ctx.pushScope(owner.id); + for (const child of value.namedChildren) { + if (child.type === 'identifier') { + ctx.createNode('enum_member', getNodeText(child, ctx.source), child); + } + } + ctx.popScope(); +} + +/** `const m = @import("foo.zig")` → an `import` node plus an `imports` reference + * the resolver maps to the target file (internal) or leaves external (std). */ +function extractImport(decl: SyntaxNode, value: SyntaxNode, ctx: ExtractorContext): boolean { + const target = importTarget(value, ctx.source); + if (!target) return false; + ctx.createNode('import', target, decl, { signature: getNodeText(decl, ctx.source).trim() }); + const parentId = ctx.nodeStack[ctx.nodeStack.length - 1]; + if (parentId) { + ctx.addUnresolvedReference({ + fromNodeId: parentId, + referenceName: target, + referenceKind: 'imports', + line: decl.startPosition.row + 1, + column: decl.startPosition.column, + }); + } + return true; +} + +/** Route a `variable_declaration` to the right extraction based on its RHS. */ +function visitVarDecl(node: SyntaxNode, ctx: ExtractorContext): boolean { + const name = declName(node, ctx.source); + if (!name) return false; + const value = rhsValue(node); + + if (value) { + if (CONTAINER_KINDS.has(value.type)) { + extractContainer(node, value, name, ctx); + return true; + } + if (value.type === 'error_set_declaration') { + extractErrorSet(node, value, name, ctx); + return true; + } + if (value.type === 'builtin_function' && extractImport(node, value, ctx)) { + return true; + } + } + + // A plain value is a symbol only at container scope; inside a function body it + // is a local and must not become a node. + if (!CONTAINER_SCOPE_KINDS.has(scopeKind(ctx))) return true; + const valText = value ? getNodeText(value, ctx.source).slice(0, 80) : undefined; + ctx.createNode(isConstDecl(node) ? 'constant' : 'variable', name, node, { + docstring: getPrecedingDocstring(node, ctx.source), + signature: valText ? `= ${valText}${valText.length >= 80 ? '...' : ''}` : undefined, + visibility: hasPub(node) ? 'public' : 'private', + isExported: hasPub(node), + }); + return true; +} + +/** `test "name" { ... }` (or unnamed `test { ... }`) → a `function` node whose + * body is walked, so a test shows up in `callers`/blast-radius of what it + * exercises — the thing CodeGraph is for when triaging a Zig change. */ +function visitTest(node: SyntaxNode, ctx: ExtractorContext): boolean { + const str = node.namedChildren.find((c: SyntaxNode) => c.type === 'string' || c.type === 'identifier'); + const name = str ? getNodeText(str, ctx.source).replace(/^"/, '').replace(/"$/, '') : 'test'; + const fn = ctx.createNode('function', name, node, { signature: 'test' }); + const body = node.namedChildren.find((c: SyntaxNode) => c.type === 'block'); + if (fn && body) { + ctx.pushScope(fn.id); + ctx.visitFunctionBody(body, fn.id); + ctx.popScope(); + } + return true; +} + +/** A type factory `fn Name(...) type { return struct {...}; }` — return the + * container the function produces, or null if it isn't one. Only a direct + * `return` statement is considered; nested scopes (the container's own methods) + * are not searched. A returned `struct {...}` is a type DEFINITION + * (struct_declaration); a returned `.{...}` value is not, so plain functions + * are never mistaken for factories. */ +function returnedContainer(fnNode: SyntaxNode): SyntaxNode | null { + const body = getChildByField(fnNode, 'body'); + if (!body) return null; + for (const stmt of body.namedChildren) { + const ret = stmt.type === 'return_expression' + ? stmt + : stmt.namedChildren.find((c: SyntaxNode) => c.type === 'return_expression'); + const val = ret?.namedChildren[0]; + if (val && CONTAINER_KINDS.has(val.type)) return val; + } + return null; +} + +/** Zig generic types ARE functions returning an anonymous container + * (`fn List(comptime T: type) type { return struct {...}; }` — the ArrayList + * idiom). Index such a factory as the type it yields: a struct/enum named for + * the function, with the container's declarations as methods, so `List.append` + * navigates like any other type. A normal function returns false and falls + * through to the core ladder unchanged. */ +function visitFnDecl(node: SyntaxNode, ctx: ExtractorContext): boolean { + const container = returnedContainer(node); + if (!container) return false; + const nameNode = getChildByField(node, 'name'); + if (!nameNode) return false; + extractContainer(node, container, getNodeText(nameNode, ctx.source), ctx); + return true; +} + +export const zigExtractor: LanguageExtractor = { + // function_declaration is BOTH the free-function and the method node type; + // the ladder picks method when it fires inside a pushed container scope. + functionTypes: ['function_declaration'], + classTypes: [], + methodTypes: ['function_declaration'], + interfaceTypes: [], + // Containers, imports, constants and fields are all reached through + // variable_declaration and handled in visitNode — so these stay empty. + structTypes: [], + enumTypes: [], + typeAliasTypes: [], + importTypes: [], + variableTypes: [], + callTypes: ['call_expression'], + nameField: 'name', + bodyField: 'body', + paramsField: 'parameters', + returnField: 'type', // Zig: a function's return type is the `type` field. + + getSignature: (node, source) => { + const params = node.namedChildren.find((c: SyntaxNode) => c.type === 'parameters'); + const ret = getChildByField(node, 'type'); + if (!params && !ret) return undefined; + let sig = params ? getNodeText(params, source) : '()'; + if (ret) sig += ` ${getNodeText(ret, source)}`; + return sig; + }, + + // `pub` is Zig's only visibility marker (visible to importers); everything + // else is file-private. `export`/`extern` are linkage, not source visibility. + getVisibility: (node) => (hasPub(node) ? 'public' : 'private'), + isExported: (node) => hasPub(node), + + visitNode: (node: SyntaxNode, ctx: ExtractorContext): boolean => { + if (node.type === 'variable_declaration') return visitVarDecl(node, ctx); + if (node.type === 'function_declaration') return visitFnDecl(node, ctx); + if (node.type === 'test_declaration') return visitTest(node, ctx); + return false; + }, + +}; diff --git a/src/extraction/wasm/tree-sitter-zig.wasm b/src/extraction/wasm/tree-sitter-zig.wasm new file mode 100755 index 000000000..6a0ef06a9 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-zig.wasm differ diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index badbe4b02..b3eea2b6f 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -46,6 +46,17 @@ export function resolveImportPath( language: Language, context: ResolutionContext ): string | null { + // Zig: an @import path with a `.zig` extension (or a `./` / `../` prefix) is + // a FILE resolved relative to the importing file's directory — and Zig allows + // the relative path WITHOUT a leading `./` (`@import("util/widget.zig")`), + // which the generic `.`-prefix check below would miss. A bare name (`std`, + // `builtin`, a build.zig.zon package) names no project file → external. + if (language === 'zig') { + if (!importPath.endsWith('.zig') && !importPath.startsWith('.')) return null; + const zigFromDir = path.dirname(path.join(context.getProjectRoot(), fromFile)); + return resolveRelativeImport(importPath, zigFromDir, language, context); + } + // Skip external/npm packages — but pass the context so the // bare-specifier heuristic can consult the project's tsconfig // alias map first (custom prefixes like `@components/*` would @@ -604,6 +615,8 @@ export function extractImportMappings( mappings.push(...extractPHPImports(content)); } else if (language === 'c' || language === 'cpp') { mappings.push(...extractCppImports(content)); + } else if (language === 'zig') { + mappings.push(...extractZigImports(content)); } return mappings; @@ -911,6 +924,33 @@ function extractCppImports(content: string): ImportMapping[] { return mappings; } +/** + * Extract Zig import mappings. + * + * Zig binds an imported module to a const: `const widget = + * @import("../ui/widget.zig");`. That bound name is the namespace through + * which the file's `pub` declarations are called (`widget.draw(...)`), + * so each mapping is a namespace import (isNamespace: true) whose `source` is + * the raw import path. `resolveImportPath` maps a `.zig`/relative path to the + * file and leaves bare names (`std`, `builtin`, packages) external. + */ +function extractZigImports(content: string): ImportMapping[] { + const mappings: ImportMapping[] = []; + const re = /\b(?:pub\s+)?const\s+(\w+)\s*=\s*@import\s*\(\s*"([^"]+)"\s*\)/g; + let match: RegExpExecArray | null; + while ((match = re.exec(content)) !== null) { + const [, localName, source] = match; + mappings.push({ + localName: localName!, + exportedName: '*', + source: source!, + isDefault: false, + isNamespace: true, + }); + } + return mappings; +} + // Cache import mappings per file to avoid re-reading and re-parsing const importMappingCache = new Map(); @@ -1131,7 +1171,7 @@ export function resolveViaImport( // include-dir scan path inside resolveImportPath never produces an // edge — resolveViaImport's symbol lookup below would search the // resolved file for a symbol named like the file extension and fail. - if ((ref.language === 'c' || ref.language === 'cpp') && ref.referenceKind === 'imports') { + if ((ref.language === 'c' || ref.language === 'cpp' || ref.language === 'zig') && ref.referenceKind === 'imports') { // C/C++ quoted includes (`#include "X.h"`) resolve relative to the // INCLUDING file's own directory first (the C standard's quoted-include // search order). Prefer a same-directory header over an -I directory or a diff --git a/src/types.ts b/src/types.ts index 656bb1090..f5da9dc18 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,7 @@ export const LANGUAGES = [ 'python', 'go', 'rust', + 'zig', 'java', 'c', 'cpp',