// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bytes" "fmt" "io" "strings" "golang.org/x/exp/apidiff" "golang.org/x/mod/module" "golang.org/x/mod/semver" "golang.org/x/tools/go/packages" ) // report describes the differences in the public API between two versions // of a module. type report struct { // modulePath is the name of the module. modulePath string // baseVersion is the "old" version of the module to compare against. // It may be "none" if there is no base version (for example, if this is // the first release). It may not be "". baseVersion string // baseVersionInferred is true if the base version was determined // automatically (not specified with -base). baseVersionInferred bool // releaseVersion is the version of the module to release, either // proposed with -version or inferred with suggestVersion. releaseVersion string // releaseVersionInferred is true if the release version was suggested // (not specified with -version). releaseVersionInferred bool // tagPrefix is the prefix for VCS tags for this module. For example, // if the module is defined in "foo/bar/v2/go.mod", tagPrefix will be // "foo/bar/". tagPrefix string // packages is a list of package reports, describing the differences // for individual packages, sorted by package path. packages []packageReport // diagnostics is a list of problems unrelated to the module API. // For example, if go.mod is missing some requirements, that will be // reported here. diagnostics []string // versionInvalid explains why the proposed or suggested version is not valid. versionInvalid *versionMessage // haveCompatibleChanges is true if there are any backward-compatible // changes in non-internal packages. haveCompatibleChanges bool // haveIncompatibleChanges is true if there are any backward-incompatible // changes in non-internal packages. haveIncompatibleChanges bool // haveBaseErrors is true if there were errors loading packages // in the base version. haveBaseErrors bool // haveReleaseErrors is true if there were errors loading packages // in the release version. haveReleaseErrors bool } // Text formats and writes a report to w. The report lists errors, compatible // changes, and incompatible changes in each package. If releaseVersion is set, // it states whether releaseVersion is valid (and why). If releaseVersion is not // set, it suggests a new version. func (r *report) Text(w io.Writer) error { buf := &bytes.Buffer{} for _, p := range r.packages { if err := p.Text(buf); err != nil { return err } } if r.baseVersionInferred { fmt.Fprintf(buf, "Inferred base version: %s\n", r.baseVersion) } if len(r.diagnostics) > 0 { for _, d := range r.diagnostics { fmt.Fprintln(buf, d) } } else if r.versionInvalid != nil { fmt.Fprintln(buf, r.versionInvalid) } else if r.releaseVersionInferred { if r.tagPrefix == "" { fmt.Fprintf(buf, "Suggested version: %s\n", r.releaseVersion) } else { fmt.Fprintf(buf, "Suggested version: %[1]s (with tag %[2]s%[1]s)\n", r.releaseVersion, r.tagPrefix) } } else { if r.tagPrefix == "" { fmt.Fprintf(buf, "%s is a valid semantic version for this release.\n", r.releaseVersion) } else { fmt.Fprintf(buf, "%[1]s (with tag %[2]s%[1]s) is a valid semantic version for this release\n", r.releaseVersion, r.tagPrefix) } } if r.versionInvalid == nil && r.haveBaseErrors { fmt.Fprintln(buf, "Errors were found in the base version. Some API changes may be omitted.") } _, err := io.Copy(w, buf) return err } func (r *report) addPackage(p packageReport) { r.packages = append(r.packages, p) if len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 { // Only count compatible and incompatible changes if there were no errors. // When there are errors, definitions may be missing, and fixes may appear // incompatible when they are not. Changes will still be reported, but // they won't affect version validation or suggestions. for _, c := range p.Changes { if !c.Compatible && len(p.releaseErrors) == 0 { r.haveIncompatibleChanges = true } else if c.Compatible && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 { r.haveCompatibleChanges = true } } } if len(p.baseErrors) > 0 { r.haveBaseErrors = true } if len(p.releaseErrors) > 0 { r.haveReleaseErrors = true } } // validateVersion checks whether r.releaseVersion is valid. // If r.releaseVersion is not valid, an error is returned explaining why. // r.releaseVersion must be set. func (r *report) validateVersion() { if r.releaseVersion == "" { panic("validateVersion called without version") } setNotValid := func(format string, args ...interface{}) { r.versionInvalid = &versionMessage{ message: fmt.Sprintf("%s is not a valid semantic version for this release.", r.releaseVersion), reason: fmt.Sprintf(format, args...), } } if r.haveReleaseErrors { if r.haveReleaseErrors { setNotValid("Errors were found in one or more packages.") return } } // TODO(jayconrod): link to documentation for all of these errors. // Check that the major version matches the module path. _, suffix, ok := module.SplitPathVersion(r.modulePath) if !ok { setNotValid("%s: could not find version suffix in module path", r.modulePath) return } if suffix != "" { if suffix[0] != '/' && suffix[0] != '.' { setNotValid("%s: unknown module path version suffix: %q", r.modulePath, suffix) return } pathMajor := suffix[1:] major := semver.Major(r.releaseVersion) if pathMajor != major { setNotValid(`The major version %s does not match the major version suffix in the module path: %s`, major, r.modulePath) return } } else if major := semver.Major(r.releaseVersion); major != "v0" && major != "v1" { setNotValid(`The module path does not end with the major version suffix /%s, which is required for major versions v2 or greater.`, major) return } // Check that compatible / incompatible changes are consistent. if semver.Major(r.baseVersion) == "v0" { return } if r.haveIncompatibleChanges { setNotValid("There are incompatible changes.") return } if r.haveCompatibleChanges && semver.MajorMinor(r.baseVersion) == semver.MajorMinor(r.releaseVersion) { setNotValid(`There are compatible changes, but the minor version is not incremented over the base version (%s).`, r.baseVersion) return } } // suggestVersion suggests a new version consistent with observed changes. func (r *report) suggestVersion() { setNotValid := func(format string, args ...interface{}) { r.versionInvalid = &versionMessage{ message: "Cannot suggest a release version.", reason: fmt.Sprintf(format, args...), } } setVersion := func(v string) { r.releaseVersion = v r.releaseVersionInferred = true } if r.haveReleaseErrors || r.haveBaseErrors { setNotValid("Errors were found.") return } var major, minor, patch, pre string if r.baseVersion != "none" { var err error major, minor, patch, pre, _, err = parseVersion(r.baseVersion) if err != nil { panic(fmt.Sprintf("could not parse base version: %v", err)) } } if r.haveIncompatibleChanges && r.baseVersion != "none" && pre == "" && major != "0" { setNotValid("Incompatible changes were detected.") return // TODO(jayconrod): briefly explain how to prepare major version releases // and link to documentation. } if r.baseVersion == "none" { if _, pathMajor, ok := module.SplitPathVersion(r.modulePath); !ok { panic(fmt.Sprintf("could not parse module path %q", r.modulePath)) } else if pathMajor == "" { setVersion("v0.1.0") } else { setVersion(pathMajor[1:] + ".0.0") } return } if pre != "" { // suggest non-prerelease version } else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") { minor = incDecimal(minor) patch = "0" } else { patch = incDecimal(patch) } setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch)) } // isSuccessful returns true the module appears to be safe to release at the // proposed or suggested version. func (r *report) isSuccessful() bool { return len(r.diagnostics) == 0 && r.versionInvalid == nil } type versionMessage struct { message, reason string } func (m versionMessage) String() string { return m.message + "\n" + m.reason + "\n" } // incDecimal returns the decimal string incremented by 1. func incDecimal(decimal string) string { // Scan right to left turning 9s to 0s until you find a digit to increment. digits := []byte(decimal) i := len(digits) - 1 for ; i >= 0 && digits[i] == '9'; i-- { digits[i] = '0' } if i >= 0 { digits[i]++ } else { // digits is all zeros digits[0] = '1' digits = append(digits, '0') } return string(digits) } type packageReport struct { apidiff.Report path string baseErrors, releaseErrors []packages.Error } func (p *packageReport) Text(w io.Writer) error { if len(p.Changes) == 0 && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 { return nil } buf := &bytes.Buffer{} fmt.Fprintf(buf, "%s\n%s\n", p.path, strings.Repeat("-", len(p.path))) if len(p.baseErrors) > 0 { fmt.Fprintf(buf, "errors in base version:\n") for _, e := range p.baseErrors { fmt.Fprintf(buf, "\t%v\n", e) } buf.WriteByte('\n') } if len(p.releaseErrors) > 0 { fmt.Fprintf(buf, "errors in release version:\n") for _, e := range p.releaseErrors { fmt.Fprintf(buf, "\t%v\n", e) } buf.WriteByte('\n') } if len(p.Changes) > 0 { if err := p.Report.Text(buf); err != nil { return err } buf.WriteByte('\n') } _, err := io.Copy(w, buf) return err } // parseVersion returns the major, minor, and patch numbers, prerelease text, // and metadata for a given version. // // TODO(jayconrod): extend semver to do this and delete this function. func parseVersion(vers string) (major, minor, patch, pre, meta string, err error) { if !strings.HasPrefix(vers, "v") { return "", "", "", "", "", fmt.Errorf("version %q does not start with 'v'", vers) } base := vers[1:] if i := strings.IndexByte(base, '+'); i >= 0 { meta = base[i+1:] base = base[:i] } if i := strings.IndexByte(base, '-'); i >= 0 { pre = base[i+1:] base = base[:i] } parts := strings.Split(base, ".") if len(parts) != 3 { return "", "", "", "", "", fmt.Errorf("version %q should have three numbers", vers) } major, minor, patch = parts[0], parts[1], parts[2] return major, minor, patch, pre, meta, nil }