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 modget
6
7import (
8	"fmt"
9	"path/filepath"
10	"regexp"
11	"strings"
12	"sync"
13
14	"cmd/go/internal/base"
15	"cmd/go/internal/gover"
16	"cmd/go/internal/modload"
17	"cmd/go/internal/search"
18	"cmd/go/internal/str"
19	"cmd/internal/pkgpattern"
20
21	"golang.org/x/mod/module"
22)
23
24// A query describes a command-line argument and the modules and/or packages
25// to which that argument may resolve..
26type query struct {
27	// raw is the original argument, to be printed in error messages.
28	raw string
29
30	// rawVersion is the portion of raw corresponding to version, if any
31	rawVersion string
32
33	// pattern is the part of the argument before "@" (or the whole argument
34	// if there is no "@"), which may match either packages (preferred) or
35	// modules (if no matching packages).
36	//
37	// The pattern may also be "-u", for the synthetic query representing the -u
38	// (“upgrade”)flag.
39	pattern string
40
41	// patternIsLocal indicates whether pattern is restricted to match only paths
42	// local to the main module, such as absolute filesystem paths or paths
43	// beginning with './'.
44	//
45	// A local pattern must resolve to one or more packages in the main module.
46	patternIsLocal bool
47
48	// version is the part of the argument after "@", or an implied
49	// "upgrade" or "patch" if there is no "@". version specifies the
50	// module version to get.
51	version string
52
53	// matchWildcard, if non-nil, reports whether pattern, which must be a
54	// wildcard (with the substring "..."), matches the given package or module
55	// path.
56	matchWildcard func(path string) bool
57
58	// canMatchWildcardInModule, if non-nil, reports whether the module with the given
59	// path could lexically contain a package matching pattern, which must be a
60	// wildcard.
61	canMatchWildcardInModule func(mPath string) bool
62
63	// conflict is the first query identified as incompatible with this one.
64	// conflict forces one or more of the modules matching this query to a
65	// version that does not match version.
66	conflict *query
67
68	// candidates is a list of sets of alternatives for a path that matches (or
69	// contains packages that match) the pattern. The query can be resolved by
70	// choosing exactly one alternative from each set in the list.
71	//
72	// A path-literal query results in only one set: the path itself, which
73	// may resolve to either a package path or a module path.
74	//
75	// A wildcard query results in one set for each matching module path, each
76	// module for which the matching version contains at least one matching
77	// package, and (if no other modules match) one candidate set for the pattern
78	// overall if no existing match is identified in the build list.
79	//
80	// A query for pattern "all" results in one set for each package transitively
81	// imported by the main module.
82	//
83	// The special query for the "-u" flag results in one set for each
84	// otherwise-unconstrained package that has available upgrades.
85	candidates   []pathSet
86	candidatesMu sync.Mutex
87
88	// pathSeen ensures that only one pathSet is added to the query per
89	// unique path.
90	pathSeen sync.Map
91
92	// resolved contains the set of modules whose versions have been determined by
93	// this query, in the order in which they were determined.
94	//
95	// The resolver examines the candidate sets for each query, resolving one
96	// module per candidate set in a way that attempts to avoid obvious conflicts
97	// between the versions resolved by different queries.
98	resolved []module.Version
99
100	// matchesPackages is true if the resolved modules provide at least one
101	// package matching q.pattern.
102	matchesPackages bool
103}
104
105// A pathSet describes the possible options for resolving a specific path
106// to a package and/or module.
107type pathSet struct {
108	// path is a package (if "all" or "-u" or a non-wildcard) or module (if
109	// wildcard) path that could be resolved by adding any of the modules in this
110	// set. For a wildcard pattern that so far matches no packages, the path is
111	// the wildcard pattern itself.
112	//
113	// Each path must occur only once in a query's candidate sets, and the path is
114	// added implicitly to each pathSet returned to pathOnce.
115	path string
116
117	// pkgMods is a set of zero or more modules, each of which contains the
118	// package with the indicated path. Due to the requirement that imports be
119	// unambiguous, only one such module can be in the build list, and all others
120	// must be excluded.
121	pkgMods []module.Version
122
123	// mod is either the zero Version, or a module that does not contain any
124	// packages matching the query but for which the module path itself
125	// matches the query pattern.
126	//
127	// We track this module separately from pkgMods because, all else equal, we
128	// prefer to match a query to a package rather than just a module. Also,
129	// unlike the modules in pkgMods, this module does not inherently exclude
130	// any other module in pkgMods.
131	mod module.Version
132
133	err error
134}
135
136// errSet returns a pathSet containing the given error.
137func errSet(err error) pathSet { return pathSet{err: err} }
138
139// newQuery returns a new query parsed from the raw argument,
140// which must be either path or path@version.
141func newQuery(raw string) (*query, error) {
142	pattern, rawVers, found := strings.Cut(raw, "@")
143	if found && (strings.Contains(rawVers, "@") || rawVers == "") {
144		return nil, fmt.Errorf("invalid module version syntax %q", raw)
145	}
146
147	// If no version suffix is specified, assume @upgrade.
148	// If -u=patch was specified, assume @patch instead.
149	version := rawVers
150	if version == "" {
151		if getU.version == "" {
152			version = "upgrade"
153		} else {
154			version = getU.version
155		}
156	}
157
158	q := &query{
159		raw:            raw,
160		rawVersion:     rawVers,
161		pattern:        pattern,
162		patternIsLocal: filepath.IsAbs(pattern) || search.IsRelativePath(pattern),
163		version:        version,
164	}
165	if strings.Contains(q.pattern, "...") {
166		q.matchWildcard = pkgpattern.MatchPattern(q.pattern)
167		q.canMatchWildcardInModule = pkgpattern.TreeCanMatchPattern(q.pattern)
168	}
169	if err := q.validate(); err != nil {
170		return q, err
171	}
172	return q, nil
173}
174
175// validate reports a non-nil error if q is not sensible and well-formed.
176func (q *query) validate() error {
177	if q.patternIsLocal {
178		if q.rawVersion != "" {
179			return fmt.Errorf("can't request explicit version %q of path %q in main module", q.rawVersion, q.pattern)
180		}
181		return nil
182	}
183
184	if q.pattern == "all" {
185		// If there is no main module, "all" is not meaningful.
186		if !modload.HasModRoot() {
187			return fmt.Errorf(`cannot match "all": %v`, modload.ErrNoModRoot)
188		}
189		if !versionOkForMainModule(q.version) {
190			// TODO(bcmills): "all@none" seems like a totally reasonable way to
191			// request that we remove all module requirements, leaving only the main
192			// module and standard library. Perhaps we should implement that someday.
193			return &modload.QueryUpgradesAllError{
194				MainModules: modload.MainModules.Versions(),
195				Query:       q.version,
196			}
197		}
198	}
199
200	if search.IsMetaPackage(q.pattern) && q.pattern != "all" {
201		if q.pattern != q.raw {
202			return fmt.Errorf("can't request explicit version of standard-library pattern %q", q.pattern)
203		}
204	}
205
206	return nil
207}
208
209// String returns the original argument from which q was parsed.
210func (q *query) String() string { return q.raw }
211
212// ResolvedString returns a string describing m as a resolved match for q.
213func (q *query) ResolvedString(m module.Version) string {
214	if m.Path != q.pattern {
215		if m.Version != q.version {
216			return fmt.Sprintf("%v (matching %s@%s)", m, q.pattern, q.version)
217		}
218		return fmt.Sprintf("%v (matching %v)", m, q)
219	}
220	if m.Version != q.version {
221		return fmt.Sprintf("%s@%s (%s)", q.pattern, q.version, m.Version)
222	}
223	return q.String()
224}
225
226// isWildcard reports whether q is a pattern that can match multiple paths.
227func (q *query) isWildcard() bool {
228	return q.matchWildcard != nil || (q.patternIsLocal && strings.Contains(q.pattern, "..."))
229}
230
231// matchesPath reports whether the given path matches q.pattern.
232func (q *query) matchesPath(path string) bool {
233	if q.matchWildcard != nil && !gover.IsToolchain(path) {
234		return q.matchWildcard(path)
235	}
236	return path == q.pattern
237}
238
239// canMatchInModule reports whether the given module path can potentially
240// contain q.pattern.
241func (q *query) canMatchInModule(mPath string) bool {
242	if gover.IsToolchain(mPath) {
243		return false
244	}
245	if q.canMatchWildcardInModule != nil {
246		return q.canMatchWildcardInModule(mPath)
247	}
248	return str.HasPathPrefix(q.pattern, mPath)
249}
250
251// pathOnce invokes f to generate the pathSet for the given path,
252// if one is still needed.
253//
254// Note that, unlike sync.Once, pathOnce does not guarantee that a concurrent
255// call to f for the given path has completed on return.
256//
257// pathOnce is safe for concurrent use by multiple goroutines, but note that
258// multiple concurrent calls will result in the sets being added in
259// nondeterministic order.
260func (q *query) pathOnce(path string, f func() pathSet) {
261	if _, dup := q.pathSeen.LoadOrStore(path, nil); dup {
262		return
263	}
264
265	cs := f()
266
267	if len(cs.pkgMods) > 0 || cs.mod != (module.Version{}) || cs.err != nil {
268		cs.path = path
269		q.candidatesMu.Lock()
270		q.candidates = append(q.candidates, cs)
271		q.candidatesMu.Unlock()
272	}
273}
274
275// reportError logs err concisely using base.Errorf.
276func reportError(q *query, err error) {
277	errStr := err.Error()
278
279	// If err already mentions all of the relevant parts of q, just log err to
280	// reduce stutter. Otherwise, log both q and err.
281	//
282	// TODO(bcmills): Use errors.As to unpack these errors instead of parsing
283	// strings with regular expressions.
284
285	patternRE := regexp.MustCompile("(?m)(?:[ \t(\"`]|^)" + regexp.QuoteMeta(q.pattern) + "(?:[ @:;)\"`]|$)")
286	if patternRE.MatchString(errStr) {
287		if q.rawVersion == "" {
288			base.Errorf("go: %s", errStr)
289			return
290		}
291
292		versionRE := regexp.MustCompile("(?m)(?:[ @(\"`]|^)" + regexp.QuoteMeta(q.version) + "(?:[ :;)\"`]|$)")
293		if versionRE.MatchString(errStr) {
294			base.Errorf("go: %s", errStr)
295			return
296		}
297	}
298
299	if qs := q.String(); qs != "" {
300		base.Errorf("go: %s: %s", qs, errStr)
301	} else {
302		base.Errorf("go: %s", errStr)
303	}
304}
305
306func reportConflict(pq *query, m module.Version, conflict versionReason) {
307	if pq.conflict != nil {
308		// We've already reported a conflict for the proposed query.
309		// Don't report it again, even if it has other conflicts.
310		return
311	}
312	pq.conflict = conflict.reason
313
314	proposed := versionReason{
315		version: m.Version,
316		reason:  pq,
317	}
318	if pq.isWildcard() && !conflict.reason.isWildcard() {
319		// Prefer to report the specific path first and the wildcard second.
320		proposed, conflict = conflict, proposed
321	}
322	reportError(pq, &conflictError{
323		mPath:    m.Path,
324		proposed: proposed,
325		conflict: conflict,
326	})
327}
328
329type conflictError struct {
330	mPath    string
331	proposed versionReason
332	conflict versionReason
333}
334
335func (e *conflictError) Error() string {
336	argStr := func(q *query, v string) string {
337		if v != q.version {
338			return fmt.Sprintf("%s@%s (%s)", q.pattern, q.version, v)
339		}
340		return q.String()
341	}
342
343	pq := e.proposed.reason
344	rq := e.conflict.reason
345	modDetail := ""
346	if e.mPath != pq.pattern {
347		modDetail = fmt.Sprintf("for module %s, ", e.mPath)
348	}
349
350	return fmt.Sprintf("%s%s conflicts with %s",
351		modDetail,
352		argStr(pq, e.proposed.version),
353		argStr(rq, e.conflict.version))
354}
355
356func versionOkForMainModule(version string) bool {
357	return version == "upgrade" || version == "patch"
358}
359