golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/cmd/gorelease/report.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"strings"
    10  
    11  	"golang.org/x/exp/apidiff"
    12  	"golang.org/x/mod/module"
    13  	"golang.org/x/mod/semver"
    14  	"golang.org/x/tools/go/packages"
    15  )
    16  
    17  // report describes the differences in the public API between two versions
    18  // of a module.
    19  type report struct {
    20  	// base contains information about the "old" module version being compared
    21  	// against. base.version may be "none", indicating there is no base version
    22  	// (for example, if this is the first release). base.version may not be "".
    23  	base moduleInfo
    24  
    25  	// release contains information about the version of the module to release.
    26  	// The version may be set explicitly with -version or suggested using
    27  	// suggestVersion, in which case release.versionInferred is true.
    28  	release moduleInfo
    29  
    30  	// packages is a list of package reports, describing the differences
    31  	// for individual packages, sorted by package path.
    32  	packages []packageReport
    33  
    34  	// versionInvalid explains why the proposed or suggested version is not valid.
    35  	versionInvalid *versionMessage
    36  
    37  	// haveCompatibleChanges is true if there are any backward-compatible
    38  	// changes in non-internal packages.
    39  	haveCompatibleChanges bool
    40  
    41  	// haveIncompatibleChanges is true if there are any backward-incompatible
    42  	// changes in non-internal packages.
    43  	haveIncompatibleChanges bool
    44  
    45  	// haveBaseErrors is true if there were errors loading packages
    46  	// in the base version.
    47  	haveBaseErrors bool
    48  
    49  	// haveReleaseErrors is true if there were errors loading packages
    50  	// in the release version.
    51  	haveReleaseErrors bool
    52  }
    53  
    54  // String returns a human-readable report that lists errors, compatible changes,
    55  // and incompatible changes in each package. If releaseVersion is set, the
    56  // report states whether releaseVersion is valid (and why). If releaseVersion is
    57  // not set, it suggests a new version.
    58  func (r *report) String() string {
    59  	buf := &strings.Builder{}
    60  	for _, p := range r.packages {
    61  		buf.WriteString(p.String())
    62  	}
    63  
    64  	if !r.canVerifyReleaseVersion() {
    65  		return buf.String()
    66  	}
    67  
    68  	if len(r.release.diagnostics) > 0 {
    69  		buf.WriteString("# diagnostics\n")
    70  		for _, d := range r.release.diagnostics {
    71  			fmt.Fprintln(buf, d)
    72  		}
    73  		buf.WriteByte('\n')
    74  	}
    75  
    76  	buf.WriteString("# summary\n")
    77  	baseVersion := r.base.version
    78  	if r.base.modPath != r.release.modPath {
    79  		baseVersion = r.base.modPath + "@" + baseVersion
    80  	}
    81  	if r.base.versionInferred {
    82  		fmt.Fprintf(buf, "Inferred base version: %s\n", baseVersion)
    83  	} else if r.base.versionQuery != "" {
    84  		fmt.Fprintf(buf, "Base version: %s (%s)\n", baseVersion, r.base.versionQuery)
    85  	}
    86  
    87  	if r.versionInvalid != nil {
    88  		fmt.Fprintln(buf, r.versionInvalid)
    89  	} else if r.release.versionInferred {
    90  		if r.release.tagPrefix == "" {
    91  			fmt.Fprintf(buf, "Suggested version: %s\n", r.release.version)
    92  		} else {
    93  			fmt.Fprintf(buf, "Suggested version: %[1]s (with tag %[2]s%[1]s)\n", r.release.version, r.release.tagPrefix)
    94  		}
    95  	} else if r.release.version != "" {
    96  		if r.release.tagPrefix == "" {
    97  			fmt.Fprintf(buf, "%s is a valid semantic version for this release.\n", r.release.version)
    98  
    99  			if semver.Compare(r.release.version, "v0.0.0-99999999999999-zzzzzzzzzzzz") < 0 {
   100  				fmt.Fprintf(buf, `Note: %s sorts lower in MVS than pseudo-versions, which may be
   101  unexpected for users. So, it may be better to choose a different suffix.`, r.release.version)
   102  			}
   103  		} else {
   104  			fmt.Fprintf(buf, "%[1]s (with tag %[2]s%[1]s) is a valid semantic version for this release\n", r.release.version, r.release.tagPrefix)
   105  		}
   106  	}
   107  
   108  	if r.versionInvalid == nil && r.haveBaseErrors {
   109  		fmt.Fprintln(buf, "Errors were found in the base version. Some API changes may be omitted.")
   110  	}
   111  
   112  	return buf.String()
   113  }
   114  
   115  func (r *report) addPackage(p packageReport) {
   116  	r.packages = append(r.packages, p)
   117  	if len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
   118  		// Only count compatible and incompatible changes if there were no errors.
   119  		// When there are errors, definitions may be missing, and fixes may appear
   120  		// incompatible when they are not. Changes will still be reported, but
   121  		// they won't affect version validation or suggestions.
   122  		for _, c := range p.Changes {
   123  			if !c.Compatible && len(p.releaseErrors) == 0 {
   124  				r.haveIncompatibleChanges = true
   125  			} else if c.Compatible && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
   126  				r.haveCompatibleChanges = true
   127  			}
   128  		}
   129  	}
   130  	if len(p.baseErrors) > 0 {
   131  		r.haveBaseErrors = true
   132  	}
   133  	if len(p.releaseErrors) > 0 {
   134  		r.haveReleaseErrors = true
   135  	}
   136  }
   137  
   138  // validateReleaseVersion checks whether r.release.version is valid.
   139  // If r.release.version is not valid, an error is returned explaining why.
   140  // r.release.version must be set.
   141  func (r *report) validateReleaseVersion() {
   142  	if r.release.version == "" {
   143  		panic("validateVersion called without version")
   144  	}
   145  	setNotValid := func(format string, args ...interface{}) {
   146  		r.versionInvalid = &versionMessage{
   147  			message: fmt.Sprintf("%s is not a valid semantic version for this release.", r.release.version),
   148  			reason:  fmt.Sprintf(format, args...),
   149  		}
   150  	}
   151  
   152  	if r.haveReleaseErrors {
   153  		if r.haveReleaseErrors {
   154  			setNotValid("Errors were found in one or more packages.")
   155  			return
   156  		}
   157  	}
   158  
   159  	// TODO(jayconrod): link to documentation for all of these errors.
   160  
   161  	// Check that the major version matches the module path.
   162  	_, suffix, ok := module.SplitPathVersion(r.release.modPath)
   163  	if !ok {
   164  		setNotValid("%s: could not find version suffix in module path", r.release.modPath)
   165  		return
   166  	}
   167  	if suffix != "" {
   168  		if suffix[0] != '/' && suffix[0] != '.' {
   169  			setNotValid("%s: unknown module path version suffix: %q", r.release.modPath, suffix)
   170  			return
   171  		}
   172  		pathMajor := suffix[1:]
   173  		major := semver.Major(r.release.version)
   174  		if pathMajor != major {
   175  			setNotValid(`The major version %s does not match the major version suffix
   176  in the module path: %s`, major, r.release.modPath)
   177  			return
   178  		}
   179  	} else if major := semver.Major(r.release.version); major != "v0" && major != "v1" {
   180  		setNotValid(`The module path does not end with the major version suffix /%s,
   181  which is required for major versions v2 or greater.`, major)
   182  		return
   183  	}
   184  
   185  	for _, v := range r.base.existingVersions {
   186  		if semver.Compare(v, r.release.version) == 0 {
   187  			setNotValid("version %s already exists", v)
   188  		}
   189  	}
   190  
   191  	// Check that compatible / incompatible changes are consistent.
   192  	if semver.Major(r.base.version) == "v0" || r.base.modPath != r.release.modPath {
   193  		return
   194  	}
   195  	if r.haveIncompatibleChanges {
   196  		setNotValid("There are incompatible changes.")
   197  		return
   198  	}
   199  	if r.haveCompatibleChanges && semver.MajorMinor(r.base.version) == semver.MajorMinor(r.release.version) {
   200  		setNotValid(`There are compatible changes, but the minor version is not incremented
   201  over the base version (%s).`, r.base.version)
   202  		return
   203  	}
   204  
   205  	if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, r.release.version) > 0 {
   206  		setNotValid(`Module indirectly depends on a higher version of itself (%s).
   207  		`, r.release.highestTransitiveVersion)
   208  	}
   209  }
   210  
   211  // suggestReleaseVersion suggests a new version consistent with observed
   212  // changes.
   213  func (r *report) suggestReleaseVersion() {
   214  	setNotValid := func(format string, args ...interface{}) {
   215  		r.versionInvalid = &versionMessage{
   216  			message: "Cannot suggest a release version.",
   217  			reason:  fmt.Sprintf(format, args...),
   218  		}
   219  	}
   220  	setVersion := func(v string) {
   221  		r.release.version = v
   222  		r.release.versionInferred = true
   223  	}
   224  
   225  	if r.base.modPath != r.release.modPath {
   226  		setNotValid("Base module path is different from release.")
   227  		return
   228  	}
   229  
   230  	if r.haveReleaseErrors || r.haveBaseErrors {
   231  		setNotValid("Errors were found.")
   232  		return
   233  	}
   234  
   235  	var major, minor, patch, pre string
   236  	if r.base.version != "none" {
   237  		minVersion := r.base.version
   238  		if r.release.highestTransitiveVersion != "" && semver.Compare(r.release.highestTransitiveVersion, minVersion) > 0 {
   239  			setNotValid("Module indirectly depends on a higher version of itself (%s) than the base version (%s).", r.release.highestTransitiveVersion, r.base.version)
   240  			return
   241  		}
   242  
   243  		var err error
   244  		major, minor, patch, pre, _, err = parseVersion(minVersion)
   245  		if err != nil {
   246  			panic(fmt.Sprintf("could not parse base version: %v", err))
   247  		}
   248  	}
   249  
   250  	if r.haveIncompatibleChanges && r.base.version != "none" && pre == "" && major != "0" {
   251  		setNotValid("Incompatible changes were detected.")
   252  		return
   253  		// TODO(jayconrod): briefly explain how to prepare major version releases
   254  		// and link to documentation.
   255  	}
   256  
   257  	// Check whether we're comparing to the latest version of base.
   258  	//
   259  	// This could happen further up, but we want the more pressing errors above
   260  	// to take precedence.
   261  	var latestForBaseMajor string
   262  	for _, v := range r.base.existingVersions {
   263  		if semver.Major(v) != semver.Major(r.base.version) {
   264  			continue
   265  		}
   266  		if latestForBaseMajor == "" || semver.Compare(latestForBaseMajor, v) < 0 {
   267  			latestForBaseMajor = v
   268  		}
   269  	}
   270  	if latestForBaseMajor != "" && latestForBaseMajor != r.base.version {
   271  		setNotValid(fmt.Sprintf("Can only suggest a release version when compared against the most recent version of this major: %s.", latestForBaseMajor))
   272  		return
   273  	}
   274  
   275  	if r.base.version == "none" {
   276  		if _, pathMajor, ok := module.SplitPathVersion(r.release.modPath); !ok {
   277  			panic(fmt.Sprintf("could not parse module path %q", r.release.modPath))
   278  		} else if pathMajor == "" {
   279  			setVersion("v0.1.0")
   280  		} else {
   281  			setVersion(pathMajor[1:] + ".0.0")
   282  		}
   283  		return
   284  	}
   285  
   286  	if pre != "" {
   287  		// suggest non-prerelease version
   288  	} else if r.haveCompatibleChanges || (r.haveIncompatibleChanges && major == "0") || r.requirementsChanged() {
   289  		minor = incDecimal(minor)
   290  		patch = "0"
   291  	} else {
   292  		patch = incDecimal(patch)
   293  	}
   294  	setVersion(fmt.Sprintf("v%s.%s.%s", major, minor, patch))
   295  	return
   296  }
   297  
   298  // canVerifyReleaseVersion returns true if we can safely suggest a new version
   299  // or if we can verify the version passed in with -version is safe to tag.
   300  func (r *report) canVerifyReleaseVersion() bool {
   301  	// For now, return true if the base and release module paths are the same,
   302  	// ignoring the major version suffix.
   303  	// TODO(#37562, #39192, #39666, #40267): there are many more situations when
   304  	// we can't verify a new version.
   305  	basePath := strings.TrimSuffix(r.base.modPath, r.base.modPathMajor)
   306  	releasePath := strings.TrimSuffix(r.release.modPath, r.release.modPathMajor)
   307  	return basePath == releasePath
   308  }
   309  
   310  // requirementsChanged reports whether requirements have changed from base to
   311  // version.
   312  //
   313  // requirementsChanged reports true for,
   314  //   - A requirement was upgraded to a higher minor version.
   315  //   - A requirement was added.
   316  //   - The version of Go was incremented.
   317  //
   318  // It does not report true when, for example, a requirement was downgraded or
   319  // remove. We care more about the former since that might force dependent
   320  // modules that have the same dependency to upgrade.
   321  func (r *report) requirementsChanged() bool {
   322  	if r.base.goModFile == nil {
   323  		// There wasn't a modfile before, and now there is.
   324  		return true
   325  	}
   326  
   327  	// baseReqs is a map of module path to MajorMinor of the base module
   328  	// requirements.
   329  	baseReqs := make(map[string]string)
   330  	for _, r := range r.base.goModFile.Require {
   331  		baseReqs[r.Mod.Path] = r.Mod.Version
   332  	}
   333  
   334  	for _, r := range r.release.goModFile.Require {
   335  		if _, ok := baseReqs[r.Mod.Path]; !ok {
   336  			// A module@version was added to the "require" block between base
   337  			// and release.
   338  			return true
   339  		}
   340  		if semver.Compare(semver.MajorMinor(r.Mod.Version), semver.MajorMinor(baseReqs[r.Mod.Path])) > 0 {
   341  			// The version of r.Mod.Path increased from base to release.
   342  			return true
   343  		}
   344  	}
   345  
   346  	if r.release.goModFile.Go != nil && r.base.goModFile.Go != nil {
   347  		if r.release.goModFile.Go.Version > r.base.goModFile.Go.Version {
   348  			// The Go version increased from base to release.
   349  			return true
   350  		}
   351  	}
   352  
   353  	return false
   354  }
   355  
   356  // isSuccessful returns true the module appears to be safe to release at the
   357  // proposed or suggested version.
   358  func (r *report) isSuccessful() bool {
   359  	return len(r.release.diagnostics) == 0 && r.versionInvalid == nil
   360  }
   361  
   362  type versionMessage struct {
   363  	message, reason string
   364  }
   365  
   366  func (m versionMessage) String() string {
   367  	return m.message + "\n" + m.reason + "\n"
   368  }
   369  
   370  // incDecimal returns the decimal string incremented by 1.
   371  func incDecimal(decimal string) string {
   372  	// Scan right to left turning 9s to 0s until you find a digit to increment.
   373  	digits := []byte(decimal)
   374  	i := len(digits) - 1
   375  	for ; i >= 0 && digits[i] == '9'; i-- {
   376  		digits[i] = '0'
   377  	}
   378  	if i >= 0 {
   379  		digits[i]++
   380  	} else {
   381  		// digits is all zeros
   382  		digits[0] = '1'
   383  		digits = append(digits, '0')
   384  	}
   385  	return string(digits)
   386  }
   387  
   388  type packageReport struct {
   389  	apidiff.Report
   390  	path                      string
   391  	baseErrors, releaseErrors []packages.Error
   392  }
   393  
   394  func (p *packageReport) String() string {
   395  	if len(p.Changes) == 0 && len(p.baseErrors) == 0 && len(p.releaseErrors) == 0 {
   396  		return ""
   397  	}
   398  	buf := &strings.Builder{}
   399  	fmt.Fprintf(buf, "# %s\n", p.path)
   400  	if len(p.baseErrors) > 0 {
   401  		fmt.Fprintf(buf, "## errors in base version:\n")
   402  		for _, e := range p.baseErrors {
   403  			fmt.Fprintln(buf, e)
   404  		}
   405  		buf.WriteByte('\n')
   406  	}
   407  	if len(p.releaseErrors) > 0 {
   408  		fmt.Fprintf(buf, "## errors in release version:\n")
   409  		for _, e := range p.releaseErrors {
   410  			fmt.Fprintln(buf, e)
   411  		}
   412  		buf.WriteByte('\n')
   413  	}
   414  	if len(p.Changes) > 0 {
   415  		var compatible, incompatible []apidiff.Change
   416  		for _, c := range p.Changes {
   417  			if c.Compatible {
   418  				compatible = append(compatible, c)
   419  			} else {
   420  				incompatible = append(incompatible, c)
   421  			}
   422  		}
   423  		if len(incompatible) > 0 {
   424  			fmt.Fprintf(buf, "## incompatible changes\n")
   425  			for _, c := range incompatible {
   426  				fmt.Fprintln(buf, c.Message)
   427  			}
   428  		}
   429  		if len(compatible) > 0 {
   430  			fmt.Fprintf(buf, "## compatible changes\n")
   431  			for _, c := range compatible {
   432  				fmt.Fprintln(buf, c.Message)
   433  			}
   434  		}
   435  		buf.WriteByte('\n')
   436  	}
   437  	return buf.String()
   438  }
   439  
   440  // parseVersion returns the major, minor, and patch numbers, prerelease text,
   441  // and metadata for a given version.
   442  //
   443  // TODO(jayconrod): extend semver to do this and delete this function.
   444  func parseVersion(vers string) (major, minor, patch, pre, meta string, err error) {
   445  	if !strings.HasPrefix(vers, "v") {
   446  		return "", "", "", "", "", fmt.Errorf("version %q does not start with 'v'", vers)
   447  	}
   448  	base := vers[1:]
   449  	if i := strings.IndexByte(base, '+'); i >= 0 {
   450  		meta = base[i+1:]
   451  		base = base[:i]
   452  	}
   453  	if i := strings.IndexByte(base, '-'); i >= 0 {
   454  		pre = base[i+1:]
   455  		base = base[:i]
   456  	}
   457  	parts := strings.Split(base, ".")
   458  	if len(parts) != 3 {
   459  		return "", "", "", "", "", fmt.Errorf("version %q should have three numbers", vers)
   460  	}
   461  	major, minor, patch = parts[0], parts[1], parts[2]
   462  	return major, minor, patch, pre, meta, nil
   463  }