1// Copyright 2020 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
5package mvs
6
7import (
8	"fmt"
9	"strings"
10
11	"golang.org/x/mod/module"
12)
13
14// BuildListError decorates an error that occurred gathering requirements
15// while constructing a build list. BuildListError prints the chain
16// of requirements to the module where the error occurred.
17type BuildListError struct {
18	Err   error
19	stack []buildListErrorElem
20}
21
22type buildListErrorElem struct {
23	m module.Version
24
25	// nextReason is the reason this module depends on the next module in the
26	// stack. Typically either "requires", or "updating to".
27	nextReason string
28}
29
30// NewBuildListError returns a new BuildListError wrapping an error that
31// occurred at a module found along the given path of requirements and/or
32// upgrades, which must be non-empty.
33//
34// The isVersionChange function reports whether a path step is due to an
35// explicit upgrade or downgrade (as opposed to an existing requirement in a
36// go.mod file). A nil isVersionChange function indicates that none of the path
37// steps are due to explicit version changes.
38func NewBuildListError(err error, path []module.Version, isVersionChange func(from, to module.Version) bool) *BuildListError {
39	stack := make([]buildListErrorElem, 0, len(path))
40	for len(path) > 1 {
41		reason := "requires"
42		if isVersionChange != nil && isVersionChange(path[0], path[1]) {
43			reason = "updating to"
44		}
45		stack = append(stack, buildListErrorElem{
46			m:          path[0],
47			nextReason: reason,
48		})
49		path = path[1:]
50	}
51	stack = append(stack, buildListErrorElem{m: path[0]})
52
53	return &BuildListError{
54		Err:   err,
55		stack: stack,
56	}
57}
58
59// Module returns the module where the error occurred. If the module stack
60// is empty, this returns a zero value.
61func (e *BuildListError) Module() module.Version {
62	if len(e.stack) == 0 {
63		return module.Version{}
64	}
65	return e.stack[len(e.stack)-1].m
66}
67
68func (e *BuildListError) Error() string {
69	b := &strings.Builder{}
70	stack := e.stack
71
72	// Don't print modules at the beginning of the chain without a
73	// version. These always seem to be the main module or a
74	// synthetic module ("target@").
75	for len(stack) > 0 && stack[0].m.Version == "" {
76		stack = stack[1:]
77	}
78
79	if len(stack) == 0 {
80		b.WriteString(e.Err.Error())
81	} else {
82		for _, elem := range stack[:len(stack)-1] {
83			fmt.Fprintf(b, "%s %s\n\t", elem.m, elem.nextReason)
84		}
85		// Ensure that the final module path and version are included as part of the
86		// error message.
87		m := stack[len(stack)-1].m
88		if mErr, ok := e.Err.(*module.ModuleError); ok {
89			actual := module.Version{Path: mErr.Path, Version: mErr.Version}
90			if v, ok := mErr.Err.(*module.InvalidVersionError); ok {
91				actual.Version = v.Version
92			}
93			if actual == m {
94				fmt.Fprintf(b, "%v", e.Err)
95			} else {
96				fmt.Fprintf(b, "%s (replaced by %s): %v", m, actual, mErr.Err)
97			}
98		} else {
99			fmt.Fprintf(b, "%v", module.VersionError(m, e.Err))
100		}
101	}
102	return b.String()
103}
104
105func (e *BuildListError) Unwrap() error { return e.Err }
106