Skip to content

Commit

Permalink
cmd/go: refactor modload.CheckRetractions
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Jay Conrod committed Apr 5, 2021
1 parent ee51e3d commit 6ed045b
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 79 deletions.
179 changes: 102 additions & 77 deletions src/cmd/go/internal/modload/modfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// 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 == "" {
// Main module, standard library, or file replacement module.
// Cannot be retracted.
return nil
}

// Look up retraction information from the latest available version of
// the module. Cache retraction information so we don't parse the go.mod
// file repeatedly.
type entry struct {
retract []retraction
err error
if repl := Replacement(module.Version{Path: m.Path}); repl.Path != "" {
// All versions of the module were replaced.
// Don't load retractions, since we'd just load the replacement.
return nil
}
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 != "" {
// All versions of the module were replaced with a local directory.
// Don't load retractions.
return &entry{nil, nil}
}

// 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{nil, err}
}

// Load go.mod for that version.
// If the version is replaced, we'll load retractions from the replacement.
//
// 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}
// Find the latest available version of the module, and load its go.mod. If
// the latest version is replaced, we'll load the replacement.
//
// 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
// 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, err := queryLatestVersionIgnoringRetractions(ctx, m.Path)
if err != nil {
return err
}
summary, err := rawGoModSummary(rm)
if err != nil {
return err
}

var rationale []string
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 {
isRetracted = true
if r.Rationale != "" {
Expand All @@ -172,8 +152,6 @@ func CheckRetractions(ctx context.Context, m module.Version) error {
return nil
}

var retractCache par.Cache

type ModuleRetractedError struct {
Rationale []string
}
Expand All @@ -183,7 +161,7 @@ func (e *ModuleRetractedError) Error() string {
if len(e.Rationale) > 0 {
// This is meant to be a short error printed on a terminal, so just
// print the first rationale.
msg += ": " + ShortRetractionRationale(e.Rationale[0])
msg += ": " + ShortMessage(e.Rationale[0], "retracted by module author")
}
return msg
}
Expand All @@ -205,28 +183,31 @@ func (e *retractionLoadingError) Unwrap() error {
return e.err
}

// 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.
func ShortRetractionRationale(rationale string) string {
const maxRationaleBytes = 500
if i := strings.Index(rationale, "\n"); i >= 0 {
rationale = rationale[:i]
}
rationale = strings.TrimSpace(rationale)
if rationale == "" {
return "retracted by module author"
}
if len(rationale) > maxRationaleBytes {
return "(rationale omitted: too long)"
}
for _, r := range rationale {
// ShortMessage returns a string from go.mod (for example, a retraction
// rationale or deprecation message) that is safe to print in a terminal.
//
// If the given string is empty, ShortMessage returns the given default. If the
// given string is too long or contains non-printable characters, ShortMessage
// returns a hard-coded string.
func ShortMessage(message, emptyDefault string) string {
const maxLen = 500
if i := strings.Index(message, "\n"); i >= 0 {
message = message[:i]
}
message = strings.TrimSpace(message)
if message == "" {
return emptyDefault
}
if len(message) > maxLen {
return "(message omitted: too long)"
}
for _, r := range message {
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.
return rationale
return message
}

// Replacement returns the replacement for mod, if any, from go.mod.
Expand Down Expand Up @@ -596,3 +577,47 @@ func rawGoModSummary(m module.Version) (*modFileSummary, error) {
}

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
4 changes: 2 additions & 2 deletions src/cmd/go/testdata/script/mod_retract_rationale.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cmp stdout multiline

# 'go get' should omit long messages.
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 -m -retracted -f '{{.Retracted}}' example.com/retract/rationale
Expand All @@ -38,7 +38,7 @@ stdout '^\[lo{500}ng\]$'

# 'go get' should omit messages with unprintable characters.
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 -m -retracted -f '{{.Retracted}}' example.com/retract/rationale
Expand Down

0 comments on commit 6ed045b

Please sign in to comment.