mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(zig): import every @import in expression position; resolve @import("x").f()
Both query sets only saw `@import` as the value of a const/var or under
`usingnamespace`, so an @import in any other position produced no file
edge: Lightpanda's `pub const Interfaces = .{ @import("a.zig"), … }`
registration table (288 modules), call arguments
(`CounterEnum("size", @import("ArenaPool.zig").BucketSize)`), comparison
operands (`JsApi == @import("x.zig").JsApi`) and member-call receivers
(`try @import("dump.zig").root(...)`) — 417 of 3,401 in-repo import pairs
had no IMPORTS edge, and the 80 inline-receiver calls resolved 0 times.
Scope query: a catch-all `@import.inline` rule matches every `@import`
builtin; `emitZigScopeCaptures` drops the ones a binding rule (or the
keyword-less side-effect rule) already claimed (by string-node id) so a
bound import is never doubled, emits the rest as side-effect imports once
per distinct source per file, and binds a member-call receiver as a
namespace import whose local name is the builtin's own text — the
`@reference.receiver` text on that call is identical, so the shared
namespace-receiver lookup (Case 1) resolves the member in the imported
module.
ZIG_QUERIES: the three variable_declaration/usingnamespace-anchored
`@import` rules collapse into the same single builtin rule (the structure
phase only skips import matches; one match per builtin keeps
tree-sitter-languages' exact-capture assertion intact).
Lightpanda corpus (zig-corpus-check, before → after): IMPORTS 3014 → 3426,
in-repo pairs missing 417 → 5 (4 under a default-ignored `cache/` dir, 1 a
commented-out import the census regex counts), `@import(..).f()` 0/80 →
72/80 (the 8 left are `@import("root")` and non-import builtins the census
mislabels), CALLS 13885 → 13959; every other line unchanged.
This commit is contained in:
parent
2a0cfe6726
commit
606fb07612
10 changed files with 247 additions and 26 deletions
|
|
@ -31,6 +31,19 @@ export function zigImportRootOf(value: SyntaxNode | null): SyntaxNode | null {
|
|||
return isZigImportBuiltin(cur) ? cur : null;
|
||||
}
|
||||
|
||||
/** Is this `@import(…)` builtin the receiver of a member call —
|
||||
* `@import("dump.zig").root(...)` — i.e. the `object` of a `field_expression`
|
||||
* that is the `function` of a `call_expression`? A deeper chain
|
||||
* (`@import("x.zig").Foo.init()`) is not: its receiver is `….Foo`, and the
|
||||
* builtin is only the module the chain starts from. */
|
||||
export function isZigInlineImportReceiver(importNode: SyntaxNode): boolean {
|
||||
const field = importNode.parent;
|
||||
if (field?.type !== 'field_expression') return false;
|
||||
if (field.childForFieldName('object')?.id !== importNode.id) return false;
|
||||
const call = field.parent;
|
||||
return call?.type === 'call_expression' && call.childForFieldName('function')?.id === field.id;
|
||||
}
|
||||
|
||||
/** Is this variable_declaration a container binding (`const T = struct {…}`)
|
||||
* or an import binding (`const x = @import("…")`, `const X = @import("…").X`)?
|
||||
* Those groups are emitted by their dedicated query rules; the plain
|
||||
|
|
@ -197,11 +210,27 @@ export function emitZigScopeCaptures(
|
|||
// would outrank the import binding it stands for).
|
||||
const importSources = new Map<string, SyntaxNode>();
|
||||
const aliasDeclIds = new Set<number>();
|
||||
// The `@import(…)` string nodes a BINDING rule (or the keyword-less
|
||||
// side-effect rule) matched, by node id, plus their texts. The catch-all
|
||||
// `@import.inline` rule matches those same builtins again; the id set
|
||||
// keeps a bound import from being doubled, the text set keeps a second
|
||||
// spelling of an already-imported file from adding a redundant edge.
|
||||
const claimedImportSourceIds = new Set<number>();
|
||||
const importedSourceTexts = new Set<string>();
|
||||
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');
|
||||
const importStmt = byName.get('import.statement');
|
||||
if (
|
||||
importSource !== undefined &&
|
||||
(importStmt !== undefined ||
|
||||
byName.get('import.side-effect') !== undefined ||
|
||||
byName.get('import.wildcard') !== undefined)
|
||||
) {
|
||||
claimedImportSourceIds.add(importSource.id);
|
||||
importedSourceTexts.add(importSource.text);
|
||||
}
|
||||
if (
|
||||
importName !== undefined &&
|
||||
importSource !== undefined &&
|
||||
|
|
@ -250,6 +279,43 @@ export function emitZigScopeCaptures(
|
|||
continue;
|
||||
}
|
||||
|
||||
// `@import("…")` in expression position — a tuple element, a call
|
||||
// argument, a comparison operand, a member-call receiver, a chain deeper
|
||||
// than the binding rules follow. Skip the builtins a binding rule (or the
|
||||
// keyword-less side-effect rule) already owns; the rest are file
|
||||
// dependencies without a name. The receiver of a member call
|
||||
// (`try @import("dump.zig").root(...)`) is more: it is a namespace used
|
||||
// in place. Bind it as a namespace import whose local name IS the
|
||||
// builtin's own text — `@reference.receiver` on that call carries the
|
||||
// same text, so the shared Case-1 namespace-receiver lookup resolves
|
||||
// `root` in dump.zig exactly as it would for `const dump =
|
||||
// @import("dump.zig"); dump.root(...)`. Anything else is a side-effect
|
||||
// import (file edge only), emitted once per distinct source per file.
|
||||
const inlineImport = nodeMap['@import.inline'];
|
||||
if (inlineImport !== undefined) {
|
||||
const source = nodeMap['@import.source']!;
|
||||
if (claimedImportSourceIds.has(source.id)) continue;
|
||||
const sourceCapture = grouped['@import.source']!;
|
||||
if (isZigInlineImportReceiver(inlineImport)) {
|
||||
const key = `receiver:${source.text}`;
|
||||
if (importedSourceTexts.has(key)) continue;
|
||||
importedSourceTexts.add(key);
|
||||
out.push({
|
||||
'@import.statement': nodeToCapture('@import.statement', inlineImport),
|
||||
'@import.name': nodeToCapture('@import.name', inlineImport),
|
||||
'@import.source': sourceCapture,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (importedSourceTexts.has(source.text)) continue;
|
||||
importedSourceTexts.add(source.text);
|
||||
out.push({
|
||||
'@import.side-effect': nodeToCapture('@import.side-effect', inlineImport),
|
||||
'@import.source': sourceCapture,
|
||||
});
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -23,7 +23,14 @@ export function zigModuleNameOf(targetRaw: string): string {
|
|||
* `_ = @import("x.zig");` (and any keyword-less `<ident> = @import(…)`, a
|
||||
* statement rather than a declaration in this grammar) references the file
|
||||
* without binding a name — the `refAllDecls` / test-aggregation idiom. That
|
||||
* is a `side-effect` import: file edge, no binding (TS `import './x'`).
|
||||
* is a `side-effect` import: file edge, no binding (TS `import './x'`). So is
|
||||
* an `@import` in any expression position (a tuple element, a call argument,
|
||||
* a comparison operand — `emitZigScopeCaptures`'s `@import.inline` rule).
|
||||
*
|
||||
* The receiver of a member call, `@import("dump.zig").root(...)`, arrives as
|
||||
* a namespace import whose `@import.name` is the builtin's own text: the
|
||||
* shared namespace-receiver lookup keys on the receiver text, and that is
|
||||
* how the call resolves into `dump.zig` without a `const` handle.
|
||||
*/
|
||||
export function interpretZigImport(captures: CaptureMatch): ParsedImport | null {
|
||||
const source = captures['@import.source']?.text;
|
||||
|
|
|
|||
|
|
@ -197,6 +197,24 @@ const ZIG_SCOPE_QUERY = `
|
|||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.wildcard
|
||||
|
||||
;; Imports — \`@import("...")\` in ANY other position: a tuple element
|
||||
;; (\`pub const Interfaces = .{ @import("a.zig"), @import("b.zig") }\`, the
|
||||
;; JS-API registration table), a call argument (\`event.is(@import("x.zig"))\`),
|
||||
;; a comparison operand (\`T == @import("x.zig").T\`), the receiver of a
|
||||
;; member call (\`try @import("dump.zig").root(...)\`), a 3-deep member chain…
|
||||
;; Every one of them is a file dependency; only the const/var/usingnamespace
|
||||
;; shapes above bind a name. This rule matches EVERY \`@import\` builtin, the
|
||||
;; bound shapes included — \`emitZigScopeCaptures\` drops the matches whose
|
||||
;; string node a binding rule (or the keyword-less side-effect rule) already
|
||||
;; claimed, so a bound import is never doubled, and emits the rest as
|
||||
;; side-effect imports (file edge, no binding) — except the member-call
|
||||
;; receiver, which becomes a namespace import keyed by its own source text so
|
||||
;; the call resolves into the imported module (see the emitter).
|
||||
((builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.inline
|
||||
|
||||
;; Type bindings — parameter annotations (incl. self: *T receivers)
|
||||
(parameter
|
||||
name: (identifier) @type-binding.name
|
||||
|
|
|
|||
|
|
@ -2540,31 +2540,22 @@ export const ZIG_QUERIES = `
|
|||
(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).
|
||||
(variable_declaration
|
||||
(builtin_function
|
||||
(builtin_identifier) @builtin
|
||||
(arguments
|
||||
(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))
|
||||
; @import("path") — capture the string argument as @import.source, in
|
||||
; EVERY position: the value of a const/var (\`const std = @import("std")\`),
|
||||
; a member chain (\`const X = @import("x.zig").X\`), \`pub usingnamespace
|
||||
; @import("path")\`, a tuple element (\`pub const Interfaces = .{
|
||||
; @import("a.zig"), @import("b.zig") }\`), a call argument, a comparison
|
||||
; operand, the receiver of a member call (\`try @import("dump.zig").root(...)\`).
|
||||
; Zig has no import statement — the builtin IS the import, wherever it sits,
|
||||
; and every occurrence is a file dependency. The #eq? predicate keeps the
|
||||
; other builtins (@sizeOf, @TypeOf, @as, …) out. One rule, one match per
|
||||
; builtin: the structure phase only skips import matches (IMPORTS edges come
|
||||
; from the scope phase — \`emitZigScopeCaptures\`, whose \`@import.inline\`
|
||||
; rule is this rule's twin, decides which occurrences bind a name).
|
||||
((builtin_function
|
||||
(builtin_identifier) @builtin
|
||||
(arguments
|
||||
(string) @import.source))
|
||||
(#eq? @builtin "@import")) @import
|
||||
|
||||
; Free calls: foo(...)
|
||||
|
|
|
|||
4
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/dump.zig
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/dump.zig
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Only reached through an inline receiver: `@import("dump.zig").root(…)`.
|
||||
pub fn root(depth: u32) void {
|
||||
_ = depth;
|
||||
}
|
||||
|
|
@ -15,6 +15,13 @@ const idioms = @import("idioms");
|
|||
const build_config = @import("build_config");
|
||||
// Removed from the language in 0.15, still everywhere in 0.11–0.14 code.
|
||||
pub usingnamespace @import("mixin.zig");
|
||||
// @import in EXPRESSION position — the JS-API registration table idiom
|
||||
// (Lightpanda's bridge.zig lists ~290 modules this way). No name is bound;
|
||||
// each element is still a file dependency.
|
||||
pub const Interfaces = .{
|
||||
@import("webapi/AbortController.zig"),
|
||||
@import("webapi/AbortSignal.zig"),
|
||||
};
|
||||
|
||||
pub fn main() void {
|
||||
// call-return inference: the receiver of the constructor call names the type
|
||||
|
|
@ -45,6 +52,9 @@ pub fn main() void {
|
|||
var arena = idioms.Arena{};
|
||||
arena.reset();
|
||||
_ = build_config.version;
|
||||
// inline import as a member-call receiver: no `const dump = @import(...)`
|
||||
// handle, the module is used in place.
|
||||
@import("dump.zig").root(2);
|
||||
// statement assignments share the variable_declaration node type with
|
||||
// declarations in tree-sitter-zig 1.1.2 — none of these is a binding.
|
||||
counter.global_count = 5;
|
||||
|
|
|
|||
8
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/webapi/AbortController.zig
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/webapi/AbortController.zig
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// A JS-API module: only ever referenced from the registration table in
|
||||
// main.zig (`Interfaces = .{ @import("webapi/AbortController.zig"), … }`),
|
||||
// never bound to a `const`.
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = struct {
|
||||
pub const name = "AbortController";
|
||||
};
|
||||
};
|
||||
6
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/webapi/AbortSignal.zig
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/zig-idioms/src/webapi/AbortSignal.zig
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Second entry of the registration table in main.zig.
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = struct {
|
||||
pub const name = "AbortSignal";
|
||||
};
|
||||
};
|
||||
|
|
@ -264,6 +264,26 @@ describe.skipIf(!zigAvailable)('Zig idioms (zig-idioms fixture)', () => {
|
|||
expect(calls).toContain('main → push');
|
||||
});
|
||||
|
||||
it('imports every file behind an `@import` in EXPRESSION position (the `Interfaces = .{ @import(…), … }` table)', () => {
|
||||
// Both query sets only matched `@import` as the value of a const/var or
|
||||
// under `usingnamespace`, so a registration table of inline imports
|
||||
// (Lightpanda's bridge.zig: ~290 modules) produced NO file edges — the
|
||||
// modules looked unreferenced. Neither element binds a name; each is
|
||||
// still a dependency of main.zig.
|
||||
expect(imports).toContain('main.zig → src/webapi/AbortController.zig');
|
||||
expect(imports).toContain('main.zig → src/webapi/AbortSignal.zig');
|
||||
// and it mints no Const for the tuple elements — only for the table
|
||||
expect(getNodesByLabel(result, 'Const')).toContain('Interfaces');
|
||||
});
|
||||
|
||||
it('resolves a member call whose receiver is an inline import (`@import("dump.zig").root(…)`)', () => {
|
||||
// The receiver text is the builtin itself, not a `const` handle; the
|
||||
// inline import is bound as a namespace import under that very text so
|
||||
// the shared namespace-receiver lookup lands in dump.zig.
|
||||
expect(imports).toContain('main.zig → src/dump.zig');
|
||||
expect(calls).toContain('main → root');
|
||||
});
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -522,6 +522,97 @@ test {
|
|||
expect(vars).toEqual(['notAnImport']);
|
||||
});
|
||||
|
||||
it('emits a side-effect import for every @import in EXPRESSION position, once per source, never doubling a bound one', () => {
|
||||
// Both query sets only saw `@import` as the value of a const/var (or
|
||||
// under `usingnamespace`). Lightpanda's `Interfaces = .{ @import(…), … }`
|
||||
// table (288 modules), `CounterEnum("size", @import("ArenaPool.zig").BucketSize)`
|
||||
// (call argument), `event.is(@import("x.zig"))` and
|
||||
// `JsApi == @import("x.zig").JsApi` (comparison) all produced NO file
|
||||
// edge: 487 of 3,471 in-repo import pairs missing. Each is a dependency
|
||||
// without a name — a side-effect import — and the same file spelled
|
||||
// twice, or spelled inline AND bound to a const, gets one edge, not two.
|
||||
const src = `
|
||||
const std = @import("std");
|
||||
const c = @import("counter.zig");
|
||||
pub const Interfaces = .{ @import("a.zig"), @import("b.zig"), @import("a.zig") };
|
||||
const size = CounterEnum("size", @import("ArenaPool.zig").BucketSize);
|
||||
pub fn f() void {
|
||||
if (event.is(@import("event/MouseEvent.zig"))) {}
|
||||
if (JsApi == @import("cdata/Text.zig").JsApi) {}
|
||||
_ = @import("counter.zig").Extra;
|
||||
_ = std.mem;
|
||||
}
|
||||
`;
|
||||
const imports = emitZigScopeCaptures(src, 'x.zig')
|
||||
.filter((m) => m['@import.source'] !== undefined)
|
||||
.map((m) => interpretZigImport(m));
|
||||
expect(imports).toEqual([
|
||||
{ kind: 'namespace', localName: 'std', importedName: 'std', targetRaw: 'std' },
|
||||
{ kind: 'namespace', localName: 'c', importedName: 'counter', targetRaw: 'counter.zig' },
|
||||
{ kind: 'side-effect', targetRaw: 'a.zig' },
|
||||
{ kind: 'side-effect', targetRaw: 'b.zig' },
|
||||
{ kind: 'side-effect', targetRaw: 'ArenaPool.zig' },
|
||||
{ kind: 'side-effect', targetRaw: 'event/MouseEvent.zig' },
|
||||
{ kind: 'side-effect', targetRaw: 'cdata/Text.zig' },
|
||||
// `@import("counter.zig").Extra` in a discard: counter.zig is already
|
||||
// bound above (`const c = …`) — no second edge, and no phantom binding.
|
||||
]);
|
||||
// The tuple binds `Interfaces` as an ordinary Const (its value is a
|
||||
// struct literal, not an import); the discard binds nothing.
|
||||
const vars = emitZigScopeCaptures(src, 'x.zig')
|
||||
.filter((m) => m['@declaration.variable'] !== undefined)
|
||||
.map((m) => m['@declaration.name']?.text);
|
||||
expect(vars).toEqual(['Interfaces', 'size']);
|
||||
});
|
||||
|
||||
it('binds an inline import used as a member-call receiver under its own text, so `@import("dump.zig").root()` resolves', () => {
|
||||
// `try @import("dump.zig").root(…)` (80 sites in Lightpanda, 0 resolved):
|
||||
// the receiver is the builtin, not a const handle. Emitting a namespace
|
||||
// import whose local name IS the receiver text lets the shared
|
||||
// namespace-receiver lookup (`namespaceTargets.get(receiverText)`) find
|
||||
// dump.zig. One binding per distinct source; a deeper chain
|
||||
// (`@import("x.zig").Foo.init()`) is not a module receiver and stays a
|
||||
// plain side-effect import.
|
||||
const src = `
|
||||
pub fn f() !void {
|
||||
try @import("dump.zig").root(1);
|
||||
@import("dump.zig").other();
|
||||
_ = @import("../id.zig").uuidv4(&buf);
|
||||
_ = @import("x.zig").Foo.init();
|
||||
}
|
||||
`;
|
||||
const groups = emitZigScopeCaptures(src, 'x.zig');
|
||||
const imports = groups
|
||||
.filter((m) => m['@import.source'] !== undefined)
|
||||
.map((m) => interpretZigImport(m));
|
||||
expect(imports).toEqual([
|
||||
{
|
||||
kind: 'namespace',
|
||||
localName: '@import("dump.zig")',
|
||||
importedName: 'dump',
|
||||
targetRaw: 'dump.zig',
|
||||
},
|
||||
{
|
||||
kind: 'namespace',
|
||||
localName: '@import("../id.zig")',
|
||||
importedName: 'id',
|
||||
targetRaw: '../id.zig',
|
||||
},
|
||||
{ kind: 'side-effect', targetRaw: 'x.zig' },
|
||||
]);
|
||||
// The receiver capture on the call carries exactly the binding's text —
|
||||
// that identity is what makes the lookup succeed.
|
||||
const receivers = groups
|
||||
.filter((m) => m['@reference.call.member'] !== undefined)
|
||||
.map((m) => m['@reference.receiver']?.text);
|
||||
expect(receivers).toEqual([
|
||||
'@import("dump.zig")',
|
||||
'@import("dump.zig")',
|
||||
'@import("../id.zig")',
|
||||
'@import("x.zig").Foo',
|
||||
]);
|
||||
});
|
||||
|
||||
it('a function-scoped @import is not deferred: Zig imports are compile-time (importsExecuteWhereWritten: false)', () => {
|
||||
// C `#include` and Rust `use` answer the same. Without the flag the scope
|
||||
// extractor marks a body-level `@import` `runsOnlyWhenCalled`, hiding a
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue