diff --git a/gitnexus/bench/import-target/baselines.json b/gitnexus/bench/import-target/baselines.json index 4c8e75f9f..0dac29cea 100644 --- a/gitnexus/bench/import-target/baselines.json +++ b/gitnexus/bench/import-target/baselines.json @@ -164,7 +164,7 @@ "files_small": 8000, "files_large": 32000, "path_segments": 14, - "probe": "github.com/org/repo0/pkg/util" + "probe": "example.com/mod/repo0/pkg/util" }, "_measured": { "collide_ms": 7.15, diff --git a/gitnexus/bench/import-target/measure.mjs b/gitnexus/bench/import-target/measure.mjs index 61aed7775..aaf61ec83 100644 --- a/gitnexus/bench/import-target/measure.mjs +++ b/gitnexus/bench/import-target/measure.mjs @@ -2016,8 +2016,8 @@ const HEAP_PROBE_TARGET = { // budgeted ones above: a spelling `uniqueTarget` already mints for that language, and // one that MISSES, so the reading is the index and the cascade runs to the // end. Chosen from the miss family that reaches furthest into each cascade: - // - `go` takes the GOPATH fallback, one `filesDirectlyInPkgDir` per path - // segment, which is the leg that forces `PackageDirIndex`; + // - `go` names a missing package inside GO_MODULE, which reaches the + // package-directory lookup and forces `PackageDirIndex`; // - `dart` is an external package, so BOTH candidate paths miss and both // walk the basename bucket to completion; // - `kotlin` misses in `suffixByStem`, the map its four-tier cascade builds; @@ -2028,7 +2028,7 @@ const HEAP_PROBE_TARGET = { // `javascript` and `c` arms they are excluded as duplicates OF, so the // bound compares like with like. `vue`'s is bare rather than `@/…` // because the alias branch rewrites to `src/` and would resolve. - go: 'github.com/org/repo0/pkg/util', + go: 'example.com/mod/repo0/pkg/util', dart: 'package:ext0/src/thing.dart', kotlin: 'com.ghost0.deep.Missing', cobol: 'VENDOR0', diff --git a/gitnexus/src/core/ingestion/languages/go/import-target.ts b/gitnexus/src/core/ingestion/languages/go/import-target.ts index 847af0c09..14d9e2d48 100644 --- a/gitnexus/src/core/ingestion/languages/go/import-target.ts +++ b/gitnexus/src/core/ingestion/languages/go/import-target.ts @@ -16,9 +16,9 @@ import { perFileSet } from '../../import-resolvers/per-file-set.js'; * IMPORTS edge fanout AND binding materialization for every exported symbol in * the package. * - * Strategy (first match wins): - * 1. go.mod-based: strip module prefix, match package directory - * 2. Non-go.mod / GOPATH: progressively shorter directory suffixes + * Strategy: + * 1. With go.mod: resolve only imports owned by that module + * 2. Without go.mod / GOPATH: progressively shorter directory suffixes */ export function resolveGoImportTarget( targetRaw: string, @@ -30,11 +30,14 @@ export function resolveGoImportTarget( const goModule = resolutionConfig as GoModuleConfig | undefined; - // 1) go.mod-based: strip module prefix, match directory - if ( - goModule != null && - (targetRaw === goModule.modulePath || targetRaw.startsWith(`${goModule.modulePath}/`)) - ) { + // 1) go.mod is authoritative: only this module's exact path or subpackages + // can name files in the workspace. Standard-library and third-party + // imports must not fall through to the suffix matcher below. + if (goModule != null) { + const ownedByModule = + targetRaw === goModule.modulePath || targetRaw.startsWith(`${goModule.modulePath}/`); + if (!ownedByModule) return null; + const relativePkg = targetRaw === goModule.modulePath ? '' : targetRaw.slice(goModule.modulePath.length + 1); // e.g. "internal/models" const files = @@ -42,6 +45,7 @@ export function resolveGoImportTarget( ? findRootPackageFiles(allFilePaths) : findAllFilesInPkgDir(allFilePaths, relativePkg); if (files.length > 0) return files; + return null; } // 2) Non-go.mod / GOPATH: progressively shorter directory suffixes. @@ -66,10 +70,9 @@ function isGoPackageFile(normalized: string): boolean { * Package index over the file set, memoized on the Set's identity (#2877). * * Every leg above used to walk all of `allFilePaths`, and the GOPATH fallback - * walks once per path segment — so a single unresolved import (which is most of - * them: stdlib and third-party module paths run the whole cascade to completion - * before returning null) cost several full workspace scans, making resolution - * O(imports × files). + * walks once per path segment — so without go.mod a single unresolved stdlib or + * third-party import ran the whole cascade before returning null and cost + * several full workspace scans, making resolution O(imports × files). * * The orchestrator hands the same Set to every import in a pass, so the index * is built once per run. `resolveGoImportTarget` must therefore never copy the diff --git a/gitnexus/test/integration/go-import-index-reuse.test.ts b/gitnexus/test/integration/go-import-index-reuse.test.ts index ac798e471..44cd9dec5 100644 --- a/gitnexus/test/integration/go-import-index-reuse.test.ts +++ b/gitnexus/test/integration/go-import-index-reuse.test.ts @@ -60,10 +60,8 @@ describe('Go import resolution — index reuse across imports (#2877)', () => { const resolved: (string | readonly string[] | null)[] = []; for (let i = 0; i < 200; i++) { - // Three shapes that between them reach every leg: the module-relative - // package leg, the root-package leg, and a third-party path that misses - // and so runs the whole GOPATH suffix cascade to completion — the case - // that used to cost one full workspace scan per path segment. + // Three module-mode shapes: the module-relative package leg, the + // root-package leg, and a third-party path rejected by the go.mod gate. resolved.push( resolveImportTarget('example.com/mod/internal/models', FROM_FILE, files, GO_MODULE), ); @@ -83,6 +81,19 @@ describe('Go import resolution — index reuse across imports (#2877)', () => { expect(resolved[2]).toBeNull(); }); + it('rejects foreign and stdlib imports before building the package index', () => { + const files = buildWorkspace(300); + + for (let i = 0; i < 200; i++) { + expect( + resolveImportTarget(`github.com/vendor/dep${i}/sub`, FROM_FILE, files, GO_MODULE), + ).toBeNull(); + expect(resolveImportTarget('fmt', FROM_FILE, files, GO_MODULE)).toBeNull(); + } + + expect(files.scans).toBe(0); + }); + it('a distinct file set gets its own index (no stale cross-run reuse)', () => { expectDistinctFileSetsGetOwnIndex({ resolveImportTarget, diff --git a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts index b7dd03473..42dbf9799 100644 --- a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts +++ b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts @@ -326,7 +326,6 @@ const CASES: ReadonlyMap = new Map([ */ const KNOWN_GAPS: ReadonlyMap = new Map([ [SupportedLanguages.Kotlin, '`org.junit.Assert` -> `src/main/kotlin/vendor/Assert.kt`'], - [SupportedLanguages.Go, '`github.com/vendor/dep/internal/models` -> `internal/models/user.go`'], [SupportedLanguages.Ruby, '`rails/generators` -> `lib/generators.rb`'], [SupportedLanguages.PHP, '`Vendor\\Ghost\\Missing` -> `lib/Legacy/Missing.php`'], [SupportedLanguages.Dart, '`package:http/http.dart` -> `lib/http.dart`'], diff --git a/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts b/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts index 1e1e715b3..c3e4eac26 100644 --- a/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/go/go-imports.test.ts @@ -126,6 +126,31 @@ describe('Go import target resolution', () => { ]); }); + it.each(['github.com/vendor/dep/internal/models', 'fmt', 'example.com/modular/internal/models'])( + 'rejects imports outside the go.mod module: %s', + (targetRaw) => { + const result = resolveGoImportTarget( + targetRaw, + 'main.go', + new Set(['internal/models/user.go', 'main.go']), + { modulePath: 'example.com/mod' }, + ); + + expect(result).toBeNull(); + }, + ); + + it('treats a semantic import-version suffix as part of the module path', () => { + const result = resolveGoImportTarget( + 'example.com/mod/v2/internal/models', + 'cmd/app/main.go', + new Set(['internal/models/user.go']), + { modulePath: 'example.com/mod/v2' }, + ); + + expect(result).toEqual(['internal/models/user.go']); + }); + it('rejects single-segment GOPATH suffix that collides with a local dir', () => { // "github.com/other/team/pkg" suffix-stripped would eventually // reach "pkg" which matches the local pkg/ dir — but we require diff --git a/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts b/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts index 311ca8aa2..560a5daca 100644 --- a/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts +++ b/gitnexus/test/unit/scope-resolution/import-target-index-parity.test.ts @@ -99,16 +99,17 @@ function legacyResolveGoImportTarget( modulePath: string | undefined, ): string | readonly string[] | null { if (!targetRaw) return null; - if ( - modulePath !== undefined && - (targetRaw === modulePath || targetRaw.startsWith(`${modulePath}/`)) - ) { + if (modulePath !== undefined) { + const ownedByModule = targetRaw === modulePath || targetRaw.startsWith(`${modulePath}/`); + if (!ownedByModule) return null; + const relativePkg = targetRaw === modulePath ? '' : targetRaw.slice(modulePath.length + 1); const files = relativePkg === '' ? legacyFindRootPackageFiles(allFilePaths) : legacyFindAllFilesInPkgDir(allFilePaths, relativePkg); if (files.length > 0) return files; + return null; } const parts = targetRaw.split('/').filter(Boolean); for (let i = 0; i < parts.length - 1; i++) { @@ -717,7 +718,7 @@ describe('import-target index hoist — output parity with the pre-change scans' for (let repo = 0; repo < 40; repo++) { const go = corpus(repo, '.go', 6 + (repo % 25)); for (const t of GO_TARGETS) { - if (resolveGoImportTarget(t, 'main.go', go, { modulePath: 'example.com/mod' }) !== null) { + if (resolveGoImportTarget(t, 'main.go', go) !== null) { hits.go++; } } @@ -782,7 +783,7 @@ describe('import-target index hoist — built once per file set, not once per im it('go builds one index for many imports (#2877)', () => { const files = countingCorpus(1, '.go'); for (let i = 0; i < 200; i++) { - resolveGoImportTarget(`github.com/org/repo${i}/pkg`, 'main.go', files, { + resolveGoImportTarget(`example.com/mod/repo${i}/pkg`, 'main.go', files, { modulePath: 'example.com/mod', }); }