Skip to content

Commit 814c5ff

Browse files
author
Jay Conrod
committed
cmd/go: support module deprecation
A module is deprecated if its author adds a comment containing a paragraph starting with "Deprecated:" to its go.mod file. The comment must appear immediately before the "module" directive or as a suffix on the same line. The deprecation message runs from just after "Deprecated:" to the end of the paragraph. This is implemented in CL 301089. 'go list -m -u' loads deprecation messages from the latest version of each module, not considering retractions (i.e., deprecations and retractions are loaded from the same version). By default, deprecated modules are printed with a "(deprecated)" suffix. The full deprecation message is available in the -f and -json output. 'go get' prints deprecation warnings for modules named on the command line. It also prints warnings for modules needed to build packages named on the command line if those modules are direct dependencies of the main module. For #40357 Change-Id: Id81fb2b24710681b025becd6cd74f746f4378e78 Reviewed-on: https://go-review.googlesource.com/c/go/+/306334 Trust: Jay Conrod <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent 952187a commit 814c5ff

20 files changed

+550
-55
lines changed

src/cmd/go/alldocs.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/internal/list/list.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ func runList(ctx context.Context, cmd *base.Command, args []string) {
348348
if *listM {
349349
*listFmt = "{{.String}}"
350350
if *listVersions {
351-
*listFmt = `{{.Path}}{{range .Versions}} {{.}}{{end}}`
351+
*listFmt = `{{.Path}}{{range .Versions}} {{.}}{{end}}{{if .Deprecated}} (deprecated){{end}}`
352352
}
353353
} else {
354354
*listFmt = "{{.ImportPath}}"
@@ -453,7 +453,7 @@ func runList(ctx context.Context, cmd *base.Command, args []string) {
453453

454454
var mode modload.ListMode
455455
if *listU {
456-
mode |= modload.ListU | modload.ListRetracted
456+
mode |= modload.ListU | modload.ListRetracted | modload.ListDeprecated
457457
}
458458
if *listRetracted {
459459
mode |= modload.ListRetracted

src/cmd/go/internal/modcmd/edit.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ writing it back to go.mod. The JSON output corresponds to these Go types:
8686
8787
type Module struct {
8888
Path string
89-
Version string
89+
Deprecated string
9090
}
9191
9292
type GoMod struct {
@@ -450,14 +450,19 @@ func flagDropRetract(arg string) {
450450

451451
// fileJSON is the -json output data structure.
452452
type fileJSON struct {
453-
Module module.Version
453+
Module editModuleJSON
454454
Go string `json:",omitempty"`
455455
Require []requireJSON
456456
Exclude []module.Version
457457
Replace []replaceJSON
458458
Retract []retractJSON
459459
}
460460

461+
type editModuleJSON struct {
462+
Path string
463+
Deprecated string `json:",omitempty"`
464+
}
465+
461466
type requireJSON struct {
462467
Path string
463468
Version string `json:",omitempty"`
@@ -479,7 +484,10 @@ type retractJSON struct {
479484
func editPrintJSON(modFile *modfile.File) {
480485
var f fileJSON
481486
if modFile.Module != nil {
482-
f.Module = modFile.Module.Mod
487+
f.Module = editModuleJSON{
488+
Path: modFile.Module.Mod.Path,
489+
Deprecated: modFile.Module.Deprecated,
490+
}
483491
}
484492
if modFile.Go != nil {
485493
f.Go = modFile.Go.Version

src/cmd/go/internal/modget/get.go

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ func runGet(ctx context.Context, cmd *base.Command, args []string) {
354354
pkgPatterns = append(pkgPatterns, q.pattern)
355355
}
356356
}
357-
r.checkPackagesAndRetractions(ctx, pkgPatterns)
357+
r.checkPackageProblems(ctx, pkgPatterns)
358358

359359
// We've already downloaded modules (and identified direct and indirect
360360
// dependencies) by loading packages in findAndUpgradeImports.
@@ -1463,25 +1463,31 @@ func (r *resolver) chooseArbitrarily(cs pathSet) (isPackage bool, m module.Versi
14631463
return false, cs.mod
14641464
}
14651465

1466-
// checkPackagesAndRetractions reloads packages for the given patterns and
1467-
// reports missing and ambiguous package errors. It also reports loads and
1468-
// reports retractions for resolved modules and modules needed to build
1469-
// named packages.
1466+
// checkPackageProblems reloads packages for the given patterns and reports
1467+
// missing and ambiguous package errors. It also reports retractions and
1468+
// deprecations for resolved modules and modules needed to build named packages.
14701469
//
14711470
// We skip missing-package errors earlier in the process, since we want to
14721471
// resolve pathSets ourselves, but at that point, we don't have enough context
14731472
// to log the package-import chains leading to each error.
1474-
func (r *resolver) checkPackagesAndRetractions(ctx context.Context, pkgPatterns []string) {
1473+
func (r *resolver) checkPackageProblems(ctx context.Context, pkgPatterns []string) {
14751474
defer base.ExitIfErrors()
14761475

1477-
// Build a list of modules to load retractions for. Start with versions
1478-
// selected based on command line queries.
1479-
//
1480-
// This is a subset of the build list. If the main module has a lot of
1481-
// dependencies, loading retractions for the entire build list would be slow.
1482-
relevantMods := make(map[module.Version]struct{})
1476+
// Gather information about modules we might want to load retractions and
1477+
// deprecations for. Loading this metadata requires at least one version
1478+
// lookup per module, and we don't want to load information that's neither
1479+
// relevant nor actionable.
1480+
type modFlags int
1481+
const (
1482+
resolved modFlags = 1 << iota // version resolved by 'go get'
1483+
named // explicitly named on command line or provides a named package
1484+
hasPkg // needed to build named packages
1485+
direct // provides a direct dependency of the main module
1486+
)
1487+
relevantMods := make(map[module.Version]modFlags)
14831488
for path, reason := range r.resolvedVersion {
1484-
relevantMods[module.Version{Path: path, Version: reason.version}] = struct{}{}
1489+
m := module.Version{Path: path, Version: reason.version}
1490+
relevantMods[m] |= resolved
14851491
}
14861492

14871493
// Reload packages, reporting errors for missing and ambiguous imports.
@@ -1518,44 +1524,89 @@ func (r *resolver) checkPackagesAndRetractions(ctx context.Context, pkgPatterns
15181524
base.SetExitStatus(1)
15191525
if ambiguousErr := (*modload.AmbiguousImportError)(nil); errors.As(err, &ambiguousErr) {
15201526
for _, m := range ambiguousErr.Modules {
1521-
relevantMods[m] = struct{}{}
1527+
relevantMods[m] |= hasPkg
15221528
}
15231529
}
15241530
}
15251531
if m := modload.PackageModule(pkg); m.Path != "" {
1526-
relevantMods[m] = struct{}{}
1532+
relevantMods[m] |= hasPkg
1533+
}
1534+
}
1535+
for _, match := range matches {
1536+
for _, pkg := range match.Pkgs {
1537+
m := modload.PackageModule(pkg)
1538+
relevantMods[m] |= named
15271539
}
15281540
}
15291541
}
15301542

1531-
// Load and report retractions.
1532-
type retraction struct {
1533-
m module.Version
1534-
err error
1535-
}
1536-
retractions := make([]retraction, 0, len(relevantMods))
1543+
reqs := modload.LoadModFile(ctx)
15371544
for m := range relevantMods {
1538-
retractions = append(retractions, retraction{m: m})
1545+
if reqs.IsDirect(m.Path) {
1546+
relevantMods[m] |= direct
1547+
}
15391548
}
1540-
sort.Slice(retractions, func(i, j int) bool {
1541-
return retractions[i].m.Path < retractions[j].m.Path
1542-
})
1543-
for i := 0; i < len(retractions); i++ {
1549+
1550+
// Load retractions for modules mentioned on the command line and modules
1551+
// needed to build named packages. We care about retractions of indirect
1552+
// dependencies, since we might be able to upgrade away from them.
1553+
type modMessage struct {
1554+
m module.Version
1555+
message string
1556+
}
1557+
retractions := make([]modMessage, 0, len(relevantMods))
1558+
for m, flags := range relevantMods {
1559+
if flags&(resolved|named|hasPkg) != 0 {
1560+
retractions = append(retractions, modMessage{m: m})
1561+
}
1562+
}
1563+
sort.Slice(retractions, func(i, j int) bool { return retractions[i].m.Path < retractions[j].m.Path })
1564+
for i := range retractions {
15441565
i := i
15451566
r.work.Add(func() {
15461567
err := modload.CheckRetractions(ctx, retractions[i].m)
15471568
if retractErr := (*modload.ModuleRetractedError)(nil); errors.As(err, &retractErr) {
1548-
retractions[i].err = err
1569+
retractions[i].message = err.Error()
15491570
}
15501571
})
15511572
}
1573+
1574+
// Load deprecations for modules mentioned on the command line. Only load
1575+
// deprecations for indirect dependencies if they're also direct dependencies
1576+
// of the main module. Deprecations of purely indirect dependencies are
1577+
// not actionable.
1578+
deprecations := make([]modMessage, 0, len(relevantMods))
1579+
for m, flags := range relevantMods {
1580+
if flags&(resolved|named) != 0 || flags&(hasPkg|direct) == hasPkg|direct {
1581+
deprecations = append(deprecations, modMessage{m: m})
1582+
}
1583+
}
1584+
sort.Slice(deprecations, func(i, j int) bool { return deprecations[i].m.Path < deprecations[j].m.Path })
1585+
for i := range deprecations {
1586+
i := i
1587+
r.work.Add(func() {
1588+
deprecation, err := modload.CheckDeprecation(ctx, deprecations[i].m)
1589+
if err != nil || deprecation == "" {
1590+
return
1591+
}
1592+
deprecations[i].message = modload.ShortMessage(deprecation, "")
1593+
})
1594+
}
1595+
15521596
<-r.work.Idle()
1597+
1598+
// Report deprecations, then retractions.
1599+
for _, mm := range deprecations {
1600+
if mm.message != "" {
1601+
fmt.Fprintf(os.Stderr, "go: warning: module %s is deprecated: %s\n", mm.m.Path, mm.message)
1602+
}
1603+
}
15531604
var retractPath string
1554-
for _, r := range retractions {
1555-
if r.err != nil {
1556-
fmt.Fprintf(os.Stderr, "go: warning: %v\n", r.err)
1605+
for _, mm := range retractions {
1606+
if mm.message != "" {
1607+
fmt.Fprintf(os.Stderr, "go: warning: %v\n", mm.message)
15571608
if retractPath == "" {
1558-
retractPath = r.m.Path
1609+
retractPath = mm.m.Path
15591610
} else {
15601611
retractPath = "<module>"
15611612
}

src/cmd/go/internal/modinfo/info.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@ import "time"
1010
// and the fields are documented in the help text in ../list/list.go
1111

1212
type ModulePublic struct {
13-
Path string `json:",omitempty"` // module path
14-
Version string `json:",omitempty"` // module version
15-
Versions []string `json:",omitempty"` // available module versions
16-
Replace *ModulePublic `json:",omitempty"` // replaced by this module
17-
Time *time.Time `json:",omitempty"` // time version was created
18-
Update *ModulePublic `json:",omitempty"` // available update (with -u)
19-
Main bool `json:",omitempty"` // is this the main module?
20-
Indirect bool `json:",omitempty"` // module is only indirectly needed by main module
21-
Dir string `json:",omitempty"` // directory holding local copy of files, if any
22-
GoMod string `json:",omitempty"` // path to go.mod file describing module, if any
23-
GoVersion string `json:",omitempty"` // go version used in module
24-
Retracted []string `json:",omitempty"` // retraction information, if any (with -retracted or -u)
25-
Error *ModuleError `json:",omitempty"` // error loading module
13+
Path string `json:",omitempty"` // module path
14+
Version string `json:",omitempty"` // module version
15+
Versions []string `json:",omitempty"` // available module versions
16+
Replace *ModulePublic `json:",omitempty"` // replaced by this module
17+
Time *time.Time `json:",omitempty"` // time version was created
18+
Update *ModulePublic `json:",omitempty"` // available update (with -u)
19+
Main bool `json:",omitempty"` // is this the main module?
20+
Indirect bool `json:",omitempty"` // module is only indirectly needed by main module
21+
Dir string `json:",omitempty"` // directory holding local copy of files, if any
22+
GoMod string `json:",omitempty"` // path to go.mod file describing module, if any
23+
GoVersion string `json:",omitempty"` // go version used in module
24+
Retracted []string `json:",omitempty"` // retraction information, if any (with -retracted or -u)
25+
Deprecated string `json:",omitempty"` // deprecation message, if any (with -u)
26+
Error *ModuleError `json:",omitempty"` // error loading module
2627
}
2728

2829
type ModuleError struct {
@@ -45,6 +46,9 @@ func (m *ModulePublic) String() string {
4546
s += " [" + versionString(m.Update) + "]"
4647
}
4748
}
49+
if m.Deprecated != "" {
50+
s += " (deprecated)"
51+
}
4852
if m.Replace != nil {
4953
s += " => " + m.Replace.Path
5054
if m.Replace.Version != "" {
@@ -53,6 +57,9 @@ func (m *ModulePublic) String() string {
5357
s += " [" + versionString(m.Replace.Update) + "]"
5458
}
5559
}
60+
if m.Replace.Deprecated != "" {
61+
s += " (deprecated)"
62+
}
5663
}
5764
return s
5865
}

src/cmd/go/internal/modload/build.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ func addUpdate(ctx context.Context, m *modinfo.ModulePublic) {
112112
info, err := Query(ctx, m.Path, "upgrade", m.Version, CheckAllowed)
113113
var noVersionErr *NoMatchingVersionError
114114
if errors.Is(err, fs.ErrNotExist) || errors.As(err, &noVersionErr) {
115-
// Ignore "not found" and "no matching version" errors. This usually means
116-
// the user is offline or the proxy doesn't have a matching version.
115+
// Ignore "not found" and "no matching version" errors.
116+
// This means the proxy has no matching version or no versions at all.
117117
//
118118
// We should report other errors though. An attacker that controls the
119119
// network shouldn't be able to hide versions by interfering with
@@ -163,9 +163,8 @@ func addRetraction(ctx context.Context, m *modinfo.ModulePublic) {
163163
var noVersionErr *NoMatchingVersionError
164164
var retractErr *ModuleRetractedError
165165
if err == nil || errors.Is(err, fs.ErrNotExist) || errors.As(err, &noVersionErr) {
166-
// Ignore "not found" and "no matching version" errors. This usually means
167-
// the user is offline or the proxy doesn't have a go.mod file that could
168-
// contain retractions.
166+
// Ignore "not found" and "no matching version" errors.
167+
// This means the proxy has no matching version or no versions at all.
169168
//
170169
// We should report other errors though. An attacker that controls the
171170
// network shouldn't be able to hide versions by interfering with
@@ -184,6 +183,31 @@ func addRetraction(ctx context.Context, m *modinfo.ModulePublic) {
184183
}
185184
}
186185

186+
// addDeprecation fills in m.Deprecated if the module was deprecated by its
187+
// author. m.Error is set if there's an error loading deprecation information.
188+
func addDeprecation(ctx context.Context, m *modinfo.ModulePublic) {
189+
deprecation, err := CheckDeprecation(ctx, module.Version{Path: m.Path, Version: m.Version})
190+
var noVersionErr *NoMatchingVersionError
191+
if errors.Is(err, fs.ErrNotExist) || errors.As(err, &noVersionErr) {
192+
// Ignore "not found" and "no matching version" errors.
193+
// This means the proxy has no matching version or no versions at all.
194+
//
195+
// We should report other errors though. An attacker that controls the
196+
// network shouldn't be able to hide versions by interfering with
197+
// the HTTPS connection. An attacker that controls the proxy may still
198+
// hide versions, since the "list" and "latest" endpoints are not
199+
// authenticated.
200+
return
201+
}
202+
if err != nil {
203+
if m.Error == nil {
204+
m.Error = &modinfo.ModuleError{Err: err.Error()}
205+
}
206+
return
207+
}
208+
m.Deprecated = deprecation
209+
}
210+
187211
// moduleInfo returns information about module m, loaded from the requirements
188212
// in rs (which may be nil to indicate that m was not loaded from a requirement
189213
// graph).

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ func (rs *Requirements) Graph(ctx context.Context) (*ModuleGraph, error) {
191191
return cached.mg, cached.err
192192
}
193193

194+
// IsDirect returns whether the given module provides a package directly
195+
// imported by a package or test in the main module.
196+
func (rs *Requirements) IsDirect(path string) bool {
197+
return rs.direct[path]
198+
}
199+
194200
// A ModuleGraph represents the complete graph of module dependencies
195201
// of a main module.
196202
//

src/cmd/go/internal/modload/list.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ListMode int
2525
const (
2626
ListU ListMode = 1 << iota
2727
ListRetracted
28+
ListDeprecated
2829
ListVersions
2930
ListRetractedVersions
3031
)
@@ -52,6 +53,9 @@ func ListModules(ctx context.Context, args []string, mode ListMode) ([]*modinfo.
5253
if mode&ListRetracted != 0 {
5354
addRetraction(ctx, m)
5455
}
56+
if mode&ListDeprecated != 0 {
57+
addDeprecation(ctx, m)
58+
}
5559
<-sem
5660
}()
5761
}

0 commit comments

Comments
 (0)