From fe24b37f5eb2dfa6323925eef5189423560e21cd Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Thu, 3 Sep 2026 00:47:12 +0200 Subject: [PATCH] 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. --- .../src/core/ingestion/import-resolvers/zig.ts | 15 ++++++++++----- gitnexus/test/unit/zig-import-resolver.test.ts | 14 +++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/gitnexus/src/core/ingestion/import-resolvers/zig.ts b/gitnexus/src/core/ingestion/import-resolvers/zig.ts index b45da6fde..280adccd3 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/zig.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/zig.ts @@ -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(); + 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; } diff --git a/gitnexus/test/unit/zig-import-resolver.test.ts b/gitnexus/test/unit/zig-import-resolver.test.ts index 29c438699..796d11109 100644 --- a/gitnexus/test/unit/zig-import-resolver.test.ts +++ b/gitnexus/test/unit/zig-import-resolver.test.ts @@ -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([...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', () => {