cmd/gorelease: report a diagnostic error for retracted dependencies

Fixes golang/go#37781

Change-Id: I109ce5da26c757e7e1bdd6bdcee0ff14be35230b
Reviewed-on: https://go-review.googlesource.com/c/exp/+/310370
Trust: Jean de Klerk <deklerk@google.com>
Run-TryBot: Jean de Klerk <deklerk@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/cmd/gorelease/gorelease.go b/cmd/gorelease/gorelease.go
index fffb42d..6347059 100644
--- a/cmd/gorelease/gorelease.go
+++ b/cmd/gorelease/gorelease.go
@@ -80,6 +80,7 @@
 package main
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
@@ -95,6 +96,7 @@
 	"path/filepath"
 	"sort"
 	"strings"
+	"unicode"
 
 	"golang.org/x/exp/apidiff"
 	"golang.org/x/mod/modfile"
@@ -426,6 +428,12 @@
 		m.highestTransitiveVersion = highestVersion
 	}
 
+	retracted, err := loadRetractions(ctx, tmpLoadDir)
+	if err != nil {
+		return moduleInfo{}, err
+	}
+	m.diagnostics = append(m.diagnostics, retracted...)
+
 	return m, nil
 }
 
@@ -1375,3 +1383,76 @@
 	copy(clone, env)
 	return clone
 }
