mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(zig): export fn visibility, opaque containers, named test blocks, member ownership
Ports the parts of upstream PR #305 (closed, unmerged) that our Zig provider lacked, plus two gaps found while porting. - `export fn` / `export var` (C-ABI linkage, never `pub`) are exported; the pub/export predicate is now shared by the export checker and the method/variable extractors' visibility (`hasZigVisibilityKeyword`). - `const H = opaque { … }` is a Struct-labelled container (it may own methods, never fields) in both the structure queries and the scope query; ZIG_CONTAINER_TYPES is the single source for the extractor configs. - `test "name" { … }` blocks are Function nodes named by the string node WITH quotes, so `test "add"` beside `fn add` cannot merge onto Function:<file>:add; `test_declaration` joins FUNCTION_NODE_TYPES and the Zig method config names it in the enclosing-function walk, so calls inside a test attribute to the test. Anonymous `test {}` and decl-tests `test add {}` are scopes without a node (an empty-name hook result stops the walk instead of falling through to the identifier of the function under test). - Empty container bodies (`struct {}`, `opaque {}`) no longer mint a nameless Property: tree-sitter-zig 1.1.2 recovers them as a container_field with a MISSING identifier; #not-eq? guards in both queries and the field extractor drop it. - Owner walk (`findEnclosingClassInfo`): an anonymous container bound by the enclosing `variable_declaration` takes the binding identifier, same shape as the Go `type_spec` branch. Before this NO Zig member had an owner — zero HAS_METHOD / HAS_PROPERTY edges for Zig. Not ported from #305, deliberately: `builtInNames` (a bare-name call-site drop filter; `alloc`/`free`/`append`/`print` are the most common user method names in Zig and `std.*` receivers are already external via the import binding), `usingnamespace` (removed in Zig 0.15), `@cImport`, `build.zig` ignore, and the web-app changes.
This commit is contained in:
parent
889da858d1
commit
d02f09c53a
15 changed files with 420 additions and 48 deletions
|
|
@ -1,9 +1,10 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
* Zig containers (struct/enum/union) are anonymous in the grammar:
|
||||
* Zig containers (struct/enum/union/opaque) are anonymous in the grammar:
|
||||
*
|
||||
* const Point = struct { ... };
|
||||
*
|
||||
|
|
@ -24,12 +25,15 @@ const extractZigContainerType = (node: SyntaxNode): ClassLikeNodeLabel | undefin
|
|||
if (node.type === 'struct_declaration') return 'Struct';
|
||||
if (node.type === 'enum_declaration') return 'Enum';
|
||||
if (node.type === 'union_declaration') return 'Union';
|
||||
// `opaque {}` is a fieldless container that may own methods — Struct is the
|
||||
// closest class-like label (rationale in ZIG_QUERIES).
|
||||
if (node.type === 'opaque_declaration') return 'Struct';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const zigClassConfig: ClassExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
typeDeclarationNodes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
|
||||
typeDeclarationNodes: [...ZIG_CONTAINER_TYPES],
|
||||
extractName: extractZigContainerName,
|
||||
extractType: extractZigContainerType,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -243,14 +243,17 @@ export const rubyExportChecker: ExportChecker = (_node, _name) => true;
|
|||
/** Dart: public if no leading underscore (convention, same as Python). */
|
||||
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');
|
||||
|
||||
/** Zig declaration node types whose `pub` keyword child marks the symbol public. */
|
||||
/** Zig declaration node types whose `pub` / `export` keyword child marks the symbol public. */
|
||||
const ZIG_DECL_TYPES = new Set(['function_declaration', 'variable_declaration']);
|
||||
|
||||
/**
|
||||
* Zig: walk to the enclosing decl, scan its direct children for an unnamed `pub`
|
||||
* keyword token (tree-sitter-zig models `pub` as an anonymous keyword child of
|
||||
* function_declaration / variable_declaration). Container fields (struct/enum
|
||||
* variants) are public if their enclosing variable_declaration is public.
|
||||
* 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`.
|
||||
* Container fields (struct/enum variants) are public if their enclosing
|
||||
* variable_declaration is public.
|
||||
*
|
||||
* The walk stops at the FIRST declaration it reaches: a `fn` inside
|
||||
* `pub const T = struct { … }` carries its own `pub` (or not), independent of
|
||||
|
|
@ -260,14 +263,23 @@ const ZIG_DECL_TYPES = new Set(['function_declaration', 'variable_declaration'])
|
|||
export const zigExportChecker: ExportChecker = (node, _name) => {
|
||||
let current: SyntaxNode | null = node;
|
||||
while (current) {
|
||||
if (ZIG_DECL_TYPES.has(current.type)) {
|
||||
for (let i = 0; i < current.childCount; i++) {
|
||||
const child = current.child(i);
|
||||
if (child?.type === 'pub') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (ZIG_DECL_TYPES.has(current.type)) return hasZigVisibilityKeyword(current);
|
||||
current = current.parent;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
export function hasZigVisibilityKeyword(declNode: SyntaxNode): boolean {
|
||||
for (let i = 0; i < declNode.childCount; i++) {
|
||||
const child = declNode.child(i);
|
||||
if (child?.type === 'pub' || child?.type === 'export') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
* Zig containers (struct/enum/union) are anonymous in tree-sitter-zig; the
|
||||
* 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.
|
||||
*/
|
||||
const extractZigOwnerName = (node: SyntaxNode): string | undefined => {
|
||||
|
|
@ -24,7 +25,7 @@ const extractZigOwnerName = (node: SyntaxNode): string | undefined => {
|
|||
*/
|
||||
export const zigFieldConfig: FieldExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
typeDeclarationNodes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
|
||||
typeDeclarationNodes: [...ZIG_CONTAINER_TYPES],
|
||||
fieldNodeTypes: ['container_field'],
|
||||
bodyNodeTypes: [],
|
||||
defaultVisibility: 'public',
|
||||
|
|
@ -32,7 +33,11 @@ export const zigFieldConfig: FieldExtractionConfig = {
|
|||
|
||||
extractName(node) {
|
||||
const name = node.childForFieldName('name');
|
||||
return name?.text;
|
||||
// An empty container body (`struct {}`, `opaque {}`) is recovered by
|
||||
// tree-sitter-zig 1.1.2 as one container_field with a zero-width MISSING
|
||||
// identifier. Not a field — declining here keeps it out of the field map.
|
||||
if (name === null || name.text.length === 0) return undefined;
|
||||
return name.text;
|
||||
},
|
||||
|
||||
extractType(node) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* and no heritage hooks are provided (Zig queries never produce
|
||||
* `@heritage.*` captures).
|
||||
* - exportChecker: walks to the enclosing variable_declaration /
|
||||
* function_declaration and looks for a `pub` keyword child.
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ import { getTreeSitterBufferSize } from '../../constants.js';
|
|||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
|
||||
/** Zig container node types: `struct`, `enum`, `union` and the fieldless
|
||||
* `opaque` all bind through `const T = <container> {…}` and may own methods.
|
||||
* Single source for the class/field/method extractor configs. */
|
||||
export const ZIG_CONTAINER_TYPES: ReadonlySet<string> = new Set([
|
||||
'struct_declaration',
|
||||
'enum_declaration',
|
||||
'union_declaration',
|
||||
'opaque_declaration',
|
||||
]);
|
||||
|
||||
/** Is this variable_declaration a container binding (`const T = struct {…}`)
|
||||
|
|
@ -30,14 +34,21 @@ export function isZigContainerOrImportBinding(declNode: SyntaxNode): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** A `fn` nested in a struct/enum/union container is a method. Single
|
||||
/** 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
|
||||
* node label and the scope-side def label cannot drift apart. The loose
|
||||
* parameter shape matches what `labelOverride` receives. */
|
||||
* parameter shape matches what `labelOverride` receives.
|
||||
*
|
||||
* Only a `function_declaration` can be a method: a named `test "…" {}`
|
||||
* inside a container is also a Function definition, but the method
|
||||
* extractor does not know test blocks (no parameters, no `self`), so
|
||||
* relabelling it would mint a Method id the definition phase never
|
||||
* builds. Tests stay Functions wherever they sit. */
|
||||
export function isZigContainerMethod(
|
||||
captureNode: { readonly parent?: SyntaxNode | null } | null | undefined,
|
||||
captureNode: { readonly type?: string; readonly parent?: SyntaxNode | null } | null | undefined,
|
||||
): boolean {
|
||||
if (captureNode?.type !== undefined && captureNode.type !== 'function_declaration') return false;
|
||||
let ancestor = captureNode?.parent;
|
||||
while (ancestor) {
|
||||
if (ZIG_CONTAINER_TYPES.has(ancestor.type)) return true;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const _require = createRequire(import.meta.url);
|
|||
* `parser-loader.isLanguageAvailable` before any scope extraction runs.
|
||||
*
|
||||
* Zig specifics encoded here:
|
||||
* - Containers (struct/enum/union) are anonymous nodes bound by the
|
||||
* - Containers (struct/enum/union/opaque) are anonymous nodes bound by the
|
||||
* enclosing `variable_declaration`; declarations capture the binding
|
||||
* identifier from the wrapper.
|
||||
* - `@import` is a builtin call, not import-statement syntax; the
|
||||
|
|
@ -28,7 +28,9 @@ const ZIG_SCOPE_QUERY = `
|
|||
(struct_declaration) @scope.class
|
||||
(enum_declaration) @scope.class
|
||||
(union_declaration) @scope.class
|
||||
(opaque_declaration) @scope.class
|
||||
(function_declaration) @scope.function
|
||||
(test_declaration) @scope.function
|
||||
(block) @scope.block
|
||||
|
||||
;; Declarations — functions (relabeled @declaration.method inside containers
|
||||
|
|
@ -36,6 +38,12 @@ const ZIG_SCOPE_QUERY = `
|
|||
(function_declaration
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
;; Declarations — named tests. Same naming rule as ZIG_QUERIES: the string
|
||||
;; node WITH quotes, so the def joins the graph node and never collides with
|
||||
;; a same-named fn. Anonymous / decl-form tests are scopes without a def.
|
||||
(test_declaration
|
||||
(string) @declaration.name) @declaration.function
|
||||
|
||||
;; Declarations — containers. The binding name lives on the wrapper
|
||||
;; variable_declaration, but the ANCHOR is the container node itself so its
|
||||
;; range equals the @scope.class range: the extractor then attaches the def
|
||||
|
|
@ -51,10 +59,18 @@ const ZIG_SCOPE_QUERY = `
|
|||
(variable_declaration
|
||||
(identifier) @declaration.name
|
||||
(union_declaration) @declaration.union)
|
||||
;; opaque {} is a fieldless container that may own methods — Struct, as in
|
||||
;; ZIG_QUERIES (see the rationale there).
|
||||
(variable_declaration
|
||||
(identifier) @declaration.name
|
||||
(opaque_declaration) @declaration.struct)
|
||||
|
||||
;; Declarations — container fields (struct fields, enum/union variants)
|
||||
(container_field
|
||||
;; 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).
|
||||
((container_field
|
||||
name: (identifier) @declaration.name) @declaration.field
|
||||
(#not-eq? @declaration.name ""))
|
||||
|
||||
;; Declarations — const/var bindings (import/container groups filtered in TS).
|
||||
;; The \`.\` anchor pins the FIRST named child: without it the pattern also
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
* Zig method extraction.
|
||||
*
|
||||
* tree-sitter-zig containers (struct/enum/union) are anonymous; the binding
|
||||
* 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.
|
||||
*
|
||||
|
|
@ -75,14 +77,6 @@ const extractZigParameters = (node: SyntaxNode): ParameterInfo[] => {
|
|||
return params;
|
||||
};
|
||||
|
||||
const hasPubKeyword = (node: SyntaxNode): boolean => {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child?.type === 'pub') return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const extractZigReceiverType = (node: SyntaxNode): string | undefined => {
|
||||
const paramList = zigParameterList(node);
|
||||
if (!paramList) return undefined;
|
||||
|
|
@ -94,16 +88,39 @@ const extractZigReceiverType = (node: SyntaxNode): string | undefined => {
|
|||
return typeNode?.text?.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Names a `test_declaration` during the enclosing-function walk (parse-worker
|
||||
* `findEnclosingFunctionId`) with the SAME spelling ZIG_QUERIES captures as
|
||||
* `@name` — the string node with its quotes — so calls inside `test "x" {}`
|
||||
* attribute to the test's own Function node.
|
||||
*
|
||||
* Anonymous `test {}` and decl-tests `test add {}` are not graph nodes. They
|
||||
* return `''`, not `null`: `null` falls through to `genericFuncName`, whose
|
||||
* first-identifier scan would name `test add {}` "add" — the REAL `fn add`'s
|
||||
* id — and hang the test body's calls on it. The empty name ends the walk at
|
||||
* this node and lets the caller fall back to the File.
|
||||
*/
|
||||
const extractZigFunctionName = (
|
||||
node: SyntaxNode,
|
||||
): { funcName: string | null; label: 'Function' } | null => {
|
||||
if (node.type !== 'test_declaration') return null;
|
||||
const nameString = node.namedChildren.find(
|
||||
(child): child is SyntaxNode => child?.type === 'string',
|
||||
);
|
||||
return { funcName: nameString?.text ?? '', label: 'Function' };
|
||||
};
|
||||
|
||||
export const zigMethodConfig: MethodExtractionConfig = {
|
||||
language: SupportedLanguages.Zig,
|
||||
typeDeclarationNodes: ['struct_declaration', 'enum_declaration', 'union_declaration'],
|
||||
typeDeclarationNodes: [...ZIG_CONTAINER_TYPES],
|
||||
methodNodeTypes: ['function_declaration'],
|
||||
bodyNodeTypes: [],
|
||||
extractOwnerName: extractZigOwnerName,
|
||||
extractName: extractZigName,
|
||||
extractFunctionName: extractZigFunctionName,
|
||||
extractReturnType: extractZigReturnType,
|
||||
extractParameters: extractZigParameters,
|
||||
extractVisibility: (node) => (hasPubKeyword(node) ? 'public' : 'private'),
|
||||
extractVisibility: (node) => (hasZigVisibilityKeyword(node) ? 'public' : 'private'),
|
||||
extractReceiverType: extractZigReceiverType,
|
||||
|
||||
isStatic(node) {
|
||||
|
|
|
|||
|
|
@ -2472,9 +2472,31 @@ export const ZIG_QUERIES = `
|
|||
(identifier) @name
|
||||
(union_declaration)) @definition.union
|
||||
|
||||
; Opaque: const Handle = opaque { ... } — the FFI handle type. It is a
|
||||
; container (it may declare methods, never fields), so it is labelled Struct:
|
||||
; the owner of a HAS_METHOD edge must be class-like, and there is no closer
|
||||
; label. It is NOT a TypeAlias — an opaque type is a distinct nominal type,
|
||||
; deliberately incompatible with whatever it wraps.
|
||||
(variable_declaration
|
||||
(identifier) @name
|
||||
(opaque_declaration)) @definition.struct
|
||||
|
||||
; Container fields (struct fields, enum variants, union variants).
|
||||
(container_field
|
||||
; #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
|
||||
; without the guard it minted a Property with an empty name.
|
||||
((container_field
|
||||
name: (identifier) @name) @definition.property
|
||||
(#not-eq? @name ""))
|
||||
|
||||
; Named tests: test "description" { ... }. The name is the string node WITH
|
||||
; its quotes, so \`test "add"\` next to \`fn add\` (the idiomatic layout) does
|
||||
; not collide on Function:<file>:add. Anonymous \`test {}\` and decl-tests
|
||||
; \`test add {}\` have no name of their own and are not graph nodes; their
|
||||
; bodies' calls attribute to the File.
|
||||
(test_declaration
|
||||
(string) @name) @definition.function
|
||||
|
||||
; @import("path") — capture the string argument as @import.source.
|
||||
; The #eq? predicate restricts the match to the @import builtin (other
|
||||
|
|
|
|||
|
|
@ -321,6 +321,9 @@ export const FUNCTION_NODE_TYPES = new Set([
|
|||
// Dart
|
||||
'function_signature',
|
||||
'method_signature',
|
||||
// Zig: `test "…" { }` bodies are callable scopes — calls inside attribute
|
||||
// to the test, not the file. Named via methodExtractor.extractFunctionName.
|
||||
'test_declaration',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -372,6 +375,7 @@ export const CLASS_CONTAINER_TYPES = new Set([
|
|||
'interface_type',
|
||||
// Zig
|
||||
'union_declaration',
|
||||
'opaque_declaration',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -440,10 +444,12 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
|
|||
companion_object: 'Class',
|
||||
struct_type: 'Struct',
|
||||
interface_type: 'Interface',
|
||||
// Zig: tagged and untagged unions are class-like containers.
|
||||
// `struct_declaration` and `enum_declaration` are already present
|
||||
// (Dart / generic).
|
||||
// Zig: tagged and untagged unions are class-like containers, and so is
|
||||
// the fieldless `opaque {}` (may own methods; labelled Struct, see
|
||||
// ZIG_QUERIES). `struct_declaration` and `enum_declaration` are already
|
||||
// present (Dart / generic).
|
||||
union_declaration: 'Union',
|
||||
opaque_declaration: 'Struct',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1051,7 +1057,19 @@ export const findEnclosingClassInfo = (
|
|||
c.type === 'identifier' ||
|
||||
c.type === 'name' ||
|
||||
c.type === 'constant',
|
||||
);
|
||||
) ??
|
||||
// An ANONYMOUS container bound by the enclosing declaration —
|
||||
// `const Point = struct { … }` (tree-sitter-zig: struct/enum/union/
|
||||
// opaque nodes carry no name; the binding identifier is the first
|
||||
// named child of the parent `variable_declaration`). Same shape as the
|
||||
// Go `type_spec` branch above: the name lives one level up. Without it
|
||||
// the walk climbed past every Zig container and no member ever got a
|
||||
// HAS_METHOD / HAS_PROPERTY owner. The definition phase names the
|
||||
// container node from the same binding (`@name` on the wrapper), so
|
||||
// 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);
|
||||
if (nameNode) {
|
||||
let label = CONTAINER_TYPE_TO_LABEL[current.type] || 'Class';
|
||||
// Kotlin: class_declaration with an anonymous "interface" keyword child
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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';
|
||||
|
||||
/**
|
||||
|
|
@ -18,14 +19,6 @@ import { isZigContainerOrImportBinding } from '../../languages/zig/captures.js';
|
|||
* broad `expression_statement` config for the same pattern).
|
||||
*/
|
||||
|
||||
const hasPubKeyword = (node: SyntaxNode): boolean => {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child?.type === 'pub') return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isVarKeyword = (node: SyntaxNode): boolean => {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
|
|
@ -62,7 +55,7 @@ export const zigVariableConfig: VariableExtractionConfig = {
|
|||
},
|
||||
|
||||
extractVisibility(node): VariableVisibility {
|
||||
return hasPubKeyword(node) ? 'public' : 'private';
|
||||
return hasZigVisibilityKeyword(node) ? 'public' : 'private';
|
||||
},
|
||||
|
||||
isConst(node) {
|
||||
|
|
|
|||
36
gitnexus/test/fixtures/lang-resolution/zig-basic/src/ffi.zig
vendored
Normal file
36
gitnexus/test/fixtures/lang-resolution/zig-basic/src/ffi.zig
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
const std = @import("std");
|
||||
const main_mod = @import("./main.zig");
|
||||
|
||||
// C-ABI export: no `pub`, still the most externally visible symbol in the file.
|
||||
export fn c_add(a: i32, b: i32) i32 {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Fieldless container: FFI handle type. May own methods, never fields.
|
||||
pub const Handle = opaque {
|
||||
pub fn close(self: *Handle) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
// Empty container body — tree-sitter-zig recovers it with a MISSING
|
||||
// placeholder field; it must not become a nameless Property.
|
||||
pub const Empty = struct {};
|
||||
|
||||
pub fn release(h: *Handle) void {
|
||||
h.close();
|
||||
}
|
||||
|
||||
test "c_add adds" {
|
||||
_ = c_add(1, 2);
|
||||
}
|
||||
|
||||
// The idiomatic layout: a test named after the function it exercises.
|
||||
test "release" {
|
||||
var h: *Handle = undefined;
|
||||
release(h);
|
||||
}
|
||||
|
||||
test {
|
||||
_ = c_add(3, 4);
|
||||
}
|
||||
19
gitnexus/test/fixtures/sample-code/simple.zig
vendored
19
gitnexus/test/fixtures/sample-code/simple.zig
vendored
|
|
@ -28,3 +28,22 @@ pub fn main() void {
|
|||
_ = p.distance(Point{ .x = 3, .y = 4 });
|
||||
_ = add(1, 2);
|
||||
}
|
||||
|
||||
// C-ABI export (no `pub`), an opaque FFI handle, and test blocks.
|
||||
export fn c_add(a: i32, b: i32) i32 {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
pub const Handle = opaque {
|
||||
pub fn close(self: *Handle) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
test "add works" {
|
||||
_ = add(1, 2);
|
||||
}
|
||||
|
||||
test {
|
||||
_ = c_add(1, 2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
edgeSet,
|
||||
FIXTURES,
|
||||
getNodesByLabel,
|
||||
getNodesByLabelFull,
|
||||
getRelationships,
|
||||
runPipelineFromRepo,
|
||||
type PipelineResult,
|
||||
|
|
@ -104,3 +105,71 @@ describe.skipIf(!zigAvailable)('Zig scope captures — variable bindings', () =>
|
|||
expect(variableNames).toEqual(['h']);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!zigAvailable)('Zig export, opaque and test declarations (ffi.zig)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'zig-basic'), () => {});
|
||||
}, 60000);
|
||||
|
||||
it('marks `export fn` (C-ABI, no `pub`) as exported', () => {
|
||||
// `export` is the strongest visibility Zig has — FFI entry points are
|
||||
// declared this way and never carry `pub`. A `pub`-only checker left
|
||||
// every C-ABI symbol private in the graph.
|
||||
const cAdd = getNodesByLabelFull(result, 'Function').find((n) => n.name === 'c_add');
|
||||
expect(cAdd).toBeDefined();
|
||||
expect(cAdd!.properties.isExported).toBe(true);
|
||||
});
|
||||
|
||||
it('models `opaque {}` as a Struct that owns its methods', () => {
|
||||
expect(getNodesByLabel(result, 'Struct')).toContain('Handle');
|
||||
expect(getNodesByLabel(result, 'Method')).toContain('close');
|
||||
expect(edgeSet(getRelationships(result, 'HAS_METHOD'))).toContain('Handle → close');
|
||||
});
|
||||
|
||||
it('owns container members through the binding name (HAS_METHOD / HAS_PROPERTY)', () => {
|
||||
// tree-sitter-zig containers are anonymous; the owner walk used to climb
|
||||
// past them and NO Zig member ever got an owner edge, so `context(Pioneer)`
|
||||
// listed no methods and the struct's fields dangled off the File.
|
||||
expect(edgeSet(getRelationships(result, 'HAS_METHOD'))).toEqual(
|
||||
expect.arrayContaining(['Pioneer → tick', 'Pioneer → reset', 'Tag → isEnergy']),
|
||||
);
|
||||
expect(edgeSet(getRelationships(result, 'HAS_PROPERTY'))).toEqual(
|
||||
expect.arrayContaining(['Pioneer → energy', 'State → idle', 'Tag → energy']),
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches a method call on an opaque receiver (release → close)', () => {
|
||||
expect(edgeSet(getRelationships(result, 'CALLS'))).toContain('release → close');
|
||||
});
|
||||
|
||||
it('never mints a nameless Property for an empty container body', () => {
|
||||
// tree-sitter-zig 1.1.2 recovers `struct {}` / `opaque {}` as one
|
||||
// container_field with a zero-width MISSING identifier.
|
||||
expect(getNodesByLabel(result, 'Struct')).toContain('Empty');
|
||||
expect(getNodesByLabel(result, 'Property')).not.toContain('');
|
||||
});
|
||||
|
||||
it('captures named tests as Functions, quoted, so `test "release"` and `fn release` stay distinct nodes', () => {
|
||||
const fns = getNodesByLabel(result, 'Function');
|
||||
expect(fns).toContain('"c_add adds"');
|
||||
expect(fns).toContain('"release"');
|
||||
// Both must exist as separate nodes — an unquoted test name would have
|
||||
// merged onto Function:<file>:release and fabricated a self-call.
|
||||
expect(fns.filter((n) => n === 'release')).toHaveLength(1);
|
||||
expect(fns.filter((n) => n === '"release"')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('attributes calls inside a named test to the test node, not the file', () => {
|
||||
const calls = edgeSet(getRelationships(result, 'CALLS'));
|
||||
expect(calls).toContain('"c_add adds" → c_add');
|
||||
expect(calls).toContain('"release" → release');
|
||||
expect(calls).not.toContain('release → release');
|
||||
});
|
||||
|
||||
it('does not create a graph node for an anonymous `test {}`', () => {
|
||||
const fns = getNodesByLabel(result, 'Function');
|
||||
expect(fns.some((n) => n.startsWith('test@') || n === 'test')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -713,6 +713,17 @@ describe('Tree-sitter multi-language parsing', () => {
|
|||
expect(defTypes).toContain('definition.struct');
|
||||
expect(defTypes).toContain('definition.enum');
|
||||
|
||||
// `opaque {}` is a Struct-labelled container; a named `test "…"` block
|
||||
// is a Function whose @name is the string node WITH quotes (so it never
|
||||
// collides with a same-named fn); an anonymous `test {}` is not a def.
|
||||
const named = defs.map((d) => `${d.type}:${d.name}`);
|
||||
expect(named).toContain('definition.struct:Handle');
|
||||
expect(named).toContain('definition.function:"add works"');
|
||||
expect(named.filter((n) => n.startsWith('definition.function:')).length).toBe(
|
||||
['add', 'private_helper', 'init', 'distance', 'main', 'c_add', 'close', '"add works"']
|
||||
.length,
|
||||
);
|
||||
|
||||
// `const std = @import("std");` must yield an import.source capture —
|
||||
// without this assertion a query change that drops Zig import matching
|
||||
// would pass this test unchanged.
|
||||
|
|
|
|||
|
|
@ -219,3 +219,142 @@ describeZig('Zig scope captures — receiver is the FIRST parameter named self',
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig `export` (C-ABI) visibility', () => {
|
||||
const src = `
|
||||
export fn c_add(a: i32, b: i32) i32 { return a + b; }
|
||||
fn hidden() void {}
|
||||
export const table: [4]u8 = .{ 0, 0, 0, 0 };
|
||||
`;
|
||||
|
||||
it('zigExportChecker treats `export fn` as exported without `pub`', () => {
|
||||
// `export` is C-ABI linkage — the FFI entry point form — and never
|
||||
// carries `pub`. A `pub`-only check reported every C-ABI symbol private.
|
||||
const root = parse(src).rootNode;
|
||||
expect(zigExportChecker(find(root, 'function_declaration', 'export fn c_add'), 'c_add')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(zigExportChecker(find(root, 'function_declaration', 'fn hidden'), 'hidden')).toBe(false);
|
||||
expect(zigExportChecker(find(root, 'variable_declaration', 'export const'), 'table')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
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.
|
||||
const root = parse(`
|
||||
const C = struct {
|
||||
export fn cb(self: *C) void { _ = self; }
|
||||
};
|
||||
export var counter: 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 },
|
||||
);
|
||||
expect(variable!.visibility).toBe('public');
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig test declarations', () => {
|
||||
const extractor = createMethodExtractor(zigMethodConfig);
|
||||
const src = `
|
||||
fn add(a: i32, b: i32) i32 { return a + b; }
|
||||
test "add works" { _ = add(1, 2); }
|
||||
test { _ = add(3, 4); }
|
||||
test add { _ = add(5, 6); }
|
||||
`;
|
||||
|
||||
it('names a `test "…"` block by its string node, quotes included, in the enclosing-function walk', () => {
|
||||
// Must be byte-equal to the `@name` capture in ZIG_QUERIES (the string
|
||||
// node) so calls inside attribute to the test's own node — and the quotes
|
||||
// are what keep `test "add"` and `fn add` from sharing Function:<file>:add.
|
||||
const root = parse(src).rootNode;
|
||||
const named = extractor.extractFunctionName!(
|
||||
find(root, 'test_declaration', 'test "add works"'),
|
||||
);
|
||||
expect(named).toEqual({ funcName: '"add works"', label: 'Function' });
|
||||
});
|
||||
|
||||
it('returns an EMPTY name — never null — for anonymous and decl-form tests', () => {
|
||||
// `null` would fall through to `genericFuncName`, whose first-identifier
|
||||
// scan names `test add {}` "add": the REAL `fn add`'s id, so the test
|
||||
// body's calls would hang on the function under test. `''` stops the walk
|
||||
// here and lets the caller fall back to the File.
|
||||
const root = parse(src).rootNode;
|
||||
expect(extractor.extractFunctionName!(find(root, 'test_declaration', 'test {'))).toEqual({
|
||||
funcName: '',
|
||||
label: 'Function',
|
||||
});
|
||||
expect(extractor.extractFunctionName!(find(root, 'test_declaration', 'test add'))).toEqual({
|
||||
funcName: '',
|
||||
label: 'Function',
|
||||
});
|
||||
});
|
||||
|
||||
it('declines (null) for anything that is not a test_declaration', () => {
|
||||
const root = parse(src).rootNode;
|
||||
expect(extractor.extractFunctionName!(find(root, 'function_declaration'))).toBeNull();
|
||||
});
|
||||
|
||||
it('scope captures: a named test is a Function scope with a matching def; anonymous tests are scopes only', () => {
|
||||
const matches = emitZigScopeCaptures(src, 'test.zig');
|
||||
const fnScopes = matches.filter((m) => m['@scope.function'] !== undefined);
|
||||
// fn add + 3 test blocks
|
||||
expect(fnScopes).toHaveLength(4);
|
||||
const fnDefs = matches
|
||||
.filter((m) => m['@declaration.function'] !== undefined)
|
||||
.map((m) => m['@declaration.name']!.text);
|
||||
expect(fnDefs).toEqual(['add', '"add works"']);
|
||||
});
|
||||
|
||||
it('a test inside a container stays a Function — the method extractor cannot describe it', () => {
|
||||
const src2 = `
|
||||
const S = struct {
|
||||
fn m(self: S) void { _ = self; }
|
||||
test "S works" { _ = S{}; }
|
||||
};
|
||||
`;
|
||||
const labels = emitZigScopeCaptures(src2, 'test.zig')
|
||||
.filter(
|
||||
(m) => m['@declaration.function'] !== undefined || m['@declaration.method'] !== undefined,
|
||||
)
|
||||
.map((m) => (m['@declaration.method'] !== undefined ? 'method' : 'function'));
|
||||
expect(labels).toEqual(['method', 'function']);
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig opaque and empty containers', () => {
|
||||
it('captures `const H = opaque { … }` as a Struct-labelled class scope owning its methods', () => {
|
||||
const src = `
|
||||
pub const H = opaque {
|
||||
pub fn close(self: *H) void { _ = self; }
|
||||
};
|
||||
`;
|
||||
const matches = emitZigScopeCaptures(src, 'test.zig');
|
||||
const struct = matches.find((m) => m['@declaration.struct'] !== undefined);
|
||||
expect(struct?.['@declaration.name']?.text).toBe('H');
|
||||
expect(matches.some((m) => m['@declaration.method'] !== undefined)).toBe(true);
|
||||
// No stray plain-variable binding for the container wrapper.
|
||||
expect(
|
||||
matches.some(
|
||||
(m) => m['@declaration.variable'] !== undefined && m['@declaration.name']?.text === 'H',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('does not emit a nameless field for an empty container body', () => {
|
||||
// tree-sitter-zig 1.1.2 recovers `struct {}` / `opaque {}` as a
|
||||
// container_field whose identifier is a zero-width MISSING node.
|
||||
const fields = emitZigScopeCaptures('const E = struct {};\nconst O = opaque {};\n', 'test.zig')
|
||||
.filter((m) => m['@declaration.field'] !== undefined)
|
||||
.map((m) => m['@declaration.name']!.text);
|
||||
expect(fields).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue