diff --git a/src/cmd/go/internal/modload/modfile.go b/src/cmd/go/internal/modload/modfile.go index 53a7895c4d..6351871844 100644 --- a/src/cmd/go/internal/modload/modfile.go +++ b/src/cmd/go/internal/modload/modfile.go @@ -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 != "" { @@ -172,8 +152,6 @@ func CheckRetractions(ctx context.Context, m module.Version) error { return nil } -var retractCache par.Cache - type ModuleRetractedError struct { Rationale []string } @@ -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 } @@ -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] +// 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] } - rationale = strings.TrimSpace(rationale) - if rationale == "" { - return "retracted by module author" + message = strings.TrimSpace(message) + if message == "" { + return emptyDefault } - if len(rationale) > maxRationaleBytes { - return "(rationale omitted: too long)" + if len(message) > maxLen { + return "(message omitted: too long)" } - for _, r := range rationale { + 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. @@ -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 diff --git a/src/cmd/go/testdata/script/mod_retract_rationale.txt b/src/cmd/go/testdata/script/mod_retract_rationale.txt index 4d3a3d67c6..823c384e48 100644 --- a/src/cmd/go/testdata/script/mod_retract_rationale.txt +++ b/src/cmd/go/testdata/script/mod_retract_rationale.txt @@ -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 @@ -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