Skip to content

Commit 6196979

Browse files
author
Jay Conrod
committed
cmd/go/internal/modload: prevent tidy downgrading disambiguating modules
If an indirectly required module does not provide any packages needed to build packages in the main module but is needed to disambiguate imports, 'go mod tidy' may keep an indirect requirement on that module to prevent it from being downgraded. This can prevent the introduction of new ambiguities. This also ensures tidy keeps sums needed to load all packages. Fixes #47738 Change-Id: Ib8e33422b95394707894cda7cfbb71a4b111e0ed Reviewed-on: https://go-review.googlesource.com/c/go/+/344572 Trust: Jay Conrod <[email protected]> Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 72bb818 commit 6196979

File tree

4 files changed

+150
-34
lines changed

4 files changed

+150
-34
lines changed

src/cmd/go/internal/modload/buildlist.go

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ func updateRoots(ctx context.Context, direct map[string]bool, rs *Requirements,
591591
// selected at the same version or is upgraded by the dependencies of a
592592
// root.
593593
//
594-
// If any module that provided a package has been upgraded above its previous,
594+
// If any module that provided a package has been upgraded above its previous
595595
// version, the caller may need to reload and recompute the package graph.
596596
//
597597
// To ensure that the loading process eventually converges, the caller should
@@ -980,17 +980,37 @@ func spotCheckRoots(ctx context.Context, rs *Requirements, mods map[module.Versi
980980
return true
981981
}
982982

983-
// tidyUnprunedRoots returns a minimal set of root requirements that maintains the
984-
// selected version of every module that provided a package in pkgs, and
985-
// includes the selected version of every such module in direct as a root.
983+
// tidyUnprunedRoots returns a minimal set of root requirements that maintains
984+
// the selected version of every module that provided or lexically could have
985+
// provided a package in pkgs, and includes the selected version of every such
986+
// module in direct as a root.
986987
func tidyUnprunedRoots(ctx context.Context, mainModule module.Version, direct map[string]bool, pkgs []*loadPkg) (*Requirements, error) {
987988
var (
989+
// keep is a set of of modules that provide packages or are needed to
990+
// disambiguate imports.
988991
keep []module.Version
989992
keptPath = map[string]bool{}
990-
)
991-
var (
992-
rootPaths []string // module paths that should be included as roots
993+
994+
// rootPaths is a list of module paths that provide packages directly
995+
// imported from the main module. They should be included as roots.
996+
rootPaths []string
993997
inRootPaths = map[string]bool{}
998+
999+
// altMods is a set of paths of modules that lexically could have provided
1000+
// imported packages. It may be okay to remove these from the list of
1001+
// explicit requirements if that removes them from the module graph. If they
1002+
// are present in the module graph reachable from rootPaths, they must not
1003+
// be at a lower version. That could cause a missing sum error or a new
1004+
// import ambiguity.
1005+
//
1006+
// For example, suppose a developer rewrites imports from example.com/m to
1007+
// example.com/m/v2, then runs 'go mod tidy'. Tidy may delete the
1008+
// requirement on example.com/m if there is no other transitive requirement
1009+
// on it. However, if example.com/m were downgraded to a version not in
1010+
// go.sum, when package example.com/m/v2/p is loaded, we'd get an error
1011+
// trying to disambiguate the import, since we can't check example.com/m
1012+
// without its sum. See #47738.
1013+
altMods = map[string]string{}
9941014
)
9951015
for _, pkg := range pkgs {
9961016
if !pkg.fromExternalModule() {
@@ -1004,12 +1024,44 @@ func tidyUnprunedRoots(ctx context.Context, mainModule module.Version, direct ma
10041024
inRootPaths[m.Path] = true
10051025
}
10061026
}
1027+
for _, m := range pkg.altMods {
1028+
altMods[m.Path] = m.Version
1029+
}
10071030
}
10081031

1009-
min, err := mvs.Req(mainModule, rootPaths, &mvsReqs{roots: keep})
1032+
// Construct a build list with a minimal set of roots.
1033+
// This may remove or downgrade modules in altMods.
1034+
reqs := &mvsReqs{roots: keep}
1035+
min, err := mvs.Req(mainModule, rootPaths, reqs)
1036+
if err != nil {
1037+
return nil, err
1038+
}
1039+
buildList, err := mvs.BuildList([]module.Version{mainModule}, reqs)
10101040
if err != nil {
10111041
return nil, err
10121042
}
1043+
1044+
// Check if modules in altMods were downgraded but not removed.
1045+
// If so, add them to roots, which will retain an "// indirect" requirement
1046+
// in go.mod. See comment on altMods above.
1047+
keptAltMod := false
1048+
for _, m := range buildList {
1049+
if v, ok := altMods[m.Path]; ok && semver.Compare(m.Version, v) < 0 {
1050+
keep = append(keep, module.Version{Path: m.Path, Version: v})
1051+
keptAltMod = true
1052+
}
1053+
}
1054+
if keptAltMod {
1055+
// We must run mvs.Req again instead of simply adding altMods to min.
1056+
// It's possible that a requirement in altMods makes some other
1057+
// explicit indirect requirement unnecessary.
1058+
reqs.roots = keep
1059+
min, err = mvs.Req(mainModule, rootPaths, reqs)
1060+
if err != nil {
1061+
return nil, err
1062+
}
1063+
}
1064+
10131065
return newRequirements(unpruned, min, direct), nil
10141066
}
10151067

src/cmd/go/internal/modload/import.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -243,20 +243,24 @@ func (e *invalidImportError) Unwrap() error {
243243
//
244244
// If the package is not present in any module selected from the requirement
245245
// graph, importFromModules returns an *ImportMissingError.
246-
func importFromModules(ctx context.Context, path string, rs *Requirements, mg *ModuleGraph) (m module.Version, dir string, err error) {
246+
//
247+
// If the package is present in exactly one module, importFromModules will
248+
// return the module, its root directory, and a list of other modules that
249+
// lexically could have provided the package but did not.
250+
func importFromModules(ctx context.Context, path string, rs *Requirements, mg *ModuleGraph) (m module.Version, dir string, altMods []module.Version, err error) {
247251
if strings.Contains(path, "@") {
248-
return module.Version{}, "", fmt.Errorf("import path should not have @version")
252+
return module.Version{}, "", nil, fmt.Errorf("import path should not have @version")
249253
}
250254
if build.IsLocalImport(path) {
251-
return module.Version{}, "", fmt.Errorf("relative import not supported")
255+
return module.Version{}, "", nil, fmt.Errorf("relative import not supported")
252256
}
253257
if path == "C" {
254258
// There's no directory for import "C".
255-
return module.Version{}, "", nil
259+
return module.Version{}, "", nil, nil
256260
}
257261
// Before any further lookup, check that the path is valid.
258262
if err := module.CheckImportPath(path); err != nil {
259-
return module.Version{}, "", &invalidImportError{importPath: path, err: err}
263+
return module.Version{}, "", nil, &invalidImportError{importPath: path, err: err}
260264
}
261265

262266
// Is the package in the standard library?
@@ -265,14 +269,14 @@ func importFromModules(ctx context.Context, path string, rs *Requirements, mg *M
265269
for _, mainModule := range MainModules.Versions() {
266270
if MainModules.InGorootSrc(mainModule) {
267271
if dir, ok, err := dirInModule(path, MainModules.PathPrefix(mainModule), MainModules.ModRoot(mainModule), true); err != nil {
268-
return module.Version{}, dir, err
272+
return module.Version{}, dir, nil, err
269273
} else if ok {
270-
return mainModule, dir, nil
274+
return mainModule, dir, nil, nil
271275
}
272276
}
273277
}
274278
dir := filepath.Join(cfg.GOROOT, "src", path)
275-
return module.Version{}, dir, nil
279+
return module.Version{}, dir, nil, nil
276280
}
277281

278282
// -mod=vendor is special.
@@ -283,19 +287,19 @@ func importFromModules(ctx context.Context, path string, rs *Requirements, mg *M
283287
mainDir, mainOK, mainErr := dirInModule(path, MainModules.PathPrefix(mainModule), modRoot, true)
284288
vendorDir, vendorOK, _ := dirInModule(path, "", filepath.Join(modRoot, "vendor"), false)
285289
if mainOK && vendorOK {
286-
return module.Version{}, "", &AmbiguousImportError{importPath: path, Dirs: []string{mainDir, vendorDir}}
290+
return module.Version{}, "", nil, &AmbiguousImportError{importPath: path, Dirs: []string{mainDir, vendorDir}}
287291
}
288292
// Prefer to return main directory if there is one,
289293
// Note that we're not checking that the package exists.
290294
// We'll leave that for load.
291295
if !vendorOK && mainDir != "" {
292-
return mainModule, mainDir, nil
296+
return mainModule, mainDir, nil, nil
293297
}
294298
if mainErr != nil {
295-
return module.Version{}, "", mainErr
299+
return module.Version{}, "", nil, mainErr
296300
}
297301
readVendorList(mainModule)
298-
return vendorPkgModule[path], vendorDir, nil
302+
return vendorPkgModule[path], vendorDir, nil, nil
299303
}
300304

301305
// Check each module on the build list.
@@ -316,7 +320,7 @@ func importFromModules(ctx context.Context, path string, rs *Requirements, mg *M
316320
// already non-nil, then we attempt to load the package using the full
317321
// requirements in mg.
318322
for {
319-
var sumErrMods []module.Version
323+
var sumErrMods, altMods []module.Version
320324
for prefix := path; prefix != "."; prefix = pathpkg.Dir(prefix) {
321325
var (
322326
v string
@@ -350,13 +354,15 @@ func importFromModules(ctx context.Context, path string, rs *Requirements, mg *M
350354
// continue the loop and find the package in some other module,
351355
// we need to look at this module to make sure the import is
352356
// not ambiguous.
353-
return module.Version{}, "", err
357+
return module.Version{}, "", nil, err
354358
}
355359
if dir, ok, err := dirInModule(path, m.Path, root, isLocal); err != nil {
356-
return module.Version{}, "", err
360+
return module.Version{}, "", nil, err
357361
} else if ok {
358362
mods = append(mods, m)
359363
dirs = append(dirs, dir)
364+
} else {
365+
altMods = append(altMods, m)
360366
}
361367
}
362368

@@ -369,23 +375,23 @@ func importFromModules(ctx context.Context, path string, rs *Requirements, mg *M
369375
mods[i], mods[j] = mods[j], mods[i]
370376
dirs[i], dirs[j] = dirs[j], dirs[i]
371377
}
372-
return module.Version{}, "", &AmbiguousImportError{importPath: path, Dirs: dirs, Modules: mods}
378+
return module.Version{}, "", nil, &AmbiguousImportError{importPath: path, Dirs: dirs, Modules: mods}
373379
}
374380

375381
if len(sumErrMods) > 0 {
376382
for i := 0; i < len(sumErrMods)/2; i++ {
377383
j := len(sumErrMods) - 1 - i
378384
sumErrMods[i], sumErrMods[j] = sumErrMods[j], sumErrMods[i]
379385
}
380-
return module.Version{}, "", &ImportMissingSumError{
386+
return module.Version{}, "", nil, &ImportMissingSumError{
381387
importPath: path,
382388
mods: sumErrMods,
383389
found: len(mods) > 0,
384390
}
385391
}
386392

387393
if len(mods) == 1 {
388-
return mods[0], dirs[0], nil
394+
return mods[0], dirs[0], altMods, nil
389395
}
390396

391397
if mg != nil {
@@ -395,7 +401,7 @@ func importFromModules(ctx context.Context, path string, rs *Requirements, mg *M
395401
if !HasModRoot() {
396402
queryErr = ErrNoModRoot
397403
}
398-
return module.Version{}, "", &ImportMissingError{Path: path, QueryErr: queryErr, isStd: pathIsStd}
404+
return module.Version{}, "", nil, &ImportMissingError{Path: path, QueryErr: queryErr, isStd: pathIsStd}
399405
}
400406

401407
// So far we've checked the root dependencies.
@@ -406,7 +412,7 @@ func importFromModules(ctx context.Context, path string, rs *Requirements, mg *M
406412
// the module graph, so we can't return an ImportMissingError here — one
407413
// of the missing modules might actually contain the package in question,
408414
// in which case we shouldn't go looking for it in some new dependency.
409-
return module.Version{}, "", err
415+
return module.Version{}, "", nil, err
410416
}
411417
}
412418
}

src/cmd/go/internal/modload/load.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,7 @@ type loadPkg struct {
862862
imports []*loadPkg // packages imported by this one
863863
testImports []string // test-only imports, saved for use by pkg.test.
864864
inStd bool
865+
altMods []module.Version // modules that could have contained the package but did not
865866

866867
// Populated by (*loader).pkgTest:
867868
testOnce sync.Once
@@ -1184,8 +1185,7 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
11841185
}
11851186

11861187
// updateRequirements ensures that ld.requirements is consistent with the
1187-
// information gained from ld.pkgs and includes the modules in add as roots at
1188-
// at least the given versions.
1188+
// information gained from ld.pkgs.
11891189
//
11901190
// In particular:
11911191
//
@@ -1343,7 +1343,7 @@ func (ld *loader) updateRequirements(ctx context.Context) (changed bool, err err
13431343
//
13441344
// In some sense, we can think of this as ‘upgraded the module providing
13451345
// pkg.path from "none" to a version higher than "none"’.
1346-
if _, _, err = importFromModules(ctx, pkg.path, rs, nil); err == nil {
1346+
if _, _, _, err = importFromModules(ctx, pkg.path, rs, nil); err == nil {
13471347
changed = true
13481348
break
13491349
}
@@ -1554,7 +1554,7 @@ func (ld *loader) preloadRootModules(ctx context.Context, rootPkgs []string) (ch
15541554
// If the main module is tidy and the package is in "all" — or if we're
15551555
// lucky — we can identify all of its imports without actually loading the
15561556
// full module graph.
1557-
m, _, err := importFromModules(ctx, path, ld.requirements, nil)
1557+
m, _, _, err := importFromModules(ctx, path, ld.requirements, nil)
15581558
if err != nil {
15591559
var missing *ImportMissingError
15601560
if errors.As(err, &missing) && ld.ResolveMissingImports {
@@ -1659,7 +1659,7 @@ func (ld *loader) load(ctx context.Context, pkg *loadPkg) {
16591659
}
16601660
}
16611661

1662-
pkg.mod, pkg.dir, pkg.err = importFromModules(ctx, pkg.path, ld.requirements, mg)
1662+
pkg.mod, pkg.dir, pkg.altMods, pkg.err = importFromModules(ctx, pkg.path, ld.requirements, mg)
16631663
if pkg.dir == "" {
16641664
return
16651665
}
@@ -1918,7 +1918,7 @@ func (ld *loader) checkTidyCompatibility(ctx context.Context, rs *Requirements)
19181918

19191919
pkg := pkg
19201920
ld.work.Add(func() {
1921-
mod, _, err := importFromModules(ctx, pkg.path, rs, mg)
1921+
mod, _, _, err := importFromModules(ctx, pkg.path, rs, mg)
19221922
if mod != pkg.mod {
19231923
mismatches := <-mismatchMu
19241924
mismatches[pkg] = mismatch{mod: mod, err: err}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Verifies golang.org/issue/47738.
2+
3+
# In this test, the user has rewritten their imports to use rsc.io/quote/v3,
4+
# but their go.mod still requires rsc.io/[email protected], and they indirectly
5+
# require rsc.io/[email protected] but don't import anything from it.
6+
go list -m -f '{{.Path}}@{{.Version}}{{if .Indirect}} indirect{{end}}' all
7+
stdout '^rsc.io/[email protected]$'
8+
! stdout 'rsc.io/quote/v3'
9+
go list -e all
10+
! stdout '^rsc.io/quote$'
11+
12+
# 'go mod tidy' should preserve the requirement on rsc.io/quote but mark it
13+
# indirect. This prevents a downgrade to v1.5.1, which could introduce
14+
# an ambiguity.
15+
go mod tidy
16+
go list -m -f '{{.Path}}@{{.Version}}{{if .Indirect}} indirect{{end}}' all
17+
stdout '^rsc.io/[email protected] indirect$'
18+
stdout '^rsc.io/quote/[email protected]$'
19+
20+
-- go.mod --
21+
module use
22+
23+
go 1.16
24+
25+
require (
26+
old-indirect v0.0.0
27+
rsc.io/quote v1.5.2
28+
)
29+
30+
replace old-indirect v0.0.0 => ./old-indirect
31+
-- go.sum --
32+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:pvCbr/wm8HzDD3fVywevekufpn6tCGPY3spdHeZJEsw=
33+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
34+
rsc.io/quote v1.5.1/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
35+
rsc.io/quote v1.5.2 h1:3fEykkD9k7lYzXqCYrwGAf7iNhbk4yCjHmKBN9td4L0=
36+
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
37+
rsc.io/sampler v1.3.0 h1:HLGR/BgEtI3r0uymSP/nl2uPLsUnNJX8toRyhfpBTII=
38+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
39+
-- use.go --
40+
package use
41+
42+
import (
43+
_ "old-indirect/empty"
44+
45+
_ "rsc.io/quote/v3"
46+
)
47+
-- old-indirect/empty/empty.go --
48+
package empty
49+
-- old-indirect/go.mod --
50+
module old-indirect
51+
52+
go 1.16
53+
54+
require rsc.io/quote v1.5.1
55+
-- old-indirect/go.sum --
56+
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
57+
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
58+
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

0 commit comments

Comments
 (0)