cmd/go: refactor modload.CheckRetractions

Extract queryLatestVersionIgnoringRetractions, which returns the
version we should load retractions and deprecations from. This will be
shared with CheckDeprecations.

Rename ShortRetractionRationale to ShortMessage. This will be used to
shorten deprecation warnings as well.

For #40357

Change-Id: Ic1e0c670396bdb3bd87c7a97cf2b14ca58ea1d80
Reviewed-on: https://go-review.googlesource.com/c/go/+/306332
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
This commit is contained in:
Jay Conrod 2021-03-31 14:25:33 -04:00
parent ee51e3d895
commit 6ed045b365
2 changed files with 101 additions and 76 deletions

View file

@ -92,73 +92,53 @@ func (e *excludedError) Is(err error) bool { return err == ErrDisallowed }
// CheckRetractions returns an error if module m has been retracted by // CheckRetractions returns an error if module m has been retracted by
// its author. // its author.
func CheckRetractions(ctx context.Context, m module.Version) error { func CheckRetractions(ctx context.Context, m module.Version) (err error) {
defer func() {
if retractErr := (*ModuleRetractedError)(nil); err == nil || errors.As(err, &retractErr) {
return
}
// Attribute the error to the version being checked, not the version from
// which the retractions were to be loaded.
if mErr := (*module.ModuleError)(nil); errors.As(err, &mErr) {
err = mErr.Err
}
err = &retractionLoadingError{m: m, err: err}
}()
if m.Version == "" { if m.Version == "" {
// Main module, standard library, or file replacement module. // Main module, standard library, or file replacement module.
// Cannot be retracted. // Cannot be retracted.
return nil return nil
} }
if repl := Replacement(module.Version{Path: m.Path}); repl.Path != "" {
// Look up retraction information from the latest available version of // All versions of the module were replaced.
// the module. Cache retraction information so we don't parse the go.mod // Don't load retractions, since we'd just load the replacement.
// file repeatedly. return nil
type entry struct {
retract []retraction
err error
} }
path := m.Path
e := retractCache.Do(path, func() (v interface{}) {
ctx, span := trace.StartSpan(ctx, "checkRetractions "+path)
defer span.Done()
if repl := Replacement(module.Version{Path: m.Path}); repl.Path != "" { // Find the latest available version of the module, and load its go.mod. If
// All versions of the module were replaced with a local directory. // the latest version is replaced, we'll load the replacement.
// Don't load retractions. //
return &entry{nil, nil} // If there's an error loading the go.mod, we'll return it here. These errors
} // should generally be ignored by callers since they happen frequently when
// we're offline. These errors are not equivalent to ErrDisallowed, so they
// Find the latest version of the module. // may be distinguished from retraction errors.
// Ignore exclusions from the main module's go.mod. //
const ignoreSelected = "" // We load the raw file here: the go.mod file may have a different module
var allowAll AllowedFunc // path that we expect if the module or its repository was renamed.
rev, err := Query(ctx, path, "latest", ignoreSelected, allowAll) // We still want to apply retractions to other aliases of the module.
if err != nil { rm, err := queryLatestVersionIgnoringRetractions(ctx, m.Path)
return &entry{nil, err} if err != nil {
} return err
}
// Load go.mod for that version. summary, err := rawGoModSummary(rm)
// If the version is replaced, we'll load retractions from the replacement. if err != nil {
// return err
// If there's an error loading the go.mod, we'll return it here.
// These errors should generally be ignored by callers of checkRetractions,
// since they happen frequently when we're offline. These errors are not
// equivalent to ErrDisallowed, so they may be distinguished from
// retraction errors.
//
// We load the raw file here: the go.mod file may have a different module
// path that we expect if the module or its repository was renamed.
// We still want to apply retractions to other aliases of the module.
rm := resolveReplacement(module.Version{Path: path, Version: rev.Version})
summary, err := rawGoModSummary(rm)
if err != nil {
return &entry{nil, err}
}
return &entry{summary.retract, nil}
}).(*entry)
if err := e.err; err != nil {
// Attribute the error to the version being checked, not the version from
// which the retractions were to be loaded.
var mErr *module.ModuleError
if errors.As(err, &mErr) {
err = mErr.Err
}
return &retractionLoadingError{m: m, err: err}
} }
var rationale []string var rationale []string
isRetracted := false isRetracted := false
for _, r := range e.retract { for _, r := range summary.retract {
if semver.Compare(r.Low, m.Version) <= 0 && semver.Compare(m.Version, r.High) <= 0 { if semver.Compare(r.Low, m.Version) <= 0 && semver.Compare(m.Version, r.High) <= 0 {
isRetracted = true isRetracted = true
if r.Rationale != "" { if r.Rationale != "" {
@ -172,8 +152,6 @@ func CheckRetractions(ctx context.Context, m module.Version) error {
return nil return nil
} }
var retractCache par.Cache
type ModuleRetractedError struct { type ModuleRetractedError struct {
Rationale []string Rationale []string
} }
@ -183,7 +161,7 @@ func (e *ModuleRetractedError) Error() string {
if len(e.Rationale) > 0 { if len(e.Rationale) > 0 {
// This is meant to be a short error printed on a terminal, so just // This is meant to be a short error printed on a terminal, so just
// print the first rationale. // print the first rationale.
msg += ": " + ShortRetractionRationale(e.Rationale[0]) msg += ": " + ShortMessage(e.Rationale[0], "retracted by module author")
} }
return msg return msg
} }
@ -205,28 +183,31 @@ func (e *retractionLoadingError) Unwrap() error {
return e.err return e.err
} }
// ShortRetractionRationale returns a retraction rationale string that is safe // ShortMessage returns a string from go.mod (for example, a retraction
// to print in a terminal. It returns hard-coded strings if the rationale // rationale or deprecation message) that is safe to print in a terminal.
// is empty, too long, or contains non-printable characters. //
func ShortRetractionRationale(rationale string) string { // If the given string is empty, ShortMessage returns the given default. If the
const maxRationaleBytes = 500 // given string is too long or contains non-printable characters, ShortMessage
if i := strings.Index(rationale, "\n"); i >= 0 { // returns a hard-coded string.
rationale = rationale[:i] func ShortMessage(message, emptyDefault string) string {
const maxLen = 500
if i := strings.Index(message, "\n"); i >= 0 {
message = message[:i]
} }
rationale = strings.TrimSpace(rationale) message = strings.TrimSpace(message)
if rationale == "" { if message == "" {
return "retracted by module author" return emptyDefault
} }
if len(rationale) > maxRationaleBytes { if len(message) > maxLen {
return "(rationale omitted: too long)" return "(message omitted: too long)"
} }
for _, r := range rationale { for _, r := range message {
if !unicode.IsGraphic(r) && !unicode.IsSpace(r) { if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
return "(rationale omitted: contains non-printable characters)" return "(message omitted: contains non-printable characters)"
} }
} }
// NOTE: the go.mod parser rejects invalid UTF-8, so we don't check that here. // NOTE: the go.mod parser rejects invalid UTF-8, so we don't check that here.
return rationale return message
} }
// Replacement returns the replacement for mod, if any, from go.mod. // Replacement returns the replacement for mod, if any, from go.mod.
@ -596,3 +577,47 @@ func rawGoModSummary(m module.Version) (*modFileSummary, error) {
} }
var rawGoModSummaryCache par.Cache // module.Version → rawGoModSummary result var rawGoModSummaryCache par.Cache // module.Version → rawGoModSummary result
// queryLatestVersionIgnoringRetractions looks up the latest version of the
// module with the given path without considering retracted or excluded
// versions.
//
// If all versions of the module are replaced,
// queryLatestVersionIgnoringRetractions returns the replacement without making
// a query.
//
// If the queried latest version is replaced,
// queryLatestVersionIgnoringRetractions returns the replacement.
func queryLatestVersionIgnoringRetractions(ctx context.Context, path string) (latest module.Version, err error) {
type entry struct {
latest module.Version
err error
}
e := latestVersionIgnoringRetractionsCache.Do(path, func() interface{} {
ctx, span := trace.StartSpan(ctx, "queryLatestVersionIgnoringRetractions "+path)
defer span.Done()
if repl := Replacement(module.Version{Path: path}); repl.Path != "" {
// All versions of the module were replaced.
// No need to query.
return &entry{latest: repl}
}
// Find the latest version of the module.
// Ignore exclusions from the main module's go.mod.
const ignoreSelected = ""
var allowAll AllowedFunc
rev, err := Query(ctx, path, "latest", ignoreSelected, allowAll)
if err != nil {
return &entry{err: err}
}
latest := module.Version{Path: path, Version: rev.Version}
if repl := resolveReplacement(latest); repl.Path != "" {
latest = repl
}
return &entry{latest: latest}
}).(*entry)
return e.latest, e.err
}
var latestVersionIgnoringRetractionsCache par.Cache // path → queryLatestVersionIgnoringRetractions result

View file

@ -29,7 +29,7 @@ cmp stdout multiline
# 'go get' should omit long messages. # 'go get' should omit long messages.
go get -d example.com/retract/rationale@v1.0.0-long go get -d example.com/retract/rationale@v1.0.0-long
stderr '^go: warning: example.com/retract/rationale@v1.0.0-long: retracted by module author: \(rationale omitted: too long\)' stderr '^go: warning: example.com/retract/rationale@v1.0.0-long: retracted by module author: \(message omitted: too long\)'
# 'go list' should show the full message. # 'go list' should show the full message.
go list -m -retracted -f '{{.Retracted}}' example.com/retract/rationale go list -m -retracted -f '{{.Retracted}}' example.com/retract/rationale
@ -38,7 +38,7 @@ stdout '^\[lo{500}ng\]$'
# 'go get' should omit messages with unprintable characters. # 'go get' should omit messages with unprintable characters.
go get -d example.com/retract/rationale@v1.0.0-unprintable go get -d example.com/retract/rationale@v1.0.0-unprintable
stderr '^go: warning: example.com/retract/rationale@v1.0.0-unprintable: retracted by module author: \(rationale omitted: contains non-printable characters\)' stderr '^go: warning: example.com/retract/rationale@v1.0.0-unprintable: retracted by module author: \(message omitted: contains non-printable characters\)'
# 'go list' should show the full message. # 'go list' should show the full message.
go list -m -retracted -f '{{.Retracted}}' example.com/retract/rationale go list -m -retracted -f '{{.Retracted}}' example.com/retract/rationale