fix(go): gate imports by module path (#2984)

This commit is contained in:
azizur100389 2026-08-18 14:31:40 +01:00 committed by GitHub
parent 5708db87d3
commit 87dc6c4d00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 66 additions and 27 deletions

View file

@ -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,

View file

@ -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',

View file

@ -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

View file

@ -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,

View file

@ -326,7 +326,6 @@ const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
*/
const KNOWN_GAPS: ReadonlyMap<SupportedLanguages, string> = new Map<SupportedLanguages, string>([
[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`'],

View file

@ -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

View file

@ -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',
});
}