cmd/go: add -retracted flag to 'go list'

The -retracted flag causes 'go list' to load information about
retracted module module versions.

When -retracted is used with -f or -json, the Retracted field is set
to a string containing the reason for the retraction on retracted
module versions. The string is based on comments on the retract
directive. This field is also populated when the -u flag is used.

When -retracted is used with -versions, retracted versions are shown.
Normally, they are omitted.

For #24031

Change-Id: Ic13d516eddffb1b8404e21034f78cecc9896d1b8
Reviewed-on: https://go-review.googlesource.com/c/go/+/228382
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go
index 609ede4..98861c8 100644
--- a/src/cmd/go/alldocs.go
+++ b/src/cmd/go/alldocs.go
@@ -916,6 +916,7 @@
 //         Dir       string       // directory holding files for this module, if any
 //         GoMod     string       // path to go.mod file used when loading this module, if any
 //         GoVersion string       // go version used in module
+//         Retracted string       // retraction information, if any (with -retracted or -u)
 //         Error     *ModuleError // error loading module
 //     }
 //
@@ -947,14 +948,16 @@
 // The -u flag adds information about available upgrades.
 // When the latest version of a given module is newer than
 // the current one, list -u sets the Module's Update field
-// to information about the newer module.
+// to information about the newer module. list -u will also set
+// the module's Retracted field if the current version is retracted.
 // The Module's String method indicates an available upgrade by
 // formatting the newer version in brackets after the current version.
+// If a version is retracted, the string "(retracted)" will follow it.
 // For example, 'go list -m -u all' might print:
 //
 //     my/main/module
 //     golang.org/x/text v0.3.0 [v0.4.0] => /tmp/text
-//     rsc.io/pdf v0.1.1 [v0.1.2]
+//     rsc.io/pdf v0.1.1 (retracted) [v0.1.2]
 //
 // (For tools, 'go list -m -u -json all' may be more convenient to parse.)
 //
@@ -964,6 +967,14 @@
 // the default output format to display the module path followed by the
 // space-separated version list.
 //
+// The -retracted flag causes list to report information about retracted
+// module versions. When -retracted is used with -f or -json, the Retracted
+// field will be set to a string explaining why the version was retracted.
+// The string is taken from comments on the retract directive in the
+// module's go.mod file. When -retracted is used with -versions, retracted
+// versions are listed together with unretracted versions. The -retracted
+// flag may be used with or without -m.
+//
 // The arguments to list -m are interpreted as a list of modules, not packages.
 // The main module is the module containing the current directory.
 // The active modules are the main module and its dependencies.
diff --git a/src/cmd/go/internal/list/list.go b/src/cmd/go/internal/list/list.go
index e68c39f..6d81c1c 100644
--- a/src/cmd/go/internal/list/list.go
+++ b/src/cmd/go/internal/list/list.go
@@ -10,6 +10,7 @@
 	"bytes"
 	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"os"
 	"sort"
@@ -215,6 +216,7 @@
         Dir       string       // directory holding files for this module, if any
         GoMod     string       // path to go.mod file used when loading this module, if any
         GoVersion string       // go version used in module
+        Retracted string       // retraction information, if any (with -retracted or -u)
         Error     *ModuleError // error loading module
     }
 
@@ -246,14 +248,16 @@
 The -u flag adds information about available upgrades.
 When the latest version of a given module is newer than
 the current one, list -u sets the Module's Update field
-to information about the newer module.
+to information about the newer module. list -u will also set
+the module's Retracted field if the current version is retracted.
 The Module's String method indicates an available upgrade by
 formatting the newer version in brackets after the current version.
+If a version is retracted, the string "(retracted)" will follow it.
 For example, 'go list -m -u all' might print:
 
     my/main/module
     golang.org/x/text v0.3.0 [v0.4.0] => /tmp/text
-    rsc.io/pdf v0.1.1 [v0.1.2]
+    rsc.io/pdf v0.1.1 (retracted) [v0.1.2]
 
 (For tools, 'go list -m -u -json all' may be more convenient to parse.)
 
@@ -263,6 +267,14 @@
 the default output format to display the module path followed by the
 space-separated version list.
 
