mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(zig): Const/Variable defs, member imports, receiver typing, generic type constructors
Coverage gaps found by indexing idiomatic Zig against the branch:
- Const / Variable nodes: ZIG_QUERIES had no @definition.const /
@definition.variable, so `pub const VERSION`, error sets and type
aliases were absent and zigVariableConfig never ran. Rules are gated on
the literal `const` / `var` keyword — tree-sitter-zig 1.1.2 parses
statement assignments (`x = 5;`, `x += 1;`, `_ = expr;`) as keyword-
less `variable_declaration`s, and the scope query minted a phantom
local per assignment and one `_` per discard. Container and @import
bindings are skipped via `shouldSkipDefinitionCapture`.
- Imports: `const X = @import("x.zig").X` (named / alias), `const X =
ns.X` where `ns` is an @import binding of the file (promoted to a
named import), and `pub usingnamespace @import(...)` (wildcard, with
`expandsWildcardTo`). All three lost the file-level IMPORTS edge.
- Receiver typing: `var x: T = undefined` / decl literals `const x: T =
.init()` (annotation), `var c = T.init()` / `mod.T.init()` (call
return), `List(u8){}` (instantiation literal); `normalizeZigTypeName`
drops the comptime argument list.
- Generic type constructors `fn List(comptime T: type) type { return
struct {…}; }`: the returned container is a Struct/Union/Enum named
after the fn, owns its members (HAS_METHOD / HAS_PROPERTY), binds in
the module scope beside the Function def, and is emitted ahead of it
so a named import binds the type.
- `export` vs `pub`: `visibility` is now `pub`-only (Zig-module fact);
`isExported` keeps `pub|export` (visible outside the unit, as C's
external linkage). `export fn` without `pub` is not reachable from
other Zig files.
- Extractor configs share `zigContainerName`; ast-helpers' owner walk
learns the type-constructor shape.
Tests: zig-idioms fixture (10 resolver cases), extractor/interpret unit
cases for each rule.
This commit is contained in:
parent
69831484ef
commit
65c07c3d9a
16 changed files with 958 additions and 89 deletions
|
|
@ -1,25 +1,20 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { ClassExtractionConfig, ClassLikeNodeLabel } from '../../class-types.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { ZIG_CONTAINER_TYPES } from '../../languages/zig/captures.js';
|
||||
import { ZIG_CONTAINER_TYPES, zigContainerName } from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig containers (struct/enum/union/opaque) are anonymous in the grammar:
|
||||
*
|
||||
* const Point = struct { ... };
|
||||
* pub fn List(comptime T: type) type { return struct { ... }; }
|
||||
*
|
||||
* The binding name is the first identifier child of the parent
|
||||
* variable_declaration. Walk up one level to find it.
|
||||
* variable_declaration, or the generic type constructor's name —
|
||||
* `zigContainerName` is the single source shared with the field/method
|
||||
* extractors so owner ids and node ids agree by construction.
|
||||
*/
|
||||
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 extractZigContainerName = (node: SyntaxNode): string | undefined => zigContainerName(node);
|
||||
|
||||
const extractZigContainerType = (node: SyntaxNode): ClassLikeNodeLabel | undefined => {
|
||||
if (node.type === 'struct_declaration') return 'Struct';
|
||||
|
|
|
|||
|
|
@ -249,9 +249,16 @@ const ZIG_DECL_TYPES = new Set(['function_declaration', 'variable_declaration'])
|
|||
/**
|
||||
* Zig: walk to the enclosing decl, scan its direct children for an unnamed `pub`
|
||||
* or `export` keyword token (tree-sitter-zig models both as anonymous keyword
|
||||
* children of function_declaration / variable_declaration). `pub` is Zig-module
|
||||
* visibility; `export` is C-ABI linkage (`export fn add(...)`) — the strongest
|
||||
* form of "exported", used for FFI entry points, and it never carries `pub`.
|
||||
* children of function_declaration / variable_declaration). Two different
|
||||
* facts share this one flag, on purpose:
|
||||
* - `pub` is Zig-module visibility — reachable from another `.zig` file
|
||||
* through `@import`;
|
||||
* - `export` is C-ABI linkage (`export fn add(...)`) — the symbol lands in
|
||||
* the object file for FFI callers, and it never carries `pub`.
|
||||
* `isExported` means "visible outside this compilation unit" graph-wide (C uses
|
||||
* external linkage for the same flag), so both qualify. The `visibility`
|
||||
* property is the Zig-only fact and is `pub`-only — see `hasZigPubKeyword`:
|
||||
* an `export fn` without `pub` is public to C and PRIVATE to other Zig files.
|
||||
* Container fields (struct/enum variants) are public if their enclosing
|
||||
* variable_declaration is public.
|
||||
*
|
||||
|
|
@ -270,11 +277,9 @@ export const zigExportChecker: ExportChecker = (node, _name) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Does this Zig declaration carry a `pub` or `export` keyword child? Shared by
|
||||
* the export checker and the method/variable extractors' `extractVisibility`,
|
||||
* so the three cannot disagree on what "public" means (a `pub`-only copy in
|
||||
* one of them left `export fn` private in the graph while `isExported` said
|
||||
* otherwise).
|
||||
* Does this Zig declaration carry a `pub` or `export` keyword child? Feeds
|
||||
* `isExported` (visible outside the compilation unit — to Zig importers OR to
|
||||
* C callers).
|
||||
*/
|
||||
export function hasZigVisibilityKeyword(declNode: SyntaxNode): boolean {
|
||||
for (let i = 0; i < declNode.childCount; i++) {
|
||||
|
|
@ -283,3 +288,18 @@ export function hasZigVisibilityKeyword(declNode: SyntaxNode): boolean {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this Zig declaration carry a `pub` keyword child? Feeds `visibility`,
|
||||
* the Zig-module fact: per the language reference, only `pub` declarations are
|
||||
* accessible from another file via `@import`; `export` alone gives C linkage
|
||||
* and leaves the declaration private to Zig code. So `export fn c_add` reports
|
||||
* `isExported: true` (FFI surface) with `visibility: 'private'` (Zig surface)
|
||||
* — two facts, two properties, deliberately not one.
|
||||
*/
|
||||
export function hasZigPubKeyword(declNode: SyntaxNode): boolean {
|
||||
for (let i = 0; i < declNode.childCount; i++) {
|
||||
if (declNode.child(i)?.type === 'pub') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import type { FieldExtractionConfig } from '../generic.js';
|
||||
import { ZIG_CONTAINER_TYPES } from '../../languages/zig/captures.js';
|
||||
import { ZIG_CONTAINER_TYPES, zigContainerName } from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig containers (struct/enum/union/opaque) are anonymous in tree-sitter-zig; the
|
||||
* binding name is the first identifier child of the parent variable_declaration.
|
||||
* Zig containers (struct/enum/union/opaque) are anonymous in tree-sitter-zig;
|
||||
* the binding name is the first identifier child of the parent
|
||||
* variable_declaration, or the enclosing generic type constructor's name —
|
||||
* `zigContainerName` is the single source.
|
||||
*/
|
||||
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 extractZigOwnerName = (node: SyntaxNode): string | undefined => zigContainerName(node);
|
||||
|
||||
/**
|
||||
* Container fields appear as direct children of struct_declaration /
|
||||
|
|
|
|||
|
|
@ -7,17 +7,25 @@
|
|||
* `@heritage.*` captures).
|
||||
* - exportChecker: walks to the enclosing variable_declaration /
|
||||
* function_declaration and looks for a `pub` or `export` 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.
|
||||
* - importResolver: resolves local `@import("./foo.zig")` paths and
|
||||
* build.zig.zon `.path` deps (root the dep's build.zig declares, then
|
||||
* src/root.zig, src/<name>.zig, src/main.zig); `@import("std")` and
|
||||
* `.url` packages are deliberately external.
|
||||
* - namedBindingExtractor: omitted — the scope side handles
|
||||
* `const Foo = @import("x").Foo` and `const Foo = ns.Foo` (ns an
|
||||
* @import binding) as NAMED imports instead (`zig/captures.ts`).
|
||||
* - 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).
|
||||
* `zig/query.ts` (containers as Class scopes — including the container a
|
||||
* generic type constructor `fn List(comptime T: type) type` returns,
|
||||
* named after the fn; container-nested fns relabeled @declaration.method;
|
||||
* plain-variable groups filtered for container/import bindings and for
|
||||
* the keyword-less `variable_declaration`s tree-sitter-zig uses for
|
||||
* statement assignments); `interpretImport` maps `const x = @import("…")`
|
||||
* to a namespace import, member forms to named/alias imports and
|
||||
* `usingnamespace` to a wildcard; receiver types come from `self`
|
||||
* parameters, `T{…}` / `mod.T{…}` / `List(u8){…}` literals, `T.init()`
|
||||
* call returns and `x: T` annotations (incl. decl literals). The
|
||||
* emit-side wiring lives in `zig/scope-resolver.ts` (SCOPE_RESOLVERS).
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
|
@ -42,6 +50,7 @@ import {
|
|||
interpretZigImport,
|
||||
interpretZigTypeBinding,
|
||||
isZigContainerMethod,
|
||||
isZigContainerOrImportBinding,
|
||||
zigArityCompatibility,
|
||||
zigBindingScopeFor,
|
||||
zigReceiverBinding,
|
||||
|
|
@ -64,6 +73,13 @@ export const zigProvider = defineLanguage({
|
|||
fieldExtractor: createFieldExtractor(zigFieldConfig),
|
||||
methodExtractor: createMethodExtractor(zigMethodConfig),
|
||||
variableExtractor: createVariableExtractor(zigVariableConfig),
|
||||
// A `const`/`var` whose value is a container or an `@import` is the
|
||||
// Struct/Enum/Union node or the import binding, not a Const beside it.
|
||||
shouldSkipDefinitionCapture: (captureMap, defaultLabel) => {
|
||||
if (defaultLabel !== 'Const' && defaultLabel !== 'Variable') return false;
|
||||
const decl = captureMap['definition.const'] ?? captureMap['definition.variable'];
|
||||
return decl !== undefined && isZigContainerOrImportBinding(decl);
|
||||
},
|
||||
labelOverride: (functionNode, defaultLabel) => {
|
||||
if (defaultLabel !== 'Function') return defaultLabel;
|
||||
if (isZigContainerMethod(functionNode)) return 'Method';
|
||||
|
|
|
|||
|
|
@ -15,25 +15,97 @@ export const ZIG_CONTAINER_TYPES: ReadonlySet<string> = new Set([
|
|||
'opaque_declaration',
|
||||
]);
|
||||
|
||||
/** Is `node` the `@import(…)` builtin call? */
|
||||
function isZigImportBuiltin(node: SyntaxNode | null): boolean {
|
||||
if (node?.type !== 'builtin_function') return false;
|
||||
const builtin = node.namedChild(0);
|
||||
return builtin?.type === 'builtin_identifier' && builtin.text === '@import';
|
||||
}
|
||||
|
||||
/** The `@import(…)` call at the ROOT of a value expression: the value itself
|
||||
* (`@import("x.zig")`) or the leftmost object of a member chain
|
||||
* (`@import("x.zig").Foo`, `@import("std").mem.Allocator`). Null otherwise. */
|
||||
export function zigImportRootOf(value: SyntaxNode | null): SyntaxNode | null {
|
||||
let cur = value;
|
||||
while (cur?.type === 'field_expression') cur = cur.childForFieldName('object');
|
||||
return isZigImportBuiltin(cur) ? cur : null;
|
||||
}
|
||||
|
||||
/** 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. Shared with
|
||||
* the variable extractor config so the structure-phase Variable records and
|
||||
* the scope-side bindings agree on what counts as a plain variable. */
|
||||
* or an import binding (`const x = @import("…")`, `const X = @import("…").X`)?
|
||||
* Those groups are emitted by their dedicated query rules; the plain
|
||||
* @declaration.variable / @definition.const match for the same node must be
|
||||
* dropped so the name binds exactly once (and no Const node shadows the
|
||||
* Struct / import). Shared by the scope walker, the variable extractor and
|
||||
* the provider's `shouldSkipDefinitionCapture` so the structure-phase records
|
||||
* and the scope-side bindings agree on what counts as a plain variable. */
|
||||
export function isZigContainerOrImportBinding(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;
|
||||
}
|
||||
if (zigImportRootOf(child) !== null) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Does this `variable_declaration` carry a `const` / `var` keyword child?
|
||||
* tree-sitter-zig 1.1.2 parses statement-position ASSIGNMENTS (`x = 5;`,
|
||||
* `x += 1;`, `_ = expr;`) as `variable_declaration` too — the only thing
|
||||
* that separates a real binding from a re-assignment is the keyword. Query
|
||||
* rules match the keyword literally; this is the JS-side twin for the
|
||||
* extractors and the phantom-local guard in `emitZigScopeCaptures`. */
|
||||
export function isZigKeywordDeclaration(declNode: SyntaxNode): boolean {
|
||||
for (let i = 0; i < declNode.childCount; i++) {
|
||||
const t = declNode.child(i)?.type;
|
||||
if (t === 'const' || t === 'var') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The binding name of a Zig container node, or undefined for a truly
|
||||
* anonymous one. Two shapes carry a name:
|
||||
* - `const Point = struct {…}` — the first identifier of the wrapping
|
||||
* `variable_declaration`;
|
||||
* - `pub fn List(comptime T: type) type { return struct {…}; }` — the
|
||||
* generic type constructor. Zig has no other spelling for a generic
|
||||
* type, and every reader calls the returned container `List`, so the
|
||||
* enclosing function's name IS the type name (`ArrayList(u8)`).
|
||||
* Single source for the class/field/method extractor configs. */
|
||||
export function zigContainerName(containerNode: SyntaxNode): string | undefined {
|
||||
const parent = containerNode.parent;
|
||||
if (parent === null || parent === undefined) return undefined;
|
||||
if (parent.type === 'variable_declaration') {
|
||||
for (let i = 0; i < parent.namedChildCount; i++) {
|
||||
const child = parent.namedChild(i);
|
||||
if (child?.type === 'identifier') return child.text;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return zigTypeConstructorOf(containerNode)?.childForFieldName('name')?.text;
|
||||
}
|
||||
|
||||
/** For a container that is the direct `return` value of a function whose
|
||||
* return type is `type` — `fn List(comptime T: type) type { return struct
|
||||
* {…}; }` — the function_declaration; null for any other placement. Only the
|
||||
* literal `return_expression → expression_statement → block → fn` chain
|
||||
* counts: a container nested deeper (`return struct {…}.field`, a container
|
||||
* inside an `if`) is not the type the function constructs. */
|
||||
export function zigTypeConstructorOf(containerNode: SyntaxNode): SyntaxNode | null {
|
||||
if (!ZIG_CONTAINER_TYPES.has(containerNode.type)) return null;
|
||||
const ret = containerNode.parent;
|
||||
if (ret?.type !== 'return_expression') return null;
|
||||
const stmt = ret.parent;
|
||||
if (stmt?.type !== 'expression_statement') return null;
|
||||
const block = stmt.parent;
|
||||
if (block?.type !== 'block') return null;
|
||||
const fn = block.parent;
|
||||
if (fn?.type !== 'function_declaration' || fn.childForFieldName('body')?.id !== block.id) {
|
||||
return null;
|
||||
}
|
||||
return fn.childForFieldName('type')?.text === 'type' ? fn : null;
|
||||
}
|
||||
|
||||
/** A `fn` nested in a struct/enum/union/opaque container is a method. Single
|
||||
* predicate shared between the provider's `labelOverride` (worker
|
||||
* structure phase) and the scope-capture relabel below, so the graph
|
||||
|
|
@ -117,6 +189,36 @@ export function emitZigScopeCaptures(
|
|||
const rawMatches = getZigScopeQuery().matches(tree.rootNode);
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
// Pre-pass: this file's `@import` bindings (`const counter =
|
||||
// @import("counter.zig")` → counter ↦ the string node), so a member alias
|
||||
// `const Counter = counter.Counter;` can be promoted to a named import of
|
||||
// `Counter` from `counter.zig` below. Aliases are collected in the same
|
||||
// pass so their plain-variable group is dropped (a local Const binding
|
||||
// would outrank the import binding it stands for).
|
||||
const importSources = new Map<string, SyntaxNode>();
|
||||
const aliasDeclIds = new Set<number>();
|
||||
for (const m of rawMatches) {
|
||||
const byName = new Map(m.captures.map((c) => [c.name, c.node] as const));
|
||||
const importName = byName.get('import.name');
|
||||
const importSource = byName.get('import.source');
|
||||
if (
|
||||
importName !== undefined &&
|
||||
importSource !== undefined &&
|
||||
byName.get('import.imported') === undefined &&
|
||||
byName.get('import.statement')?.parent?.type === 'source_file'
|
||||
) {
|
||||
importSources.set(importName.text, importSource);
|
||||
}
|
||||
}
|
||||
for (const m of rawMatches) {
|
||||
const byName = new Map(m.captures.map((c) => [c.name, c.node] as const));
|
||||
const stmt = byName.get('alias.statement');
|
||||
const ns = byName.get('alias.namespace');
|
||||
if (stmt !== undefined && ns !== undefined && importSources.has(ns.text)) {
|
||||
aliasDeclIds.add(stmt.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const m of rawMatches) {
|
||||
const grouped: Record<string, Capture> = {};
|
||||
const nodeMap: Record<string, SyntaxNode> = {};
|
||||
|
|
@ -128,10 +230,35 @@ export function emitZigScopeCaptures(
|
|||
}
|
||||
if (Object.keys(grouped).length === 0) continue;
|
||||
|
||||
// Member aliases: promote to a named import when the object is one of
|
||||
// this file's @import bindings; otherwise the group is inert (the same
|
||||
// node is also matched by the plain-variable rule).
|
||||
const aliasStmt = nodeMap['@alias.statement'];
|
||||
if (aliasStmt !== undefined) {
|
||||
if (!aliasDeclIds.has(aliasStmt.id)) continue;
|
||||
const source = importSources.get(nodeMap['@alias.namespace']!.text)!;
|
||||
out.push({
|
||||
'@import.statement': nodeToCapture('@import.statement', aliasStmt),
|
||||
'@import.name': nodeToCapture('@import.name', nodeMap['@alias.name']!),
|
||||
'@import.imported': nodeToCapture('@import.imported', nodeMap['@alias.member']!),
|
||||
'@import.source': nodeToCapture('@import.source', source),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drop the plain-variable group for container/import bindings — their
|
||||
// dedicated rules already bind the name (as Struct/Enum/Union or import).
|
||||
// The query already requires a `const`/`var` keyword, so statement
|
||||
// assignments (`x = 5;`, `_ = expr;` — same node type, no keyword) never
|
||||
// mint phantom locals; `isZigKeywordDeclaration` is the belt to that
|
||||
// brace should the rule ever be loosened.
|
||||
const variableAnchor = nodeMap['@declaration.variable'];
|
||||
if (variableAnchor !== undefined && isZigContainerOrImportBinding(variableAnchor)) {
|
||||
if (
|
||||
variableAnchor !== undefined &&
|
||||
(isZigContainerOrImportBinding(variableAnchor) ||
|
||||
aliasDeclIds.has(variableAnchor.id) ||
|
||||
!isZigKeywordDeclaration(variableAnchor))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -155,6 +282,25 @@ export function emitZigScopeCaptures(
|
|||
);
|
||||
}
|
||||
|
||||
// Mark containers returned by a generic type constructor so
|
||||
// `zigBindingScopeFor` hoists their name to the module scope (beside the
|
||||
// Function def of the same name) instead of the fn body it sits in.
|
||||
for (const kind of ['@declaration.struct', '@declaration.union', '@declaration.enum']) {
|
||||
const containerAnchor = nodeMap[kind];
|
||||
const nameNode = nodeMap['@declaration.name'];
|
||||
if (
|
||||
containerAnchor !== undefined &&
|
||||
nameNode !== undefined &&
|
||||
zigTypeConstructorOf(containerAnchor) !== null
|
||||
) {
|
||||
grouped['@declaration.type-constructor'] = syntheticCapture(
|
||||
'@declaration.type-constructor',
|
||||
nameNode,
|
||||
'true',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Relabel container-nested fns Function → Method (provider labelOverride
|
||||
// parity). The anchor capture name carries the kind, so rebuild it.
|
||||
const fnAnchor = nodeMap['@declaration.function'];
|
||||
|
|
@ -167,6 +313,39 @@ export function emitZigScopeCaptures(
|
|||
out.push(grouped);
|
||||
}
|
||||
|
||||
// A generic type constructor yields two module-scope defs of one name: the
|
||||
// Function `Stack` and the Struct `Stack` it returns. Import materialization
|
||||
// keeps the FIRST def of a name (finalize `indexExportsByName`), and query
|
||||
// order puts the fn (outer node) first — so `const Stack =
|
||||
// @import("x.zig").Stack;` bound the Function and `Stack(u8){}` typed
|
||||
// nothing. Emit the container def ahead of its constructor: the type is what
|
||||
// an importer instantiates, and the free call `Stack(u8)` still resolves
|
||||
// (a Struct is a valid call target — the constructor-reference path).
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
const group = out[i]!;
|
||||
if (group['@declaration.type-constructor'] === undefined) continue;
|
||||
const anchor =
|
||||
group['@declaration.struct'] ?? group['@declaration.union'] ?? group['@declaration.enum'];
|
||||
if (anchor === undefined) continue;
|
||||
for (let j = 0; j < i; j++) {
|
||||
const fn = out[j]!['@declaration.function'];
|
||||
if (fn === undefined) continue;
|
||||
if (
|
||||
fn.range.startLine < anchor.range.startLine ||
|
||||
(fn.range.startLine === anchor.range.startLine &&
|
||||
fn.range.startCol <= anchor.range.startCol)
|
||||
) {
|
||||
if (
|
||||
fn.range.endLine > anchor.range.endLine ||
|
||||
(fn.range.endLine === anchor.range.endLine && fn.range.endCol >= anchor.range.endCol)
|
||||
) {
|
||||
out.splice(j, 0, ...out.splice(i, 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push(...synthesizeCallableFlowCaptures(tree.rootNode, ZIG_CALLABLE_CAPTURE_OPTIONS));
|
||||
|
||||
return out;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@ export {
|
|||
emitZigScopeCaptures,
|
||||
isZigContainerMethod,
|
||||
isZigContainerOrImportBinding,
|
||||
isZigKeywordDeclaration,
|
||||
zigContainerName,
|
||||
zigImportRootOf,
|
||||
zigTypeConstructorOf,
|
||||
ZIG_CONTAINER_TYPES,
|
||||
} from './captures.js';
|
||||
export { interpretZigImport, interpretZigTypeBinding, normalizeZigTypeName } from './interpret.js';
|
||||
export {
|
||||
expandZigWildcardNames,
|
||||
zigArityCompatibility,
|
||||
zigBindingScopeFor,
|
||||
zigMergeBindings,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,32 @@ const stripQuotes = (s: string): string => s.replace(/^["']|["']$/g, '');
|
|||
* 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;
|
||||
if (source === undefined) return null;
|
||||
const targetRaw = stripQuotes(source);
|
||||
if (targetRaw.length === 0) return null;
|
||||
|
||||
// `pub usingnamespace @import("x.zig");` — every pub decl of the target
|
||||
// becomes a decl of this container. A wildcard, expanded by
|
||||
// `expandZigWildcardNames` in the scope resolver.
|
||||
if (captures['@import.wildcard'] !== undefined) {
|
||||
return { kind: 'wildcard', targetRaw };
|
||||
}
|
||||
|
||||
const name = captures['@import.name']?.text;
|
||||
if (name === undefined) return null;
|
||||
|
||||
// `const Foo = @import("x.zig").Foo;` — one member, under a name of the
|
||||
// importer's choosing (a rename when it differs: `const Alloc =
|
||||
// @import("std").mem;`). Same fact as TS `import { Foo } from './x'` /
|
||||
// `import { Foo as Bar }`.
|
||||
const imported = captures['@import.imported']?.text;
|
||||
if (imported !== undefined) {
|
||||
return imported === name
|
||||
? { kind: 'named', localName: name, importedName: imported, targetRaw }
|
||||
: { kind: 'alias', localName: name, importedName: imported, alias: name, targetRaw };
|
||||
}
|
||||
|
||||
return { kind: 'namespace', localName: name, importedName: name, targetRaw };
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +55,15 @@ export function normalizeZigTypeName(text: string): string {
|
|||
} while (t !== previous);
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
// Generic instantiation `List(u8)` / `std.ArrayList(u8)` → the type
|
||||
// constructor `List` / `std.ArrayList`: Zig spells a generic type as a
|
||||
// call, and the container def is registered under the function's name.
|
||||
// Builtins (`@This()`, `@TypeOf(x)`) keep their parentheses — they are
|
||||
// not constructor names and must not turn into `@This`.
|
||||
if (!t.startsWith('@')) {
|
||||
const paren = t.indexOf('(');
|
||||
if (paren > 0 && t.endsWith(')')) t = t.slice(0, paren).trim();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +82,14 @@ export function interpretZigTypeBinding(captures: CaptureMatch): ParsedTypeBindi
|
|||
source = isReceiver ? 'self' : 'parameter-annotation';
|
||||
} else if (captures['@type-binding.constructor'] !== undefined) {
|
||||
source = 'constructor-inferred';
|
||||
} else if (captures['@type-binding.call-return'] !== undefined) {
|
||||
// `var c = Counter.init();` — the call's receiver names the type when it
|
||||
// is a container (`Counter`, `mod.Counter`, `List(u8)`); a value receiver
|
||||
// (`std.mem`, `self.items`) simply finds no container and declines.
|
||||
source = 'constructor-inferred';
|
||||
} else if (captures['@type-binding.annotation'] !== undefined) {
|
||||
// `var x: T = undefined;` / `const x: T = .init(…);` — the declared type.
|
||||
source = 'annotation';
|
||||
}
|
||||
|
||||
return { boundName: name, rawTypeName: normalizeZigTypeName(type), source };
|
||||
|
|
|
|||
|
|
@ -65,6 +65,33 @@ const ZIG_SCOPE_QUERY = `
|
|||
(identifier) @declaration.name
|
||||
(opaque_declaration) @declaration.struct)
|
||||
|
||||
;; Declarations — generic type constructors. \`fn List(comptime T: type) type
|
||||
;; { return struct {…}; }\` is Zig's only spelling of a generic type; the
|
||||
;; returned container is anonymous in the grammar but every reader (and every
|
||||
;; caller: \`List(u8)\`) names it after the function. Anchor on the container
|
||||
;; so the def sits in its own class scope; the name binding is hoisted to the
|
||||
;; MODULE scope by \`zigBindingScopeFor\` (not the fn body, where the name
|
||||
;; would be invisible to callers) and coexists with the Function def of the
|
||||
;; same name — \`List\` really is both a callable and a type.
|
||||
((function_declaration
|
||||
name: (identifier) @declaration.name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(struct_declaration) @declaration.struct))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @declaration.name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(union_declaration) @declaration.union))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @declaration.name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(enum_declaration) @declaration.enum))))
|
||||
(#eq? @_ret "type"))
|
||||
|
||||
;; Declarations — container fields (struct fields, enum/union variants).
|
||||
;; The #not-eq? guard drops the MISSING placeholder identifier tree-sitter-zig
|
||||
;; recovers for an empty container body (see ZIG_QUERIES).
|
||||
|
|
@ -76,9 +103,15 @@ const ZIG_SCOPE_QUERY = `
|
|||
;; The \`.\` anchor pins the FIRST named child: without it the pattern also
|
||||
;; matched the initializer of \`const first = target;\`, minting a phantom
|
||||
;; local named \`target\` that shadowed the real callee for every later
|
||||
;; reference in the block.
|
||||
;; reference in the block. The literal keyword is load-bearing too:
|
||||
;; tree-sitter-zig 1.1.2 parses statement assignments (\`x = 5;\`, \`x += 1;\`,
|
||||
;; \`_ = expr;\`) as \`variable_declaration\` WITHOUT a keyword child, and
|
||||
;; without the keyword every assignment and every discard minted a phantom
|
||||
;; local (one \`_\` per statement).
|
||||
(variable_declaration
|
||||
. (identifier) @declaration.name) @declaration.variable
|
||||
"const" . (identifier) @declaration.name) @declaration.variable
|
||||
(variable_declaration
|
||||
"var" . (identifier) @declaration.name) @declaration.variable
|
||||
|
||||
;; Imports — const x = @import("...")
|
||||
(variable_declaration
|
||||
|
|
@ -88,6 +121,59 @@ const ZIG_SCOPE_QUERY = `
|
|||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
|
||||
;; Imports — const X = @import("...").X : a NAMED import of one member. The
|
||||
;; local name is whatever the user chose (\`const Alloc = @import("std").mem;\`
|
||||
;; is a rename), the imported name is the member. Deeper chains
|
||||
;; (\`@import("std").mem.Allocator\`) bind the innermost member — the file
|
||||
;; edge is what matters; a member-of-a-member resolves through the namespace
|
||||
;; later or not at all.
|
||||
(variable_declaration
|
||||
(identifier) @import.name
|
||||
(field_expression
|
||||
object: (builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
member: (identifier) @import.imported)
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
(variable_declaration
|
||||
(identifier) @import.name
|
||||
(field_expression
|
||||
object: (field_expression
|
||||
object: (builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source)))
|
||||
member: (identifier) @import.imported)
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
|
||||
;; Aliases of a namespace member — const Counter = counter.Counter; where
|
||||
;; \`counter\` is an @import binding of THIS file. The query cannot know which
|
||||
;; identifiers are import bindings, so it captures every one-level member
|
||||
;; alias and \`emitZigScopeCaptures\` promotes the ones whose object is a
|
||||
;; known @import to a named import (same fact as \`const Counter =
|
||||
;; @import("counter.zig").Counter;\`); the rest stay ordinary variables.
|
||||
;; Deeper chains (\`std.mem.Allocator\`) bind the innermost member off the
|
||||
;; leftmost namespace.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @alias.name
|
||||
(field_expression
|
||||
object: (identifier) @alias.namespace
|
||||
member: (identifier) @alias.member) .) @alias.statement
|
||||
(variable_declaration
|
||||
"const" . (identifier) @alias.name
|
||||
(field_expression
|
||||
object: (field_expression
|
||||
object: (identifier) @alias.namespace)
|
||||
member: (identifier) @alias.member) .) @alias.statement
|
||||
|
||||
;; Imports — pub usingnamespace @import("..."); : every pub decl of the target
|
||||
;; becomes a decl of this container (removed from the language in 0.15, still
|
||||
;; everywhere in 0.11–0.14 code). Modelled as a wildcard import.
|
||||
(using_namespace_declaration
|
||||
(builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.wildcard
|
||||
|
||||
;; Type bindings — parameter annotations (incl. self: *T receivers)
|
||||
(parameter
|
||||
name: (identifier) @type-binding.name
|
||||
|
|
@ -108,6 +194,41 @@ const ZIG_SCOPE_QUERY = `
|
|||
(struct_initializer
|
||||
(field_expression) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings — generic instantiation literal: const l = List(u8){ ... }.
|
||||
;; The callee is the type constructor; \`normalizeZigTypeName\` drops the
|
||||
;; comptime argument list so \`List(u8)\` looks up \`List\`.
|
||||
(variable_declaration
|
||||
(identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(call_expression) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings — declared type: var x: T = …; const x: T = .init(…);
|
||||
;; The annotation is the ONLY type source for \`= undefined\` and for 0.14+
|
||||
;; decl literals (\`.init\`, \`.empty\`), which are the idiomatic
|
||||
;; constructors in current std. Ranked below constructor inference by the
|
||||
;; shared resolver (source 'annotation'), so a literal on the right still
|
||||
;; wins when both are present.
|
||||
(variable_declaration
|
||||
. (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.annotation
|
||||
|
||||
;; Type bindings — call-return inference: var c = Counter.init(); (Rust's
|
||||
;; \`let x = Foo::new()\` twin). The receiver of the call is the type when it
|
||||
;; names a container (\`Counter\`, \`mod.Counter\`, \`List(u8)\`); when it is a
|
||||
;; value (\`std.mem\`, \`self.items\`) the lookup finds no container and
|
||||
;; declines, exactly as Rust's does. Keyword-gated: \`_ = e.top();\` is an
|
||||
;; assignment (same node type), not a binding of \`_\`.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
object: (_) @type-binding.type)) .) @type-binding.call-return
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
object: (_) @type-binding.type)) .) @type-binding.call-return
|
||||
|
||||
;; References — free calls: foo(...)
|
||||
(call_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolv
|
|||
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';
|
||||
import { expandZigWildcardNames, zigArityCompatibility, zigMergeBindings } from './index.js';
|
||||
|
||||
export const zigScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Zig,
|
||||
|
|
@ -34,6 +34,10 @@ export const zigScopeResolver: ScopeResolver = {
|
|||
(resolutionConfig as ZigBuildZonConfig | null | undefined) ?? null,
|
||||
),
|
||||
|
||||
// `pub usingnamespace @import("x.zig");` — target decls become local decls.
|
||||
expandsWildcardTo: (targetModuleScope, parsedFiles) =>
|
||||
expandZigWildcardNames(targetModuleScope, parsedFiles),
|
||||
|
||||
mergeBindings: zigMergeBindings,
|
||||
arityCompatibility: zigArityCompatibility,
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type {
|
|||
BindingRef,
|
||||
Callsite,
|
||||
CaptureMatch,
|
||||
ParsedFile,
|
||||
Scope,
|
||||
ScopeId,
|
||||
ScopeTree,
|
||||
|
|
@ -15,14 +16,50 @@ import type {
|
|||
export function zigBindingScopeFor(
|
||||
decl: CaptureMatch,
|
||||
innermost: Scope,
|
||||
_tree: ScopeTree,
|
||||
tree: ScopeTree,
|
||||
): ScopeId | null {
|
||||
if (decl['@type-binding.parameter'] !== undefined) {
|
||||
return innermost.id;
|
||||
}
|
||||
// A container returned by a generic type constructor (`fn List(comptime T:
|
||||
// type) type { return struct {…}; }`) is anchored on the container node,
|
||||
// whose innermost enclosing scope is the FUNCTION BODY. Its name is only
|
||||
// useful to callers (`List(u8){}`, `List(u8).init()`), so bind it in the
|
||||
// module scope beside the Function def of the same name — `List` really is
|
||||
// both a callable and a type. `zigTypeConstructorOf` in the walker decides
|
||||
// WHICH containers get this rule; the marker capture carries the verdict.
|
||||
if (decl['@declaration.type-constructor'] !== undefined) {
|
||||
let scope: Scope | undefined = innermost;
|
||||
while (scope !== undefined && scope.kind !== 'Module') scope = tree.getParent(scope.id);
|
||||
return scope?.id ?? null;
|
||||
}
|
||||
return null; // default auto-hoist for other bindings
|
||||
}
|
||||
|
||||
/** `pub usingnamespace @import("x.zig");` — every declaration of the target
|
||||
* module becomes a declaration of the importer. Enumerates the target's
|
||||
* module-level names for the finalize wildcard expansion (Dart pattern).
|
||||
* Zig visibility (`pub`) is not recorded on scope-side defs, so this
|
||||
* over-approximates to every top-level name; the structure phase's
|
||||
* `isExported` is the authoritative visibility. */
|
||||
export function expandZigWildcardNames(
|
||||
targetModuleScope: ScopeId,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): readonly string[] {
|
||||
const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
|
||||
if (target === undefined) return [];
|
||||
const seen = new Set<string>();
|
||||
const names: string[] = [];
|
||||
for (const def of target.localDefs) {
|
||||
const qn = def.qualifiedName;
|
||||
if (qn === undefined || qn.length === 0 || qn.includes('.')) continue; // top-level only
|
||||
if (seen.has(qn)) continue;
|
||||
seen.add(qn);
|
||||
names.push(qn);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Zig's receiver convention is a FIRST parameter named `self`; the
|
||||
* `self`-sourced typeBinding on the function scope carries its type.
|
||||
* Position is enforced upstream, not here: `interpretZigTypeBinding` only
|
||||
|
|
|
|||
|
|
@ -1,29 +1,23 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { MethodExtractionConfig, ParameterInfo } from '../../method-types.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { hasZigVisibilityKeyword } from '../../export-detection.js';
|
||||
import { ZIG_CONTAINER_TYPES } from '../../languages/zig/captures.js';
|
||||
import { hasZigPubKeyword } from '../../export-detection.js';
|
||||
import { ZIG_CONTAINER_TYPES, zigContainerName } from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig method extraction.
|
||||
*
|
||||
* tree-sitter-zig containers (struct/enum/union/opaque) 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.
|
||||
* tree-sitter-zig containers (struct/enum/union/opaque) are anonymous; the
|
||||
* binding name lives on the parent variable_declaration, or on the enclosing
|
||||
* generic type constructor (`fn List(comptime T: type) type { return struct
|
||||
* {…}; }`) — `zigContainerName` decides. 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 extractZigOwnerName = (node: SyntaxNode): string | undefined => zigContainerName(node);
|
||||
|
||||
const extractZigName = (node: SyntaxNode): string | undefined => {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
|
|
@ -120,7 +114,9 @@ export const zigMethodConfig: MethodExtractionConfig = {
|
|||
extractFunctionName: extractZigFunctionName,
|
||||
extractReturnType: extractZigReturnType,
|
||||
extractParameters: extractZigParameters,
|
||||
extractVisibility: (node) => (hasZigVisibilityKeyword(node) ? 'public' : 'private'),
|
||||
// `pub` only: `export fn` is C linkage, still private to other Zig files
|
||||
// (isExported carries the FFI fact — see export-detection.ts).
|
||||
extractVisibility: (node) => (hasZigPubKeyword(node) ? 'public' : 'private'),
|
||||
extractReceiverType: extractZigReceiverType,
|
||||
|
||||
isStatic(node) {
|
||||
|
|
|
|||
|
|
@ -2481,7 +2481,37 @@ export const ZIG_QUERIES = `
|
|||
(identifier) @name
|
||||
(opaque_declaration)) @definition.struct
|
||||
|
||||
; Container fields (struct fields, enum variants, union variants).
|
||||
; Generic type constructors: \`pub fn List(comptime T: type) type { return
|
||||
; struct { … }; }\` — Zig's only spelling of a generic type. The returned
|
||||
; container is anonymous in the grammar; the definition anchor is the
|
||||
; container node and its name is the enclosing function's (\`List\`), which
|
||||
; is what every caller writes (\`List(u8)\`). Only the direct \`return
|
||||
; <container>\` of a fn whose return type is \`type\` qualifies (see
|
||||
; \`zigTypeConstructorOf\`). The Function node \`List\` coexists: \`List\` is
|
||||
; both a callable and a type.
|
||||
((function_declaration
|
||||
name: (identifier) @name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(struct_declaration) @definition.struct))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(union_declaration) @definition.union))))
|
||||
(#eq? @_ret "type"))
|
||||
((function_declaration
|
||||
name: (identifier) @name
|
||||
type: (builtin_type) @_ret
|
||||
body: (block (expression_statement (return_expression
|
||||
(enum_declaration) @definition.enum))))
|
||||
(#eq? @_ret "type"))
|
||||
|
||||
; Container fields (struct fields, enum variants, union variants) — all are
|
||||
; \`container_field\` in the grammar and all become Property (C labels its
|
||||
; enumerators Const; Rust captures no variants; Zig's own vocabulary is
|
||||
; "field" for all three, so one label keeps the query honest).
|
||||
; #not-eq? guard: tree-sitter-zig 1.1.2 recovers an EMPTY container body
|
||||
; (\`struct {}\`, \`opaque {}\`) as a container_field whose identifier is a
|
||||
; zero-width MISSING placeholder — a parser artefact, not a field, and
|
||||
|
|
@ -2498,6 +2528,18 @@ export const ZIG_QUERIES = `
|
|||
(test_declaration
|
||||
(string) @name) @definition.function
|
||||
|
||||
; const / var bindings that are neither a container nor an @import (those two
|
||||
; are skipped by the provider's \`shouldSkipDefinitionCapture\` so the Struct /
|
||||
; import binding is the only node for that name). The literal keyword is
|
||||
; load-bearing: tree-sitter-zig 1.1.2 parses statement assignments (\`x = 5;\`,
|
||||
; \`x += 1;\`, \`_ = expr;\`) as \`variable_declaration\` WITHOUT a keyword
|
||||
; child, and a keyword-less rule would mint a Const per assignment and a
|
||||
; Variable named \`_\` per discard.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @name) @definition.const
|
||||
(variable_declaration
|
||||
"var" . (identifier) @name) @definition.variable
|
||||
|
||||
; @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).
|
||||
|
|
@ -2508,6 +2550,23 @@ export const ZIG_QUERIES = `
|
|||
(string) @import.source))
|
||||
(#eq? @builtin "@import")) @import
|
||||
|
||||
; const X = @import("path").X — the same file edge, one member deep.
|
||||
(variable_declaration
|
||||
(field_expression
|
||||
object: (builtin_function
|
||||
(builtin_identifier) @builtin
|
||||
(arguments
|
||||
(string) @import.source)))
|
||||
(#eq? @builtin "@import")) @import
|
||||
|
||||
; pub usingnamespace @import("path");
|
||||
(using_namespace_declaration
|
||||
(builtin_function
|
||||
(builtin_identifier) @builtin
|
||||
(arguments
|
||||
(string) @import.source))
|
||||
(#eq? @builtin "@import")) @import
|
||||
|
||||
; Free calls: foo(...)
|
||||
(call_expression
|
||||
function: (identifier) @call.name) @call
|
||||
|
|
|
|||
|
|
@ -798,6 +798,26 @@ const javaBinaryNameOfType = (node: SyntaxNode): string | undefined => {
|
|||
return enclosingName === undefined ? undefined : `${enclosingName}$${simpleName}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* For a container node that is the direct `return` value of a function whose
|
||||
* declared return type is the literal `type` (`fn List(comptime T: type) type
|
||||
* { return struct {…}; }`), the function's `name` node; undefined otherwise.
|
||||
* Language-agnostic by shape — today only tree-sitter-zig produces it.
|
||||
*/
|
||||
function typeConstructorNameNode(container: SyntaxNode): SyntaxNode | undefined {
|
||||
const ret = container.parent;
|
||||
if (ret?.type !== 'return_expression') return undefined;
|
||||
const stmt = ret.parent;
|
||||
if (stmt?.type !== 'expression_statement') return undefined;
|
||||
const block = stmt.parent;
|
||||
if (block?.type !== 'block') return undefined;
|
||||
const fn = block.parent;
|
||||
if (fn?.type !== 'function_declaration') return undefined;
|
||||
if (fn.childForFieldName?.('body')?.id !== block.id) return undefined;
|
||||
if (fn.childForFieldName?.('type')?.text !== 'type') return undefined;
|
||||
return fn.childForFieldName?.('name') ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authoritative Java local/anonymous type identity.
|
||||
*
|
||||
|
|
@ -1069,7 +1089,15 @@ export const findEnclosingClassInfo = (
|
|||
// the owner id and the node id agree by construction.
|
||||
(current.parent?.type === 'variable_declaration'
|
||||
? current.parent.namedChildren?.find((c: SyntaxNode) => c.type === 'identifier')
|
||||
: undefined);
|
||||
: undefined) ??
|
||||
// An anonymous container RETURNED by a type-constructor function —
|
||||
// `pub fn List(comptime T: type) type { return struct { … }; }`
|
||||
// (Zig's only spelling of a generic type). The container's name is
|
||||
// the function's, which is what the definition phase uses too
|
||||
// (`@name` on the fn identifier, anchor on the container), so the
|
||||
// owner id and the node id agree by construction. Only the literal
|
||||
// `return <container>` of a fn returning `type` qualifies.
|
||||
typeConstructorNameNode(current);
|
||||
if (nameNode) {
|
||||
let label = CONTAINER_TYPE_TO_LABEL[current.type] || 'Class';
|
||||
// Kotlin: class_declaration with an anonymous "interface" keyword child
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { VariableExtractionConfig, VariableVisibility } from '../../variable-types.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { hasZigVisibilityKeyword } from '../../export-detection.js';
|
||||
import { isZigContainerOrImportBinding } from '../../languages/zig/captures.js';
|
||||
import { hasZigPubKeyword } from '../../export-detection.js';
|
||||
import {
|
||||
isZigContainerOrImportBinding,
|
||||
isZigKeywordDeclaration,
|
||||
} from '../../languages/zig/captures.js';
|
||||
|
||||
/**
|
||||
* Zig variable extraction.
|
||||
* Zig variable extraction — enriches the Const / Variable nodes minted by
|
||||
* ZIG_QUERIES' `@definition.const` / `@definition.variable` rules.
|
||||
*
|
||||
* tree-sitter-zig uses a single `variable_declaration` node for both `const`
|
||||
* and `var` bindings. The keyword is exposed as an unnamed token child
|
||||
* and `var` bindings — AND for statement assignments (`x = 5;`), which carry
|
||||
* no keyword. 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
|
||||
|
|
@ -35,6 +40,9 @@ export const zigVariableConfig: VariableExtractionConfig = {
|
|||
|
||||
extractName(node) {
|
||||
if (isZigContainerOrImportBinding(node)) return undefined;
|
||||
// Statement assignments share the node type and have no keyword — not a
|
||||
// declaration (belt to the query's literal `"const"` / `"var"` brace).
|
||||
if (!isZigKeywordDeclaration(node)) return undefined;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'identifier') return child.text;
|
||||
|
|
@ -55,7 +63,8 @@ export const zigVariableConfig: VariableExtractionConfig = {
|
|||
},
|
||||
|
||||
extractVisibility(node): VariableVisibility {
|
||||
return hasZigVisibilityKeyword(node) ? 'public' : 'private';
|
||||
// `pub` only — `export var` is C linkage, private to other Zig files.
|
||||
return hasZigPubKeyword(node) ? 'public' : 'private';
|
||||
},
|
||||
|
||||
isConst(node) {
|
||||
|
|
|
|||
|
|
@ -173,3 +173,118 @@ describe.skipIf(!zigAvailable)('Zig export, opaque and test declarations (ffi.zi
|
|||
expect(fns.some((n) => n.startsWith('test@') || n === 'test')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* `zig-idioms`: the shapes real Zig is written in that `zig-basic` does not
|
||||
* exercise. Each case names the idiom and what breaks without the rule.
|
||||
*/
|
||||
describe.skipIf(!zigAvailable)('Zig idioms (zig-idioms fixture)', () => {
|
||||
let result: PipelineResult;
|
||||
let calls: string[];
|
||||
let imports: string[];
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'zig-idioms'), () => {});
|
||||
calls = edgeSet(getRelationships(result, 'CALLS'));
|
||||
imports = getRelationships(result, 'IMPORTS').map(
|
||||
(e) => `${path.basename(e.sourceFilePath)} → ${e.targetFilePath}`,
|
||||
);
|
||||
}, 90000);
|
||||
|
||||
it('mints Const / Variable nodes for `pub const` / `pub var` (incl. error sets and type aliases)', () => {
|
||||
// ZIG_QUERIES had no @definition.const / @definition.variable at all, so
|
||||
// `zigVariableConfig` never ran and `pub const VERSION`, error sets and
|
||||
// aliases like `const Allocator = std.mem.Allocator` were absent from
|
||||
// the graph.
|
||||
expect(getNodesByLabel(result, 'Const')).toEqual(
|
||||
expect.arrayContaining(['VERSION', 'Err', 'Allocator']),
|
||||
);
|
||||
expect(getNodesByLabel(result, 'Variable')).toContain('global_count');
|
||||
});
|
||||
|
||||
it('never mints a Const for a container or an @import binding, nor for a statement assignment', () => {
|
||||
// `const Counter = struct {…}` is the Struct node; `const counter =
|
||||
// @import(…)` is the import binding; `counter.global_count = 5;` and
|
||||
// `_ = counter.VERSION;` are assignments that tree-sitter-zig 1.1.2 parses
|
||||
// as keyword-less `variable_declaration`s.
|
||||
const consts = getNodesByLabel(result, 'Const');
|
||||
// (`const Counter = counter.Counter;` IS a Const node — an alias — but
|
||||
// the struct itself is not duplicated as one: exactly one `Counter` Const,
|
||||
// from main.zig.)
|
||||
expect(getNodesByLabelFull(result, 'Const').filter((n) => n.name === 'Counter')).toHaveLength(
|
||||
1,
|
||||
);
|
||||
expect(consts).not.toContain('counter');
|
||||
expect(consts).not.toContain('std');
|
||||
expect(consts).not.toContain('_');
|
||||
expect(getNodesByLabel(result, 'Variable')).not.toContain('_');
|
||||
});
|
||||
|
||||
it('types a receiver from a constructor CALL (`var a = Counter.init(); a.incr()`)', () => {
|
||||
expect(calls).toContain('main → incr');
|
||||
});
|
||||
|
||||
it('types a receiver from its ANNOTATION (`var b: Counter = undefined; b.twice()`, `const c: Counter = .init(); c.get()`)', () => {
|
||||
// The declared type is the ONLY type source for `= undefined` and for
|
||||
// 0.14+ decl literals (`.init`, `.empty`), which current std uses for
|
||||
// every container constructor.
|
||||
expect(calls).toContain('main → twice');
|
||||
expect(calls).toContain('main → get');
|
||||
});
|
||||
|
||||
it('follows an alias of a namespace member as a named import (`const Counter = counter.Counter;`)', () => {
|
||||
// Every receiver above is typed through the alias — none resolves if the
|
||||
// scope-side binding is a plain local shadowing the import (the graph
|
||||
// still carries the alias as a Const node in main.zig, which is what it is).
|
||||
expect(calls).toContain('main → get');
|
||||
expect(calls).toContain('main → init');
|
||||
});
|
||||
|
||||
it('owns a generic type constructor’s members and dispatches on its instantiations', () => {
|
||||
// `pub fn Stack(comptime T: type) type { return struct {…}; }` — the
|
||||
// returned container had no owner (methods hung off the File) and
|
||||
// `Stack(u8){}` / `Stack(u8).init()` / `: Stack(u16)` typed nothing.
|
||||
expect(getNodesByLabel(result, 'Struct')).toContain('Stack');
|
||||
expect(getNodesByLabel(result, 'Function')).toContain('Stack');
|
||||
expect(edgeSet(getRelationships(result, 'HAS_METHOD'))).toEqual(
|
||||
expect.arrayContaining(['Stack → push', 'Stack → top', 'Stack → clear']),
|
||||
);
|
||||
expect(edgeSet(getRelationships(result, 'HAS_PROPERTY'))).toContain('Stack → items');
|
||||
expect(calls).toEqual(expect.arrayContaining(['main → push', 'main → top', 'main → clear']));
|
||||
});
|
||||
|
||||
it('imports the file behind `const X = @import("x.zig").X` and `usingnamespace @import(...)`', () => {
|
||||
// Both forms lost the file-level IMPORTS edge: the rule needed
|
||||
// `builtin_function` as a DIRECT child of the declaration.
|
||||
expect(imports).toContain('main.zig → src/mixin.zig');
|
||||
// counter.zig is imported twice from main.zig (namespace + member); the
|
||||
// edge is deduped, so its presence proves at least one form resolved and
|
||||
// `Stack` (member form only) dispatching proves the other.
|
||||
expect(imports).toContain('main.zig → src/counter.zig');
|
||||
expect(calls).toContain('main → push');
|
||||
});
|
||||
|
||||
it('resolves a build.zig.zon path dep to the root its build.zig declares (src/root.zig)', () => {
|
||||
// `zig init` ≥ 0.12 lays libraries out as src/root.zig; the resolver only
|
||||
// knew src/<name>.zig and src/main.zig, so every such dep was unresolved.
|
||||
expect(imports).toContain('main.zig → libs/geo/src/root.zig');
|
||||
expect(calls).toContain('main → area');
|
||||
expect(calls).toContain('main → shift');
|
||||
});
|
||||
|
||||
it('still resolves the older src/<name>.zig convention when the dep has no build.zig', () => {
|
||||
expect(imports).toContain('main.zig → libs/oldlib/src/oldlib.zig');
|
||||
expect(calls).toContain('main → legacy');
|
||||
});
|
||||
|
||||
it('a re-assignment (`a = Counter.init();`) is not a declaration and does not shadow the typed binding', () => {
|
||||
// Guarded on the scope side by the literal `"const"` / `"var"` in the
|
||||
// query and on the structure side by `isZigKeywordDeclaration`.
|
||||
// main → incr resolves twice through the same binding (before and after
|
||||
// the re-assignment); an untyped phantom `a` would drop the second.
|
||||
expect(
|
||||
getRelationships(result, 'CALLS').filter((e) => e.source === 'main' && e.target === 'incr')
|
||||
.length,
|
||||
).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,8 +17,21 @@ import { createMethodExtractor } from '../../src/core/ingestion/method-extractor
|
|||
import { zigMethodConfig } from '../../src/core/ingestion/method-extractors/configs/zig.js';
|
||||
import { createVariableExtractor } from '../../src/core/ingestion/variable-extractors/generic.js';
|
||||
import { zigVariableConfig } from '../../src/core/ingestion/variable-extractors/configs/zig.js';
|
||||
import { emitZigScopeCaptures } from '../../src/core/ingestion/languages/zig/captures.js';
|
||||
import { interpretZigTypeBinding } from '../../src/core/ingestion/languages/zig/interpret.js';
|
||||
import {
|
||||
emitZigScopeCaptures,
|
||||
isZigContainerOrImportBinding,
|
||||
isZigKeywordDeclaration,
|
||||
zigContainerName,
|
||||
} from '../../src/core/ingestion/languages/zig/captures.js';
|
||||
import {
|
||||
interpretZigImport,
|
||||
interpretZigTypeBinding,
|
||||
normalizeZigTypeName,
|
||||
} from '../../src/core/ingestion/languages/zig/interpret.js';
|
||||
import { createFieldExtractor } from '../../src/core/ingestion/field-extractors/generic.js';
|
||||
import { zigFieldConfig } from '../../src/core/ingestion/field-extractors/configs/zig.js';
|
||||
import { zigProvider } from '../../src/core/ingestion/languages/zig.js';
|
||||
import { createSemanticModel } from '../../src/core/ingestion/model/semantic-model.js';
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
let Zig: unknown = null;
|
||||
|
|
@ -240,25 +253,40 @@ export const table: [4]u8 = .{ 0, 0, 0, 0 };
|
|||
);
|
||||
});
|
||||
|
||||
it('the method and variable extractors report `export` as public (one shared predicate)', () => {
|
||||
// The three visibility readers share `hasZigVisibilityKeyword`; a private
|
||||
// copy in one of them made `isExported` and `visibility` disagree.
|
||||
it('`export` is C linkage, not Zig visibility: isExported true, visibility private, `pub` public', () => {
|
||||
// Language reference: only `pub` declarations are reachable from another
|
||||
// file through `@import`; `export` puts the symbol in the object file for
|
||||
// C callers and leaves it PRIVATE to Zig code. The graph keeps both facts
|
||||
// in their own property: `isExported` (visible outside the compilation
|
||||
// unit — the FFI surface, same reading as C external linkage) and
|
||||
// `visibility` (the Zig-module fact). Reporting `export fn` as `public`
|
||||
// let the resolver connect a cross-file call Zig would reject.
|
||||
const root = parse(`
|
||||
const C = struct {
|
||||
export fn cb(self: *C) void { _ = self; }
|
||||
pub fn open(self: *C) void { _ = self; }
|
||||
fn hidden(self: *C) void { _ = self; }
|
||||
};
|
||||
export var counter: u32 = 0;
|
||||
pub var visible: u32 = 0;
|
||||
`).rootNode;
|
||||
const methods = createMethodExtractor(zigMethodConfig).extract(
|
||||
find(root, 'struct_declaration'),
|
||||
{ filePath: 'test.zig', language: SupportedLanguages.Zig },
|
||||
);
|
||||
expect(methods!.methods.find((m) => m.name === 'cb')!.visibility).toBe('public');
|
||||
const variable = createVariableExtractor(zigVariableConfig).extract(
|
||||
find(root, 'variable_declaration', 'export var'),
|
||||
{ filePath: 'test.zig', language: SupportedLanguages.Zig },
|
||||
const vis = (name: string) => methods!.methods.find((m) => m.name === name)!.visibility;
|
||||
expect(vis('cb')).toBe('private');
|
||||
expect(vis('open')).toBe('public');
|
||||
expect(vis('hidden')).toBe('private');
|
||||
expect(zigExportChecker(find(root, 'function_declaration', 'export fn cb'), 'cb')).toBe(true);
|
||||
const variables = createVariableExtractor(zigVariableConfig);
|
||||
const ctx = { filePath: 'test.zig', language: SupportedLanguages.Zig };
|
||||
expect(
|
||||
variables.extract(find(root, 'variable_declaration', 'export var'), ctx)!.visibility,
|
||||
).toBe('private');
|
||||
expect(variables.extract(find(root, 'variable_declaration', 'pub var'), ctx)!.visibility).toBe(
|
||||
'public',
|
||||
);
|
||||
expect(variable!.visibility).toBe('public');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -358,3 +386,228 @@ pub const H = opaque {
|
|||
expect(fields).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig declarations vs statement assignments (tree-sitter-zig 1.1.2 quirk)', () => {
|
||||
const src = `
|
||||
var count: u32 = 0;
|
||||
fn inc() void {
|
||||
count = 5;
|
||||
count += 1;
|
||||
_ = inc;
|
||||
const local = 1;
|
||||
var m: u32 = undefined;
|
||||
_ = local; _ = m;
|
||||
}
|
||||
`;
|
||||
|
||||
it('`x = 5;`, `x += 1;` and `_ = expr;` are keyword-less variable_declarations, not bindings', () => {
|
||||
// The grammar reuses `variable_declaration` for statement assignments; the
|
||||
// `const` / `var` keyword child is the only thing that tells them apart.
|
||||
// Without the gate every assignment minted a phantom local (one `_` per
|
||||
// discard) and, on the structure side, a Const/Variable node per statement.
|
||||
const root = parse(src).rootNode;
|
||||
const decls: SyntaxNode[] = [];
|
||||
const walk = (n: SyntaxNode): void => {
|
||||
if (n.type === 'variable_declaration') decls.push(n);
|
||||
n.children.forEach(walk);
|
||||
};
|
||||
walk(root);
|
||||
const verdicts = decls.map((d) => [d.text.split('\n')[0]!.trim(), isZigKeywordDeclaration(d)]);
|
||||
expect(verdicts).toEqual([
|
||||
['var count: u32 = 0;', true],
|
||||
['count = 5;', false],
|
||||
['count += 1;', false],
|
||||
['_ = inc;', false],
|
||||
['const local = 1;', true],
|
||||
['var m: u32 = undefined;', true],
|
||||
['_ = local;', false],
|
||||
['_ = m;', false],
|
||||
]);
|
||||
});
|
||||
|
||||
it('the variable extractor declines an assignment and the scope walker binds only real declarations', () => {
|
||||
const root = parse(src).rootNode;
|
||||
const extractor = createVariableExtractor(zigVariableConfig);
|
||||
const ctx = { filePath: 'x.zig', language: SupportedLanguages.Zig };
|
||||
expect(extractor.extract(find(root, 'variable_declaration', 'count = 5'), ctx)).toBeNull();
|
||||
expect(extractor.extract(find(root, 'variable_declaration', 'const local'), ctx)?.name).toBe(
|
||||
'local',
|
||||
);
|
||||
const bound = emitZigScopeCaptures(src, 'x.zig')
|
||||
.filter((m) => m['@declaration.variable'] !== undefined)
|
||||
.map((m) => m['@declaration.name']?.text);
|
||||
expect(bound).toEqual(['count', 'local', 'm']);
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig import forms', () => {
|
||||
it('a member alias off @import and a usingnamespace are import bindings, not variables', () => {
|
||||
// `const Foo = @import("x.zig").Foo;` is the single-symbol import Zig is
|
||||
// written with; treating it as a plain Const lost the file edge and left
|
||||
// `Foo{}` untyped. `pub usingnamespace @import(...)` has no name at all.
|
||||
const root = parse(`
|
||||
const std = @import("std");
|
||||
const Foo = @import("foo.zig").Foo;
|
||||
const Alloc = @import("std").mem.Allocator;
|
||||
const plain = 1;
|
||||
`).rootNode;
|
||||
expect(isZigContainerOrImportBinding(find(root, 'variable_declaration', 'const std'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isZigContainerOrImportBinding(find(root, 'variable_declaration', 'const Foo'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isZigContainerOrImportBinding(find(root, 'variable_declaration', 'const Alloc'))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isZigContainerOrImportBinding(find(root, 'variable_declaration', 'const plain'))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('interprets namespace, named, alias, wildcard and namespace-member forms', () => {
|
||||
const src = `
|
||||
const counter = @import("counter.zig");
|
||||
const Counter = counter.Counter;
|
||||
const Renamed = @import("counter.zig").Counter;
|
||||
const Same = @import("counter.zig").Same;
|
||||
const Deep = @import("std").mem.Allocator;
|
||||
pub usingnamespace @import("mixin.zig");
|
||||
const notAnImport = other.Thing;
|
||||
`;
|
||||
const imports = emitZigScopeCaptures(src, 'x.zig')
|
||||
.filter((m) => m['@import.source'] !== undefined)
|
||||
.map((m) => interpretZigImport(m));
|
||||
expect(imports).toEqual([
|
||||
{
|
||||
kind: 'namespace',
|
||||
localName: 'counter',
|
||||
importedName: 'counter',
|
||||
targetRaw: 'counter.zig',
|
||||
},
|
||||
// alias of a namespace member → promoted to a named import of that member
|
||||
{ kind: 'named', localName: 'Counter', importedName: 'Counter', targetRaw: 'counter.zig' },
|
||||
{
|
||||
kind: 'alias',
|
||||
localName: 'Renamed',
|
||||
importedName: 'Counter',
|
||||
alias: 'Renamed',
|
||||
targetRaw: 'counter.zig',
|
||||
},
|
||||
{ kind: 'named', localName: 'Same', importedName: 'Same', targetRaw: 'counter.zig' },
|
||||
{
|
||||
kind: 'alias',
|
||||
localName: 'Deep',
|
||||
importedName: 'Allocator',
|
||||
alias: 'Deep',
|
||||
targetRaw: 'std',
|
||||
},
|
||||
{ kind: 'wildcard', targetRaw: 'mixin.zig' },
|
||||
]);
|
||||
// `other` is not an @import binding of this file → stays a variable.
|
||||
const vars = emitZigScopeCaptures(src, 'x.zig')
|
||||
.filter((m) => m['@declaration.variable'] !== undefined)
|
||||
.map((m) => m['@declaration.name']?.text);
|
||||
expect(vars).toEqual(['notAnImport']);
|
||||
});
|
||||
|
||||
it('the provider skips the Const capture for container and @import bindings', () => {
|
||||
const root = parse(`
|
||||
const std = @import("std");
|
||||
const Foo = @import("foo.zig").Foo;
|
||||
const S = struct {};
|
||||
const n = 1;
|
||||
`).rootNode;
|
||||
const skip = (prefix: string) =>
|
||||
zigProvider.shouldSkipDefinitionCapture!(
|
||||
{ 'definition.const': find(root, 'variable_declaration', prefix) },
|
||||
'Const',
|
||||
);
|
||||
expect(skip('const std')).toBe(true);
|
||||
expect(skip('const Foo')).toBe(true);
|
||||
expect(skip('const S')).toBe(true);
|
||||
expect(skip('const n')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig generic type constructors', () => {
|
||||
const src = `
|
||||
pub fn Stack(comptime T: type) type {
|
||||
return struct {
|
||||
items: []T = &.{},
|
||||
pub fn push(self: *@This(), v: T) void { _ = self; _ = v; }
|
||||
};
|
||||
}
|
||||
fn notAType(comptime T: type) u32 {
|
||||
return struct { pub fn x() u32 { return 1; } }.x();
|
||||
}
|
||||
const Plain = struct { a: u8 };
|
||||
`;
|
||||
|
||||
it('names the container returned by a fn returning `type` after that fn; other anonymous containers stay nameless', () => {
|
||||
const root = parse(src).rootNode;
|
||||
const structs: SyntaxNode[] = [];
|
||||
const walk = (n: SyntaxNode): void => {
|
||||
if (n.type === 'struct_declaration') structs.push(n);
|
||||
n.children.forEach(walk);
|
||||
};
|
||||
walk(root);
|
||||
expect(structs.map((n) => zigContainerName(n))).toEqual(['Stack', undefined, 'Plain']);
|
||||
});
|
||||
|
||||
it('the method and field extractors own the generic container’s members under the fn name', () => {
|
||||
const root = parse(src).rootNode;
|
||||
const container = find(root, 'struct_declaration', 'struct {\n items');
|
||||
const ctx = { filePath: 'x.zig', language: SupportedLanguages.Zig };
|
||||
const methods = createMethodExtractor(zigMethodConfig).extract(container, ctx);
|
||||
expect(methods?.ownerName).toBe('Stack');
|
||||
expect(methods?.methods.map((m) => m.name)).toEqual(['push']);
|
||||
const fields = createFieldExtractor(zigFieldConfig).extract(container, {
|
||||
...ctx,
|
||||
typeEnv: {
|
||||
lookup: () => undefined,
|
||||
constructorBindings: [],
|
||||
fileScope: () => new Map(),
|
||||
allScopes: () => new Map(),
|
||||
constructorTypeMap: new Map(),
|
||||
} as unknown as import('../../src/core/ingestion/type-env.js').TypeEnvironment,
|
||||
symbolTable: createSemanticModel().symbols,
|
||||
});
|
||||
expect(fields?.ownerFqn).toBe('Stack');
|
||||
expect(fields?.fields.map((f) => f.name)).toEqual(['items']);
|
||||
});
|
||||
|
||||
it('normalizes generic instantiations to the constructor name and leaves builtins alone', () => {
|
||||
expect(normalizeZigTypeName('List(u8)')).toBe('List');
|
||||
expect(normalizeZigTypeName('*std.ArrayList(u8)')).toBe('std.ArrayList');
|
||||
expect(normalizeZigTypeName('?*const Stack(u16)')).toBe('Stack');
|
||||
expect(normalizeZigTypeName('@This()')).toBe('@This()');
|
||||
expect(normalizeZigTypeName('*const @This()')).toBe('@This()');
|
||||
expect(normalizeZigTypeName('[]const u8')).toBe('u8');
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig receiver typing sources', () => {
|
||||
it('annotation and call-return bindings type receivers; a discard is never a binding', () => {
|
||||
const src = `
|
||||
pub fn run() void {
|
||||
var a = Counter.init();
|
||||
var b: Counter = undefined;
|
||||
const c: Counter = .init();
|
||||
var d = counter.Counter.init();
|
||||
var e = Stack(u8).init();
|
||||
_ = e.top();
|
||||
}
|
||||
`;
|
||||
const bindings = emitZigScopeCaptures(src, 'x.zig')
|
||||
.filter((m) => m['@type-binding.name'] !== undefined)
|
||||
.map((m) => interpretZigTypeBinding(m));
|
||||
expect(bindings).toEqual([
|
||||
{ boundName: 'a', rawTypeName: 'Counter', source: 'constructor-inferred' },
|
||||
{ boundName: 'b', rawTypeName: 'Counter', source: 'annotation' },
|
||||
{ boundName: 'c', rawTypeName: 'Counter', source: 'annotation' },
|
||||
{ boundName: 'd', rawTypeName: 'counter.Counter', source: 'constructor-inferred' },
|
||||
{ boundName: 'e', rawTypeName: 'Stack', source: 'constructor-inferred' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue