fix(zig): a build-module alias bound to an unindexed root fails closed

resolveThroughBuildModules returned undefined when the containing module
bound the alias to a file that is not indexed, which let the repo-wide
addModule map answer under the same name (gitnexus-check on 5299c552).
The module's table is the authority for its aliases: bound-but-unindexed
is null, only an unbound name falls through. Unit case with a same-named
repo-wide decoy, plus the outside-module file that still reaches it.
This commit is contained in:
Navid EMAD 2026-09-03 00:47:12 +02:00
parent 5299c5521f
commit fe24b37f5e
No known key found for this signature in database
2 changed files with 23 additions and 6 deletions

View file

@ -69,9 +69,11 @@ function zigModulesContaining(
* tables. `undefined` when no containing module binds the name (the caller
* falls back to the repo-wide tables); `null` when the containing modules
* DISAGREE two same-directory modules that bind one alias to different
* roots. That is fail-closed on purpose: picking either would mint a
* confident wrong `IMPORTS` edge for half the files, which is the
* first-wins defect this table exists to remove.
* roots or when the alias is bound to a root that is not indexed (a
* generated or skipped file). Both are fail-closed on purpose: the module's
* own table is the authority for the alias, and falling through to the
* repo-wide map would reintroduce the first-wins answer this table exists
* to remove, under a name the module never meant.
*/
function resolveThroughBuildModules(
currentFile: string,
@ -80,11 +82,14 @@ function resolveThroughBuildModules(
modules: readonly ZigBuildModule[],
): string | null | undefined {
const targets = new Set<string>();
let bound = false;
for (const mod of zigModulesContaining(currentFile, modules)) {
const target = mod.imports.get(importPath);
if (target !== undefined && allFiles.has(target)) targets.add(target);
if (target === undefined) continue;
bound = true;
if (allFiles.has(target)) targets.add(target);
}
if (targets.size === 0) return undefined;
if (targets.size === 0) return bound ? null : undefined;
if (targets.size > 1) return null;
return targets.values().next().value ?? null;
}

View file

@ -317,12 +317,24 @@ describe('resolveZigImportInternal', () => {
buildModules: [{ root: 'src/main.zig', imports: new Map([['cfg', 'src/cfg.zig']]) }],
};
expect(resolveZigImportInternal('src/main.zig', 'mylib', files, config)).toBe('src/lib.zig');
// A table entry whose root is not indexed is not an answer either.
// A table entry whose root is not indexed is not an answer either — and
// it does not fall through: the module's table is the authority for the
// alias, so a same-named repo-wide `addModule("cfg")` pointing elsewhere
// must not answer in its place (gitnexus-check on 5299c552).
const stale = {
...config,
rootModules: new Map([
['mylib', 'src/lib.zig'],
['cfg', 'src/cfg.zig'],
]),
buildModules: [{ root: 'src/main.zig', imports: new Map([['cfg', 'src/gone.zig']]) }],
};
expect(resolveZigImportInternal('src/main.zig', 'cfg', files, stale)).toBeNull();
// …while a file OUTSIDE that module (`src/lib.zig` shares the root's
// directory and so belongs to it; `other/x.zig` does not) still reaches
// the repo-wide name.
const outside = new Set<string>([...files, 'other/x.zig']);
expect(resolveZigImportInternal('other/x.zig', 'cfg', outside, stale)).toBe('src/cfg.zig');
});
it('returns null for an unknown bare name not in build.zig.zon', () => {