+The -retracted flag causes list to report information about retracted
+module versions. When -retracted is used with -f or -json, the Retracted
+field will be set to a string explaining why the version was retracted.
+The string is taken from comments on the retract directive in the
+module's go.mod file. When -retracted is used with -versions, retracted
+versions are listed together with unretracted versions. The -retracted
+flag may be used with or without -m.
+
 The arguments to list -m are interpreted as a list of modules, not packages.
 The main module is the module containing the current directory.
 The active modules are the main module and its dependencies.
@@ -296,17 +308,18 @@
 }
 
 var (
-	listCompiled = CmdList.Flag.Bool("compiled", false, "")
-	listDeps     = CmdList.Flag.Bool("deps", false, "")
-	listE        = CmdList.Flag.Bool("e", false, "")
-	listExport   = CmdList.Flag.Bool("export", false, "")
-	listFmt      = CmdList.Flag.String("f", "", "")
-	listFind     = CmdList.Flag.Bool("find", false, "")
-	listJson     = CmdList.Flag.Bool("json", false, "")
-	listM        = CmdList.Flag.Bool("m", false, "")
-	listU        = CmdList.Flag.Bool("u", false, "")
-	listTest     = CmdList.Flag.Bool("test", false, "")
-	listVersions = CmdList.Flag.Bool("versions", false, "")
+	listCompiled  = CmdList.Flag.Bool("compiled", false, "")
+	listDeps      = CmdList.Flag.Bool("deps", false, "")
+	listE         = CmdList.Flag.Bool("e", false, "")
+	listExport    = CmdList.Flag.Bool("export", false, "")
+	listFmt       = CmdList.Flag.String("f", "", "")
+	listFind      = CmdList.Flag.Bool("find", false, "")
+	listJson      = CmdList.Flag.Bool("json", false, "")
+	listM         = CmdList.Flag.Bool("m", false, "")
+	listRetracted = CmdList.Flag.Bool("retracted", false, "")
+	listTest      = CmdList.Flag.Bool("test", false, "")
+	listU         = CmdList.Flag.Bool("u", false, "")
+	listVersions  = CmdList.Flag.Bool("versions", false, "")
 )
 
 var nl = []byte{'\n'}
@@ -367,6 +380,16 @@
 		}
 	}
 
+	modload.Init()
+	if *listRetracted {
+		if cfg.BuildMod == "vendor" {
+			base.Fatalf("go list -retracted cannot be used when vendoring is enabled")
+		}
+		if !modload.Enabled() {
+			base.Fatalf("go list -retracted can only be used in module-aware mode")
+		}
+	}
+
 	if *listM {
 		// Module mode.
 		if *listCompiled {
@@ -416,7 +439,7 @@
 
 		modload.LoadBuildList(ctx)
 
-		mods := modload.ListModules(ctx, args, *listU, *listVersions)
+		mods := modload.ListModules(ctx, args, *listU, *listVersions, *listRetracted)
 		if !*listE {
 			for _, m := range mods {
 				if m.Error != nil {
@@ -607,6 +630,55 @@
 		}
 	}
 
+	// TODO(golang.org/issue/40676): This mechanism could be extended to support
+	// -u without -m.
+	if *listRetracted {
+		// Load retractions for modules that provide packages that will be printed.
+		// TODO(golang.org/issue/40775): Packages from the same module refer to
+		// distinct ModulePublic instance. It would be nice if they could all point
+		// to the same instance. This would require additional global state in
+		// modload.loaded, so that should be refactored first. For now, we update
+		// all instances.
+		modToArg := make(map[*modinfo.ModulePublic]string)
+		argToMods := make(map[string][]*modinfo.ModulePublic)
+		var args []string
+		addModule := func(mod *modinfo.ModulePublic) {
+			if mod.Version == "" {
+				return
+			}
+			arg := fmt.Sprintf("%s@%s", mod.Path, mod.Version)
+			if argToMods[arg] == nil {
+				args = append(args, arg)
+			}
+			argToMods[arg] = append(argToMods[arg], mod)
+			modToArg[mod] = arg
+		}
+		for _, p := range pkgs {
+			if p.Module == nil {
+				continue
+			}
+			addModule(p.Module)
+			if p.Module.Replace != nil {
+				addModule(p.Module.Replace)
+			}
+		}
+
+		if len(args) > 0 {
+			listU := false
+			listVersions := false
+			rmods := modload.ListModules(ctx, args, listU, listVersions, *listRetracted)
+			for i, arg := range args {
+				rmod := rmods[i]
+				for _, mod := range argToMods[arg] {
+					mod.Retracted = rmod.Retracted
+					if rmod.Error != nil && mod.Error == nil {
+						mod.Error = rmod.Error
+					}
+				}
+			}
+		}
+	}
+
 	// Record non-identity import mappings in p.ImportMap.
 	for _, p := range pkgs {
 		for i, srcPath := range p.Internal.RawImports {
diff --git a/src/cmd/go/internal/modcmd/download.go b/src/cmd/go/internal/modcmd/download.go
index d4c161f..41f294d 100644
--- a/src/cmd/go/internal/modcmd/download.go
+++ b/src/cmd/go/internal/modcmd/download.go
@@ -12,8 +12,8 @@
 
 	"cmd/go/internal/base"
 	"cmd/go/internal/cfg"
-	"cmd/go/internal/modload"
 	"cmd/go/internal/modfetch"
+	"cmd/go/internal/modload"
 	"cmd/go/internal/work"
 
 	"golang.org/x/mod/module"
@@ -136,9 +136,10 @@
 	var mods []*moduleJSON
 	listU := false
 	listVersions := false
+	listRetractions := false
 	type token struct{}
 	sem := make(chan token, runtime.GOMAXPROCS(0))
-	for _, info := range modload.ListModules(ctx, args, listU, listVersions) {
+	for _, info := range modload.ListModules(ctx, args, listU, listVersions, listRetractions) {
 		if info.Replace != nil {
 			info = info.Replace
 		}
diff --git a/src/cmd/go/internal/modcmd/why.go b/src/cmd/go/internal/modcmd/why.go
index da33fff..b16887d 100644
--- a/src/cmd/go/internal/modcmd/why.go
+++ b/src/cmd/go/internal/modcmd/why.go
@@ -69,12 +69,13 @@
 	if *whyM {
 		listU := false
 		listVersions := false
+		listRetractions := false
 		for _, arg := range args {
 			if strings.Contains(arg, "@") {
 				base.Fatalf("go mod why: module query not allowed")
 			}
 		}
-		mods := modload.ListModules(ctx, args, listU, listVersions)
+		mods := modload.ListModules(ctx, args, listU, listVersions, listRetractions)
 		byModule := make(map[module.Version][]string)
 		for _, path := range loadALL(ctx) {
 			m := modload.PackageModule(path)
diff --git a/src/cmd/go/internal/modload/build.go b/src/cmd/go/internal/modload/build.go
index a29e085..e9f9a82 100644
--- a/src/cmd/go/internal/modload/build.go
+++ b/src/cmd/go/internal/modload/build.go
@@ -8,6 +8,7 @@
 	"bytes"
 	"context"
 	"encoding/hex"
+	"errors"
 	"fmt"
 	"internal/goroot"
 	"os"
@@ -58,7 +59,9 @@
 	if !ok {
 		return nil
 	}
-	return moduleInfo(context.TODO(), m, true)
+	fromBuildList := true
+	listRetracted := false
+	return moduleInfo(context.TODO(), m, fromBuildList, listRetracted)
 }
 
 func ModuleInfo(ctx context.Context, path string) *modinfo.ModulePublic {
@@ -66,13 +69,17 @@
 		return nil
 	}
 
+	listRetracted := false
 	if i := strings.Index(path, "@"); i >= 0 {
-		return moduleInfo(ctx, module.Version{Path: path[:i], Version: path[i+1:]}, false)
+		m := module.Version{Path: path[:i], Version: path[i+1:]}
+		fromBuildList := false
+		return moduleInfo(ctx, m, fromBuildList, listRetracted)
 	}
 
 	for _, m := range BuildList() {
 		if m.Path == path {
-			return moduleInfo(ctx, m, true)
+			fromBuildList := true
+			return moduleInfo(ctx, m, fromBuildList, listRetracted)
 		}
 	}
 
@@ -100,11 +107,37 @@
 }
 
 // addVersions fills in m.Versions with the list of known versions.
-func addVersions(ctx context.Context, m *modinfo.ModulePublic) {
-	m.Versions, _ = versions(ctx, m.Path, CheckAllowed)
+// Excluded versions will be omitted. If listRetracted is false, retracted
+// versions will also be omitted.
+func addVersions(ctx context.Context, m *modinfo.ModulePublic, listRetracted bool) {
+	allowed := CheckAllowed
+	if listRetracted {
+		allowed = CheckExclusions
+	}
+	m.Versions, _ = versions(ctx, m.Path, allowed)
 }
 
-func moduleInfo(ctx context.Context, m module.Version, fromBuildList bool) *modinfo.ModulePublic {
+// addRetraction fills in m.Retracted if the module was retracted by its author.
+// m.Error is set if there's an error loading retraction information.
+func addRetraction(ctx context.Context, m *modinfo.ModulePublic) {
+	if m.Version == "" {
+		return
+	}
+
+	err := checkRetractions(ctx, module.Version{Path: m.Path, Version: m.Version})
+	var rerr *retractedError
+	if errors.As(err, &rerr) {
+		if len(rerr.rationale) == 0 {
+			m.Retracted = []string{"retracted by module author"}
+		} else {
+			m.Retracted = rerr.rationale
+		}
+	} else if err != nil && m.Error == nil {
+		m.Error = &modinfo.ModuleError{Err: err.Error()}
+	}
+}
+
+func moduleInfo(ctx context.Context, m module.Version, fromBuildList, listRetracted bool) *modinfo.ModulePublic {
 	if m == Target {
 		info := &modinfo.ModulePublic{
 			Path:    m.Path,
@@ -152,6 +185,10 @@
 			if err == nil {
 				m.Dir = dir
 			}
+
+			if listRetracted {
+				addRetraction(ctx, m)
+			}
 		}
 
 		if m.GoVersion == "" {
@@ -205,6 +242,7 @@
 		completeFromModCache(info.Replace)
 		info.Dir = info.Replace.Dir
 		info.GoMod = info.Replace.GoMod
+		info.Retracted = info.Replace.Retracted
 	}
 	info.GoVersion = info.Replace.GoVersion
 	return info
diff --git a/src/cmd/go/internal/modload/list.go b/src/cmd/go/internal/modload/list.go
index a3461ee..8c7b9a3 100644
--- a/src/cmd/go/internal/modload/list.go
+++ b/src/cmd/go/internal/modload/list.go
@@ -20,12 +20,12 @@
 	"golang.org/x/mod/module"
 )
 
-func ListModules(ctx context.Context, args []string, listU, listVersions bool) []*modinfo.ModulePublic {
-	mods := listModules(ctx, args, listVersions)
+func ListModules(ctx context.Context, args []string, listU, listVersions, listRetracted bool) []*modinfo.ModulePublic {
+	mods := listModules(ctx, args, listVersions, listRetracted)
 
 	type token struct{}
 	sem := make(chan token, runtime.GOMAXPROCS(0))
-	if listU || listVersions {
+	if listU || listVersions || listRetracted {
 		for _, m := range mods {
 			add := func(m *modinfo.ModulePublic) {
 				sem <- token{}
@@ -34,7 +34,10 @@
 						addUpdate(ctx, m)
 					}
 					if listVersions {
-						addVersions(ctx, m)
+						addVersions(ctx, m, listRetracted)
+					}
+					if listRetracted || listU {
+						addRetraction(ctx, m)
 					}
 					<-sem
 				}()
@@ -54,10 +57,10 @@
 	return mods
 }
 
-func listModules(ctx context.Context, args []string, listVersions bool) []*modinfo.ModulePublic {
+func listModules(ctx context.Context, args []string, listVersions, listRetracted bool) []*modinfo.ModulePublic {
 	LoadBuildList(ctx)
 	if len(args) == 0 {
-		return []*modinfo.ModulePublic{moduleInfo(ctx, buildList[0], true)}
+		return []*modinfo.ModulePublic{moduleInfo(ctx, buildList[0], true, listRetracted)}
 	}
 
 	var mods []*modinfo.ModulePublic
@@ -84,9 +87,9 @@
 			}
 
 			allowed := CheckAllowed
-			if IsRevisionQuery(vers) {
+			if IsRevisionQuery(vers) || listRetracted {
 				// Allow excluded and retracted versions if the user asked for a
-				// specific revision.
+				// specific revision or used 'go list -retracted'.
 				allowed = nil
 			}
 			info, err := Query(ctx, path, vers, current, allowed)
@@ -98,7 +101,8 @@
 				})
 				continue
 			}
-			mods = append(mods, moduleInfo(ctx, module.Version{Path: path, Version: info.Version}, false))
+			mod := moduleInfo(ctx, module.Version{Path: path, Version: info.Version}, false, listRetracted)
+			mods = append(mods, mod)
 			continue
 		}
 
@@ -123,7 +127,7 @@
 				matched = true
 				if !matchedBuildList[i] {
 					matchedBuildList[i] = true
-					mods = append(mods, moduleInfo(ctx, m, true))
+					mods = append(mods, moduleInfo(ctx, m, true, listRetracted))
 				}
 			}
 		}
@@ -135,7 +139,8 @@
 					// Instead, resolve the module, even if it isn't an existing dependency.
 					info, err := Query(ctx, arg, "latest", "", nil)
 					if err == nil {
-						mods = append(mods, moduleInfo(ctx, module.Version{Path: arg, Version: info.Version}, false))
+						mod := moduleInfo(ctx, module.Version{Path: arg, Version: info.Version}, false, listRetracted)
+						mods = append(mods, mod)
 					} else {
 						mods = append(mods, &modinfo.ModulePublic{
 							Path:  arg,
diff --git a/src/cmd/go/testdata/mod/example.com_retract_missingmod_v1.0.0.txt b/src/cmd/go/testdata/mod/example.com_retract_missingmod_v1.0.0.txt
new file mode 100644
index 0000000..2023c7b
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_missingmod_v1.0.0.txt
@@ -0,0 +1,8 @@
+This version should be retracted, but the go.mod file for the version that would
+contain the retraction is not available.
+-- .mod --
+module example.com/retract/missingmod
+
+go 1.14
+-- .info --
+{"Version":"v1.0.0"}
diff --git a/src/cmd/go/testdata/mod/example.com_retract_missingmod_v1.9.0.txt b/src/cmd/go/testdata/mod/example.com_retract_missingmod_v1.9.0.txt
new file mode 100644
index 0000000..bba919e
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_missingmod_v1.9.0.txt
@@ -0,0 +1,4 @@
+The go.mod file at this version will be loaded to check for retractions
+of earlier versions. However, the .mod file is not available.
+-- .info --
+{"Version":"v1.9.0"}
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_all_v1.9.0.txt b/src/cmd/go/testdata/mod/example.com_retract_self_all_v1.9.0.txt
new file mode 100644
index 0000000..4dc486b
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_self_all_v1.9.0.txt
@@ -0,0 +1,14 @@
+Module example.com/retract/self/prev is a module that retracts its own
+latest version.
+
+No unretracted versions are available.
+
+-- .mod --
+module example.com/retract/self/all
+
+go 1.15
+
+retract v1.9.0 // bad
+
+-- .info --
+{"Version":"v1.9.0"}
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.0.0.txt b/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.0.0.txt
new file mode 100644
index 0000000..04c2845
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.0.0.txt
@@ -0,0 +1,16 @@
+Module example.com/retract/self/prerelease is a module that retracts its own
+latest version and all other release version.
+
+A pre-release version higher than the highest release version is still
+available, and that should be matched by @latest.
+
+-- .mod --
+module example.com/retract/self/prerelease
+
+go 1.15
+
+-- .info --
+{"Version":"v1.0.0"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.9.0.txt b/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.9.0.txt
new file mode 100644
index 0000000..7c1c047
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.9.0.txt
@@ -0,0 +1,19 @@
+Module example.com/retract/self/prerelease is a module that retracts its own
+latest version and all other release version.
+
+A pre-release version higher than the highest release version is still
+available, and that should be matched by @latest.
+
+-- .mod --
+module example.com/retract/self/prerelease
+
+go 1.15
+
+retract v1.0.0 // bad
+retract v1.9.0 // self
+
+-- .info --
+{"Version":"v1.9.0"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.9.1-pre.txt b/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.9.1-pre.txt
new file mode 100644
index 0000000..abf44fd
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_self_prerelease_v1.9.1-pre.txt
@@ -0,0 +1,16 @@
+Module example.com/retract/self/prerelease is a module that retracts its own
+latest version and all other release version.
+
+A pre-release version higher than the highest release version is still
+available, and that should be matched by @latest.
+
+-- .mod --
+module example.com/retract/self/prerelease
+
+go 1.15
+
+-- .info --
+{"Version":"v1.9.1-pre"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v0.0.0-20200325131415-0123456789ab b/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v0.0.0-20200325131415-0123456789ab
new file mode 100644
index 0000000..f9ab41e
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v0.0.0-20200325131415-0123456789ab
@@ -0,0 +1,20 @@
+See example.com_retract_self_pseudo_v1.9.0.txt.
+
+This version is not retracted. It should be returned by the proxy's
+@latest endpoint. It should match the @latest version query.
+
+TODO(golang.org/issue/24031): the proxy and proxy.golang.org both return
+the highest release version from the @latest endpoint, even if that
+version is retracted, so there is no way for the go command to
+discover an unretracted pseudo-version.
+
+-- .mod --
+module example.com/retract/self/pseudo
+
+go 1.15
+
+-- .info --
+{"Version":"v0.0.0-20200325131415-01234567890ab"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v1.0.0-bad.txt b/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v1.0.0-bad.txt
new file mode 100644
index 0000000..d47eda0
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v1.0.0-bad.txt
@@ -0,0 +1,14 @@
+See example.com_retract_self_pseudo_v1.9.0.txt.
+
+This version is retracted.
+
+-- .mod --
+module example.com/retract/self/pseudo
+
+go 1.15
+
+-- .info --
+{"Version":"v1.0.0-bad"}
+
+-- p.go --
+package p
diff --git a/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v1.9.0.txt b/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v1.9.0.txt
new file mode 100644
index 0000000..db09cc6
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_retract_self_pseudo_v1.9.0.txt
@@ -0,0 +1,16 @@
+Module example.com/retract/self/pseudo is a module that retracts its own
+latest version, as well as an earlier version.
+
+An unretracted pseudo-version is available.
+
+-- .mod --
+module example.com/retract/self/pseudo
+
+go 1.15
+
+retract v1.0.0-bad // bad
+retract v1.9.0 // self
+
+-- .info --
+{"Version":"v1.9.0"}
+
diff --git a/src/cmd/go/testdata/script/mod_list_pseudo.txt b/src/cmd/go/testdata/script/mod_list_pseudo.txt
index 3a10b3a..056c093 100644
--- a/src/cmd/go/testdata/script/mod_list_pseudo.txt
+++ b/src/cmd/go/testdata/script/mod_list_pseudo.txt
@@ -10,30 +10,25 @@
 go mod download github.com/dmitshur-test/modtest5@v0.5.0-alpha.0.20190619023908-3da23a9deb9e
 cmp $GOPATH/pkg/mod/cache/download/github.com/dmitshur-test/modtest5/@v/list $WORK/modtest5.list
 
+env GOSUMDB=off # don't verify go.mod files when loading retractions
 env GOPROXY=file:///$GOPATH/pkg/mod/cache/download
 env GOPATH=$WORK/gopath2
 mkdir $GOPATH
 
-go list -m -json github.com/dmitshur-test/modtest5@latest
-cmp stdout $WORK/modtest5.json
+go list -m -f '{{.Path}} {{.Version}} {{.Time.Format "2006-01-02"}}' github.com/dmitshur-test/modtest5@latest
+stdout '^github.com/dmitshur-test/modtest5 v0.5.0-alpha 2019-06-18$'
 
 # If the module proxy contains only pseudo-versions, 'latest' should stat
 # the version with the most recent timestamp — not the highest semantic
 # version — and return its metadata.
 env GOPROXY=file:///$WORK/tinyproxy
-go list -m -json dmitri.shuralyov.com/test/modtest3@latest
-cmp stdout $WORK/modtest3.json
+go list -m -f '{{.Path}} {{.Version}} {{.Time.Format "2006-01-02"}}' dmitri.shuralyov.com/test/modtest3@latest
+stdout '^dmitri.shuralyov.com/test/modtest3 v0.0.0-20181023043359-a85b471d5412 2018-10-22$'
 
 -- $WORK/modtest5.list --
 v0.0.0-20190619020302-197a620e0c9a
 v0.5.0-alpha
 v0.5.0-alpha.0.20190619023908-3da23a9deb9e
--- $WORK/modtest5.json --
-{
-	"Path": "github.com/dmitshur-test/modtest5",
-	"Version": "v0.5.0-alpha",
-	"Time": "2019-06-18T19:04:46-07:00"
-}
 -- $WORK/tinyproxy/dmitri.shuralyov.com/test/modtest3/@v/list --
 v0.1.0-0.20161023043300-000000000000
 v0.0.0-20181023043359-a85b471d5412
@@ -42,9 +37,3 @@
 	"Version": "v0.0.0-20181023043359-a85b471d5412",
 	"Time": "2018-10-22T21:33:59-07:00"
 }
--- $WORK/modtest3.json --
-{
-	"Path": "dmitri.shuralyov.com/test/modtest3",
-	"Version": "v0.0.0-20181023043359-a85b471d5412",
-	"Time": "2018-10-22T21:33:59-07:00"
-}
diff --git a/src/cmd/go/testdata/script/mod_list_retract.txt b/src/cmd/go/testdata/script/mod_list_retract.txt
new file mode 100644
index 0000000..4e177b3
--- /dev/null
+++ b/src/cmd/go/testdata/script/mod_list_retract.txt
@@ -0,0 +1,108 @@
+# 'go list -mod=vendor -retracted' reports an error.
+go mod vendor
+! go list -m -retracted -mod=vendor
+stderr '^go list -retracted cannot be used when vendoring is enabled$'
+rm vendor
+
+# 'go list -retracted' reports an error in GOPATH mode.
+env GO111MODULE=off
+! go list -retracted
+stderr '^go list -retracted can only be used in module-aware mode$'
+env GO111MODULE=
+
+# 'go list pkg' does not show retraction.
+go list -f '{{with .Module}}{{with .Retracted}}retracted{{end}}{{end}}' example.com/retract
+! stdout .
+
+# 'go list -retracted pkg' shows retraction.
+go list -retracted -f '{{with .Module}}{{with .Retracted}}retracted{{end}}{{end}}' example.com/retract
+stdout retracted
+
+# 'go list -m' does not show retraction.
+go list -m -f '{{with .Retracted}}retracted{{end}}' example.com/retract
+! stdout .
+
+# 'go list -m -retracted' shows retraction.
+go list -m -retracted -f '{{with .Retracted}}retracted{{end}}' example.com/retract
+
+# 'go list -m mod@version' does not show retraction.
+go list -m -f '{{with .Retracted}}retracted{{end}}' example.com/retract@v1.0.0-unused
+! stdout .
+
+# 'go list -m -retracted mod@version' shows an error if the go.mod that should
+# contain the retractions is not available.
+! go list -m -retracted example.com/retract/missingmod@v1.0.0
+stderr '^go list -m: loading module retractions: example.com/retract/missingmod@v1.9.0:.*404 Not Found$'
+go list -e -m -retracted -f '{{.Error.Err}}' example.com/retract/missingmod@v1.0.0
+stdout '^loading module retractions: example.com/retract/missingmod@v1.9.0:.*404 Not Found$'
+
+# 'go list -m -retracted mod@version' shows retractions.
+go list -m -retracted example.com/retract@v1.0.0-unused
+stdout '^example.com/retract v1.0.0-unused \(retracted\)$'
+go list -m -retracted -f '{{with .Retracted}}retracted{{end}}' example.com/retract@v1.0.0-unused
+stdout retracted
+
+# 'go list -m mod@latest' selects a previous release version, not self-retracted latest.
+go list -m -f '{{.Version}}{{with .Retracted}} retracted{{end}}' example.com/retract/self/prev@latest
+stdout '^v1.1.0$'
+
+# 'go list -m -retracted mod@latest' selects the self-retracted latest version.
+go list -m -retracted -f '{{.Version}}{{with .Retracted}} retracted{{end}}' example.com/retract/self/prev@latest
+stdout '^v1.9.0 retracted$'
+
+# 'go list -m mod@latest' selects a pre-release version if all release versions are retracted.
+go list -m -f '{{.Version}}{{with .Retracted}} retracted{{end}}' example.com/retract/self/prerelease@latest
+stdout '^v1.9.1-pre$'
+
+# 'go list -m -retracted mod@latest' selects the self-retracted latest version.
+go list -m -retracted -f '{{.Version}}{{with .Retracted}} retracted{{end}}' example.com/retract/self/prerelease@latest
+stdout '^v1.9.0 retracted$'
+
+# 'go list -m mod@latest' selects a pseudo-version if all versions are retracted.
+# TODO(golang.org/issue/24031): the proxy does not expose the pseudo-version,
+# even if all release versions are retracted.
+go list -m -e -f '{{.Error.Err}}' example.com/retract/self/pseudo@latest
+stdout '^module example.com/retract/self/pseudo: no matching versions for query "latest"$'
+
+# 'go list -m mod@latest' reports an error if all versions are retracted.
+go list -m -e -f '{{.Error.Err}}' example.com/retract/self/all@latest
+stdout '^module example.com/retract/self/all: no matching versions for query "latest"$'
+
+# 'go list -m mod@<v1.10' selects a previous release version, not self-retracted latest.
+# The @latest query is not special with respect to retractions.
+go list -m -f '{{.Version}}{{with .Retracted}} retracted{{end}}' example.com/retract/self/prev@<v1.10
+stdout '^v1.1.0$'
+
+# 'go list -m -versions' hides retracted versions.
+go list -m -versions example.com/retract
+stdout '^example.com/retract v1.0.0-good v1.1.0$'
+
+# 'go list -m -retracted -versions' shows retracted versions.
+go list -m -retracted -versions example.com/retract
+stdout '^example.com/retract v1.0.0-bad v1.0.0-good v1.0.0-unused v1.1.0$'
+
+# 'go list -m -u -versions' loads retractions and does not show retracted versions.
+go list -m -u -versions example.com/retract
+stdout '^example.com/retract v1.0.0-good v1.1.0$'
+go list -m -u -versions -f '{{with .Retracted}}retracted{{end}}' example.com/retract
+stdout retracted
+
+# 'go list -m -u' shows retraction.
+go list -m -u -f '{{with .Retracted}}retracted{{end}}' example.com/retract
+stdout retracted
+
+# 'go list -m -u' does not suggest an update to a self-retracted latest version.
+go list -m -u -f '{{with .Update}}{{.Version}}{{with .Retracted}} retracted{{end}}{{end}}' example.com/retract/self/prev@v1.0.0-bad
+stdout '^v1.1.0$'
+
+-- go.mod --
+module example.com/use
+
+go 1.15
+
+require example.com/retract v1.0.0-bad
+
+-- use.go --
+package use
+
+import _ "example.com/retract"
diff --git a/src/cmd/go/testdata/script/mod_retract_replace.txt b/src/cmd/go/testdata/script/mod_retract_replace.txt
new file mode 100644
index 0000000..b710485
--- /dev/null
+++ b/src/cmd/go/testdata/script/mod_retract_replace.txt
@@ -0,0 +1,51 @@
+# If the latest unretracted version of a module is replaced, 'go list' should
+# obtain retractions from the replacement.
+
+# The latest version, v1.9.0, is not available on the proxy.
+! go list -m -retracted example.com/retract/missingmod
+stderr '^go list -m: loading module retractions: example.com/retract/missingmod@v1.9.0:.*404 Not Found$'
+
+# If we replace that version, we should see retractions.
+go mod edit -replace=example.com/retract/missingmod@v1.9.0=./missingmod-v1.9.0
+go list -m -retracted -f '{{range .Retracted}}{{.}}{{end}}' example.com/retract/missingmod
+stdout '^bad version$'
+
+# If we replace the retracted version, we should not see a retraction.
+go mod edit -replace=example.com/retract/missingmod=./missingmod-v1.9.0
+go list -m -retracted -f '{{if not .Retracted}}good version{{end}}' example.com/retract/missingmod
+stdout '^good version$'
+
+
+# If a replacement version is retracted, we should see a retraction.
+# It should appear in both the replaced module and the replacement, as other
+# fields like GoMod do.
+go list -m -retracted -f '{{range .Retracted}}{{.}}{{end}}' example.com/retract
+! stdout .
+go list -m -retracted -f '{{if .Replace}}replaced{{end}}' example.com/retract
+! stdout .
+go mod edit -replace example.com/retract@v1.0.0-good=example.com/retract@v1.0.0-bad
+go list -m -retracted -f '{{range .Retracted}}{{.}}{{end}}' example.com/retract
+stdout '^bad$'
+go list -m -retracted -f '{{with .Replace}}{{range .Retracted}}{{.}}{{end}}{{end}}' example.com/retract
+stdout '^bad$'
+
+-- go.mod --
+module m
+
+go 1.14
+
+require (
+	example.com/retract v1.0.0-good
+	example.com/retract/missingmod v1.0.0
+)
+-- missingmod-v1.0.0/go.mod --
+module example.com/retract/missingmod
+
+go 1.14
+-- missingmod-v1.9.0/go.mod --
+module example.com/retract/missingmod
+
+go 1.14
+
+// bad version
+retract v1.0.0