+
+// loadRetractions lists all retracted deps found at the modRoot.
+func loadRetractions(ctx context.Context, modRoot string) ([]string, error) {
+	cmd := exec.CommandContext(ctx, "go", "list", "-json", "-m", "-u", "all")
+	if env, ok := ctx.Value("env").([]string); ok {
+		cmd.Env = env
+	}
+	cmd.Dir = modRoot
+	out, err := cmd.Output()
+	if err != nil {
+		return nil, cleanCmdError(err)
+	}
+
+	var retracted []string
+	type message struct {
+		Path      string
+		Version   string
+		Retracted []string
+	}
+
+	dec := json.NewDecoder(bytes.NewBuffer(out))
+	for {
+		var m message
+		if err := dec.Decode(&m); err == io.EOF {
+			break
+		} else if err != nil {
+			return nil, err
+		}
+		if len(m.Retracted) == 0 {
+			continue
+		}
+		rationale, ok := shortRetractionRationale(m.Retracted)
+		if ok {
+			retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author: %s", m.Path, m.Version, rationale))
+		} else {
+			retracted = append(retracted, fmt.Sprintf("required module %s@%s retracted by module author", m.Path, m.Version))
+		}
+	}
+
+	return retracted, nil
+}
+
+// ShortRetractionRationale returns a retraction rationale string that is safe
+// to print in a terminal. It returns hard-coded strings if the rationale
+// is empty, too long, or contains non-printable characters.
+//
+// It returns true if the rationale was printable, and false if it was not (too
+// long, contains graphics, etc).
+func shortRetractionRationale(rationales []string) (string, bool) {
+	if len(rationales) == 0 {
+		return "", false
+	}
+	rationale := rationales[0]
+
+	const maxRationaleBytes = 500
+	if i := strings.Index(rationale, "\n"); i >= 0 {
+		rationale = rationale[:i]
+	}
+	rationale = strings.TrimSpace(rationale)
+	if rationale == "" || rationale == "retracted by module author" {
+		return "", false
+	}
+	if len(rationale) > maxRationaleBytes {
+		return "", false
+	}
+	for _, r := range rationale {
+		if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
+			return "", false
+		}
+	}
+	// NOTE: the go.mod parser rejects invalid UTF-8, so we don't check that here.
+	return rationale, true
+}
diff --git a/cmd/gorelease/testdata/mod/example.com_retract_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_retract_v0.0.1.txt
new file mode 100644
index 0000000..b1466d0
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retract_v0.0.1.txt
@@ -0,0 +1,13 @@
+-- go.mod --
+module example.com/retract
+
+go 1.12
+
+require example.com/retractdep v1.0.0
+-- go.sum --
+example.com/retractdep v1.0.0 h1:SOVn6jA2ygQY+v8/5aAwxVUJ9teuLrdH/UmbUtp2C44=
+example.com/retractdep v1.0.0/go.mod h1:UjjWSH/ulfbAGgQQwm7pAZ988MFRngUSkJnzcuPsYDI=
+-- a.go --
+package a
+
+import _ "example.com/retractdep"
diff --git a/cmd/gorelease/testdata/mod/example.com_retractdep_v1.0.0.txt b/cmd/gorelease/testdata/mod/example.com_retractdep_v1.0.0.txt
new file mode 100644
index 0000000..36aa3d9
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retractdep_v1.0.0.txt
@@ -0,0 +1,8 @@
+-- go.mod --
+module example.com/retractdep
+
+go 1.12
+-- a.go --
+package a
+
+const A = "a"
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/mod/example.com_retractdep_v1.0.1.txt b/cmd/gorelease/testdata/mod/example.com_retractdep_v1.0.1.txt
new file mode 100644
index 0000000..7548ed2
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retractdep_v1.0.1.txt
@@ -0,0 +1,11 @@
+-- go.mod --
+module example.com/retractdep
+
+go 1.12
+
+// Remote-triggered crash in package foo. See CVE-2021-01234.
+retract v1.0.0
+-- a.go --
+package a
+
+const A = "a"
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/mod/example.com_retractdep_v2_v2.0.0.txt b/cmd/gorelease/testdata/mod/example.com_retractdep_v2_v2.0.0.txt
new file mode 100644
index 0000000..77619e3
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retractdep_v2_v2.0.0.txt
@@ -0,0 +1,12 @@
+# Identical to v1.0.0: just need a new version so that we can test different
+# error messages based on the vX.0.1 retraction comments. We can't test them in
+# the same major version because go mod will always use the latest version's
+# error message.
+-- go.mod --
+module example.com/retractdep/v2
+
+go 1.12
+-- a.go --
+package a
+
+const A = "a"
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/mod/example.com_retractdep_v2_v2.0.1.txt b/cmd/gorelease/testdata/mod/example.com_retractdep_v2_v2.0.1.txt
new file mode 100644
index 0000000..5682baa
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retractdep_v2_v2.0.1.txt
@@ -0,0 +1,10 @@
+-- go.mod --
+module example.com/retractdep/v2
+
+go 1.12
+
+retract v2.0.0
+-- a.go --
+package a
+
+const A = "a"
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/mod/example.com_retractdep_v3_v3.0.0.txt b/cmd/gorelease/testdata/mod/example.com_retractdep_v3_v3.0.0.txt
new file mode 100644
index 0000000..1d526a9
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retractdep_v3_v3.0.0.txt
@@ -0,0 +1,12 @@
+# Identical to v1.0.0: just need a new version so that we can test different
+# error messages based on the vX.0.1 retraction comments. We can't test them in
+# the same major version because go mod will always use the latest version's
+# error message.
+-- go.mod --
+module example.com/retractdep/v3
+
+go 1.12
+-- a.go --
+package a
+
+const A = "a"
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/mod/example.com_retractdep_v3_v3.0.1.txt b/cmd/gorelease/testdata/mod/example.com_retractdep_v3_v3.0.1.txt
new file mode 100644
index 0000000..ed39efb
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retractdep_v3_v3.0.1.txt
@@ -0,0 +1,11 @@
+-- go.mod --
+module example.com/retractdep/v3
+
+go 1.12
+
+// This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message. This is a very long message.
+retract v3.0.0
+-- a.go --
+package a
+
+const A = "a"
\ No newline at end of file
diff --git a/cmd/gorelease/testdata/mod/example.com_retracttransitive_v0.0.1.txt b/cmd/gorelease/testdata/mod/example.com_retracttransitive_v0.0.1.txt
new file mode 100644
index 0000000..885906f
--- /dev/null
+++ b/cmd/gorelease/testdata/mod/example.com_retracttransitive_v0.0.1.txt
@@ -0,0 +1,15 @@
+-- go.mod --
+module example.com/retracttransitive
+
+go 1.12
+
+require example.com/retract v0.0.1
+-- go.sum --
+example.com/retract v0.0.1 h1:Afj8efoHilltHZNLlEARzpc1Vkc5d6ugWKIE/YDmXuQ=
+example.com/retract v0.0.1/go.mod h1:DUqXjcGF3aJhkjxsUjQ0DG65b51DDBvFrEbcr9kkyto=
+example.com/retractdep v1.0.0 h1:SOVn6jA2ygQY+v8/5aAwxVUJ9teuLrdH/UmbUtp2C44=
+example.com/retractdep v1.0.0/go.mod h1:UjjWSH/ulfbAGgQQwm7pAZ988MFRngUSkJnzcuPsYDI=
+-- a.go --
+package a
+
+import _ "example.com/retract"
diff --git a/cmd/gorelease/testdata/retract/retract_verify_direct_dep.test b/cmd/gorelease/testdata/retract/retract_verify_direct_dep.test
new file mode 100644
index 0000000..7e923c5
--- /dev/null
+++ b/cmd/gorelease/testdata/retract/retract_verify_direct_dep.test
@@ -0,0 +1,6 @@
+mod=example.com/retract
+version=v0.0.1
+success=false
+-- want --
+Inferred base version: v0.0.1
+required module example.com/retractdep@v1.0.0 retracted by module author: Remote-triggered crash in package foo. See CVE-2021-01234.
diff --git a/cmd/gorelease/testdata/retract/retract_verify_long_msg.test b/cmd/gorelease/testdata/retract/retract_verify_long_msg.test
new file mode 100644
index 0000000..a5d9fb7
--- /dev/null
+++ b/cmd/gorelease/testdata/retract/retract_verify_long_msg.test
@@ -0,0 +1,18 @@
+mod=example.com/retract
+success=false
+-- want --
+Inferred base version: v0.0.1
+required module example.com/retractdep/v3@v3.0.0 retracted by module author
+-- go.mod --
+module example.com/retract
+
+go 1.12
+
+require example.com/retractdep/v3 v3.0.0
+-- go.sum --
+example.com/retractdep/v3 v3.0.0 h1:LEaqsEpt7J4Er+qSPqL7bENpIkRdZdaOE6KaUaiNB5I=
+example.com/retractdep/v3 v3.0.0/go.mod h1:B2rEwAWayv3FJ2jyeiq9O3UBbxSvdDqZUtxmKsLyg6k=
+-- a.go --
+package a
+
+import _ "example.com/retractdep/v3"
diff --git a/cmd/gorelease/testdata/retract/retract_verify_no_msg.test b/cmd/gorelease/testdata/retract/retract_verify_no_msg.test
new file mode 100644
index 0000000..9a42760
--- /dev/null
+++ b/cmd/gorelease/testdata/retract/retract_verify_no_msg.test
@@ -0,0 +1,18 @@
+mod=example.com/retract
+success=false
+-- want --
+Inferred base version: v0.0.1
+required module example.com/retractdep/v2@v2.0.0 retracted by module author
+-- go.mod --
+module example.com/retract
+
+go 1.12
+
+require example.com/retractdep/v2 v2.0.0
+-- go.sum --
+example.com/retractdep/v2 v2.0.0 h1:ehV4yfX3A3jNlRnBmHPxq1TyVs1EhmCYI5miEva6Gv8=
+example.com/retractdep/v2 v2.0.0/go.mod h1:rV+p/Yqwnupg15GPVGFRq+un/MYczBZcF1IZ8ubecag=
+-- a.go --
+package a
+
+import _ "example.com/retractdep/v2"
diff --git a/cmd/gorelease/testdata/retract/retract_verify_transitive_dep.test b/cmd/gorelease/testdata/retract/retract_verify_transitive_dep.test
new file mode 100644
index 0000000..8338e5f
--- /dev/null
+++ b/cmd/gorelease/testdata/retract/retract_verify_transitive_dep.test
@@ -0,0 +1,8 @@
+# When a retracted version is transitively depended upon, it should still
+# result in a retraction error.
+mod=example.com/retracttransitive
+version=v0.0.1
+success=false
+-- want --
+Inferred base version: v0.0.1
+required module example.com/retractdep@v1.0.0 retracted by module author: Remote-triggered crash in package foo. See CVE-2021-01234.