cmd/go/internal/mvs: export a NewBuildListError function

Also factor out BuildListError to a separate file.

For #36460

Change-Id: Ibd1143893b09a2bbef659bea1e8c5dd35184a7ef
Reviewed-on: https://go-review.googlesource.com/c/go/+/247764
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/src/cmd/go/internal/mvs/errors.go b/src/cmd/go/internal/mvs/errors.go
new file mode 100644
index 0000000..8577902
--- /dev/null
+++ b/src/cmd/go/internal/mvs/errors.go
@@ -0,0 +1,96 @@
+// Copyright 2020 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 mvs
+
+import (
+	"fmt"
+	"strings"
+
+	"golang.org/x/mod/module"
+)
+
+// BuildListError decorates an error that occurred gathering requirements
+// while constructing a build list. BuildListError prints the chain
+// of requirements to the module where the error occurred.
+type BuildListError struct {
+	Err   error
+	stack []buildListErrorElem
+}
+
+type buildListErrorElem struct {
+	m module.Version
+
+	// nextReason is the reason this module depends on the next module in the
+	// stack. Typically either "requires", or "updating to".
+	nextReason string
+}
+
+// NewBuildListError returns a new BuildListError wrapping an error that
+// occurred at a module found along the given path of requirements and/or
+// upgrades, which must be non-empty.
+//
+// The isUpgrade function reports whether a path step is due to an upgrade.
+// A nil isUpgrade function indicates that none of the path steps are due to upgrades.
+func NewBuildListError(err error, path []module.Version, isUpgrade func(from, to module.Version) bool) *BuildListError {
+	stack := make([]buildListErrorElem, 0, len(path))
+	for len(path) > 1 {
+		reason := "requires"
+		if isUpgrade != nil && isUpgrade(path[0], path[1]) {
+			reason = "updating to"
+		}
+		stack = append(stack, buildListErrorElem{
+			m:          path[0],
+			nextReason: reason,
+		})
+		path = path[1:]
+	}
+	stack = append(stack, buildListErrorElem{m: path[0]})
+
+	return &BuildListError{
+		Err:   err,
+		stack: stack,
+	}
+}
+
+// Module returns the module where the error occurred. If the module stack
+// is empty, this returns a zero value.
+func (e *BuildListError) Module() module.Version {
+	if len(e.stack) == 0 {
+		return module.Version{}
+	}
+	return e.stack[len(e.stack)-1].m
+}
+
+func (e *BuildListError) Error() string {
+	b := &strings.Builder{}
+	stack := e.stack
+
+	// Don't print modules at the beginning of the chain without a
+	// version. These always seem to be the main module or a
+	// synthetic module ("target@").
+	for len(stack) > 0 && stack[0].m.Version == "" {
+		stack = stack[1:]
+	}
+
+	if len(stack) == 0 {
+		b.WriteString(e.Err.Error())
+	} else {
+		for _, elem := range stack[:len(stack)-1] {
+			fmt.Fprintf(b, "%s@%s %s\n\t", elem.m.Path, elem.m.Version, elem.nextReason)
+		}
+		// Ensure that the final module path and version are included as part of the
+		// error message.
+		m := stack[len(stack)-1].m
+		if _, ok := e.Err.(*module.ModuleError); ok {
+			// TODO(bcmills): Also ensure that the module path and version match.
+			// (Otherwise, we may be reporting an error from a replacement without
+			// indicating the replacement path.)
+			fmt.Fprintf(b, "%v", e.Err)
+		} else {
+			fmt.Fprintf(b, "%v", module.VersionError(m, e.Err))
+		}
+	}
+	return b.String()
+}
diff --git a/src/cmd/go/internal/mvs/mvs.go b/src/cmd/go/internal/mvs/mvs.go
index 1056a50..ea23a9f 100644
--- a/src/cmd/go/internal/mvs/mvs.go
+++ b/src/cmd/go/internal/mvs/mvs.go
@@ -9,7 +9,6 @@
 import (
 	"fmt"
 	"sort"
-	"strings"
 	"sync"
 	"sync/atomic"
 
@@ -61,59 +60,6 @@
 	Previous(m module.Version) (module.Version, error)
 }
 
-// BuildListError decorates an error that occurred gathering requirements
-// while constructing a build list. BuildListError prints the chain
-// of requirements to the module where the error occurred.
-type BuildListError struct {
-	Err   error
-	stack []buildListErrorElem
-}
-
-type buildListErrorElem struct {
-	m module.Version
-
-	// nextReason is the reason this module depends on the next module in the
-	// stack. Typically either "requires", or "upgraded to".
-	nextReason string
-}
-
-// Module returns the module where the error occurred. If the module stack
-// is empty, this returns a zero value.
-func (e *BuildListError) Module() module.Version {
-	if len(e.stack) == 0 {
-		return module.Version{}
-	}
-	return e.stack[len(e.stack)-1].m
-}
-
-func (e *BuildListError) Error() string {
-	b := &strings.Builder{}
-	stack := e.stack
-
-	// Don't print modules at the beginning of the chain without a
-	// version. These always seem to be the main module or a
-	// synthetic module ("target@").
-	for len(stack) > 0 && stack[0].m.Version == "" {
-		stack = stack[1:]
-	}
-
-	if len(stack) == 0 {
-		b.WriteString(e.Err.Error())
-	} else {
-		for _, elem := range stack[:len(stack)-1] {
-			fmt.Fprintf(b, "%s@%s %s\n\t", elem.m.Path, elem.m.Version, elem.nextReason)
-		}
-		// Ensure that the final module path and version are included as part of the
-		// error message.
-		if _, ok := e.Err.(*module.ModuleError); ok {
-			fmt.Fprintf(b, "%v", e.Err)
-		} else {
-			fmt.Fprintf(b, "%v", module.VersionError(stack[len(stack)-1].m, e.Err))
-		}
-	}
-	return b.String()
-}
-
 // BuildList returns the build list for the target module.
 //
 // target is the root vertex of a module requirement graph. For cmd/go, this is
@@ -202,29 +148,30 @@
 			q = q[1:]
 
 			if node.err != nil {
-				// Construct the stack reversed (from the error to the main module),
+				pathUpgrade := map[module.Version]module.Version{}
+
+				// Construct the error path reversed (from the error to the main module),
 				// then reverse it to obtain the usual order (from the main module to
 				// the error).
-				stack := []buildListErrorElem{{m: node.m}}
+				errPath := []module.Version{node.m}
 				for n, prev := neededBy[node], node; n != nil; n, prev = neededBy[n], n {
-					reason := "requires"
 					if n.upgrade == prev.m {
-						reason = "updating to"
+						pathUpgrade[n.m] = prev.m
 					}
-					stack = append(stack, buildListErrorElem{m: n.m, nextReason: reason})
+					errPath = append(errPath, n.m)
 				}
-				i, j := 0, len(stack)-1
+				i, j := 0, len(errPath)-1
 				for i < j {
-					stack[i], stack[j] = stack[j], stack[i]
+					errPath[i], errPath[j] = errPath[j], errPath[i]
 					i++
 					j--
 				}
 
-				err := &BuildListError{
-					Err:   node.err,
-					stack: stack,
+				isUpgrade := func(from, to module.Version) bool {
+					return pathUpgrade[from] == to
 				}
-				return nil, err
+
+				return nil, NewBuildListError(node.err, errPath, isUpgrade)
 			}
 
 			neighbors := node.required