mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(zig): pub const X = @import(…) at file scope republishes X (reexportsName)
Lightpanda's `lightpanda.zig` is one long list of `pub const Arena =
@import("Arena.zig");`, and most files name their types through it (`const
lp = @import("lightpanda"); const Arena = lp.Arena;`, `arena: *lp.Arena`).
The scope side treated those bindings as plain imports of the hub file, so
the hub never published the names it re-exports and a third file's `const
Arena = lp.Arena;` (promoted to a named import of `Arena` from the hub) found
nothing.
`emitZigScopeCaptures` now marks named/alias import groups whose declaration
is a file-level `pub const` — the `@import(...).X` form, the alias promotion
`pub const Bar = ns.Bar`, and the file-struct type twin of `pub const Arena =
@import("Arena.zig")` — and `interpretZigImport` sets the shared contract's
`reexportsName: true` on them (the Python `__init__.py` shape, consumed by
`buildReexportClosures`). Private and fn-local bindings stay unflagged.
Not covered here: a receiver ANNOTATED with the dotted hub path (`arena:
*lp.Arena`) — Case 3 of the receiver-bound pass looks the member up with
`findExportedDef`, which only sees locally declared names; following
re-exports there is a shared change left for a follow-up.
This commit is contained in:
parent
81a6b62890
commit
85df4c1d3b
6 changed files with 96 additions and 2 deletions
|
|
@ -4,6 +4,7 @@ import { getZigParser, getZigScopeQuery } from './query.js';
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
|
||||
import { hasZigPubKeyword } from '../../export-detection.js';
|
||||
|
||||
/** Zig container node types: `struct`, `enum`, `union` and the fieldless
|
||||
* `opaque` all bind through `const T = <container> {…}` and may own methods.
|
||||
|
|
@ -131,6 +132,17 @@ export function isZigFileThisAlias(declNode: SyntaxNode): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/** Does this module-level import binding REPUBLISH the name? `pub const Arena
|
||||
* = @import("Arena.zig");` at file scope makes `Arena` part of this file's
|
||||
* public surface: a third file reaches it as `lp.Arena` (Lightpanda's
|
||||
* `lightpanda.zig` is one long list of these). Same fact as a Python
|
||||
* `__init__.py` `from .impl import X` — the shared contract's
|
||||
* `reexportsName` flag. Only `pub` at container level publishes; a `const`
|
||||
* inside a fn body binds locally. */
|
||||
export function isZigPublishingImport(declNode: SyntaxNode): boolean {
|
||||
return declNode.parent?.type === 'source_file' && hasZigPubKeyword(declNode);
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
|
@ -204,6 +216,14 @@ export function isZigContainerMethod(
|
|||
return false;
|
||||
}
|
||||
|
||||
/** `{ '@import.reexports': 'true' }` when `stmt` is a publishing import
|
||||
* (see `isZigPublishingImport`), else nothing — spread into an import group. */
|
||||
function republishMarker(stmt: SyntaxNode): Record<string, Capture> {
|
||||
return isZigPublishingImport(stmt)
|
||||
? { '@import.reexports': syntheticCapture('@import.reexports', stmt, 'true') }
|
||||
: {};
|
||||
}
|
||||
|
||||
/** Every `const X = @This();` in the tree, keyed by the alias node id's owner:
|
||||
* returns a map from the DECLARING container node id to `[aliasName,
|
||||
* containerName]`. The file-level alias maps to the file-struct name (only
|
||||
|
|
@ -499,6 +519,7 @@ export function emitZigScopeCaptures(
|
|||
'@import.name': nodeToCapture('@import.name', nodeMap['@alias.name']!),
|
||||
'@import.imported': nodeToCapture('@import.imported', nodeMap['@alias.member']!),
|
||||
'@import.source': nodeToCapture('@import.source', source),
|
||||
...republishMarker(aliasStmt),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -592,6 +613,14 @@ export function emitZigScopeCaptures(
|
|||
grouped['@declaration.method'] = { ...fnCapture, name: '@declaration.method' };
|
||||
}
|
||||
|
||||
// `pub const X = @import("x.zig").X;` at file scope republishes `X`.
|
||||
if (
|
||||
nodeMap['@import.statement'] !== undefined &&
|
||||
nodeMap['@import.imported'] !== undefined &&
|
||||
isZigPublishingImport(nodeMap['@import.statement'])
|
||||
) {
|
||||
Object.assign(grouped, republishMarker(nodeMap['@import.statement']));
|
||||
}
|
||||
out.push(grouped);
|
||||
|
||||
// F5 — field types. `session: *Session,` declares the TYPE of the member
|
||||
|
|
@ -658,6 +687,9 @@ export function emitZigScopeCaptures(
|
|||
nodeMap['@import.name'],
|
||||
zigFileStructName(source),
|
||||
),
|
||||
// `pub const Arena = @import("Arena.zig");` republishes the TYPE too:
|
||||
// `lp.Arena` in a third file is the Struct `Arena.zig` declares.
|
||||
...republishMarker(importStmt),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,9 +57,21 @@ export function interpretZigImport(captures: CaptureMatch): ParsedImport | null
|
|||
// `import { Foo as Bar }`.
|
||||
const imported = captures['@import.imported']?.text;
|
||||
if (imported !== undefined) {
|
||||
// `pub const X = …` at file scope republishes the name (Python
|
||||
// `__init__.py` shape): a third file reads it as `thisModule.X`. The
|
||||
// marker is set by `emitZigScopeCaptures` where the syntax node is
|
||||
// still available (`isZigPublishingImport`).
|
||||
const republish = captures['@import.reexports'] !== undefined ? { reexportsName: true } : {};
|
||||
return imported === name
|
||||
? { kind: 'named', localName: name, importedName: imported, targetRaw }
|
||||
: { kind: 'alias', localName: name, importedName: imported, alias: name, targetRaw };
|
||||
? { kind: 'named', localName: name, importedName: imported, targetRaw, ...republish }
|
||||
: {
|
||||
kind: 'alias',
|
||||
localName: name,
|
||||
importedName: imported,
|
||||
alias: name,
|
||||
targetRaw,
|
||||
...republish,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
5
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/lp.zig
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/lp.zig
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// A re-export hub (Lightpanda `lightpanda.zig`): `pub const X = @import(...)`
|
||||
// at file scope republishes X, so a third file reaches the TYPE as `lp.X`.
|
||||
pub const Page = @import("Page.zig");
|
||||
pub const Session = @import("Session.zig");
|
||||
const private_util = @import("util.zig");
|
||||
|
|
@ -24,3 +24,10 @@ pub fn main() void {
|
|||
fn useParam(page: *Page) void {
|
||||
_ = page.getArena();
|
||||
}
|
||||
|
||||
const lp = @import("lp.zig");
|
||||
// The type behind a re-exported name, taken through the hub.
|
||||
const PageViaHub = lp.Page;
|
||||
fn viaHubAlias(page: *PageViaHub) void {
|
||||
_ = page.getArena();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -449,6 +449,14 @@ describe.skipIf(!zigAvailable)('Zig file-structs (zig-filestruct fixture)', () =
|
|||
expect(calls).toContain('bump → getArena');
|
||||
});
|
||||
|
||||
it('republishes a `pub const X = @import("X.zig")` so a third file reaches the type through the hub', () => {
|
||||
// Lightpanda's `lightpanda.zig` is one long list of `pub const X =
|
||||
// @import("...")`; `const lp = @import("lightpanda"); const Arena =
|
||||
// lp.Arena;` is how most files name their types. The re-export must
|
||||
// publish the TYPE (the file-struct), not just the module.
|
||||
expect(edgeSet(getRelationships(result, 'CALLS'))).toContain('viaHubAlias → getArena');
|
||||
});
|
||||
|
||||
it('keeps the file-struct type reachable through the namespace import binding', () => {
|
||||
// `const Page = @import("Page.zig")` binds both the module (`Page.init`)
|
||||
// and the type it declares. Two Struct defs named `Page` in different
|
||||
|
|
|
|||
|
|
@ -947,6 +947,36 @@ pub const Inner = struct {
|
|||
]);
|
||||
});
|
||||
|
||||
it('flags file-level `pub` import bindings as republishing their name (reexportsName)', () => {
|
||||
// `pub const Arena = @import("Arena.zig");` / `pub const X = @import("x.zig").X;`
|
||||
// at file scope publish the name from THIS module (Python `__init__.py`
|
||||
// shape); a private or fn-local binding does not.
|
||||
const src = `
|
||||
pub const Arena = @import("Arena.zig");
|
||||
pub const Foo = @import("foo.zig").Foo;
|
||||
const hidden = @import("hidden.zig");
|
||||
const ns = @import("ns.zig");
|
||||
pub const Bar = ns.Bar;
|
||||
fn f() void {
|
||||
const Local = @import("local.zig").Local;
|
||||
_ = Local;
|
||||
}
|
||||
`;
|
||||
const imports = emitZigScopeCaptures(src, 'lp.zig')
|
||||
.filter((m) => m['@import.source'] !== undefined)
|
||||
.map((m) => interpretZigImport(m))
|
||||
.filter((i) => i !== null && (i.kind === 'named' || i.kind === 'alias'))
|
||||
.map((i) => [i!.localName, (i as { reexportsName?: boolean }).reexportsName === true]);
|
||||
expect(imports).toEqual([
|
||||
['Arena', true], // the file-struct TYPE twin of a pub namespace import
|
||||
['Foo', true],
|
||||
['hidden', false], // no `pub`
|
||||
['ns', false],
|
||||
['Bar', true], // pub alias of a namespace member
|
||||
['Local', false], // fn-local: binds locally, publishes nothing
|
||||
]);
|
||||
});
|
||||
|
||||
it('gives a namespace import of a .zig file a TYPE twin (named import of the file stem)', () => {
|
||||
const src = `
|
||||
const Page = @import("Page.zig");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue