blob: 498ba6c2ff862b50710662cf7f10a72a4a83cafe [file] [log] [blame]
// 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 modget
import (
"fmt"
"path/filepath"
"regexp"
"strings"
"sync"
"cmd/go/internal/base"
"cmd/go/internal/gover"
"cmd/go/internal/modload"
"cmd/go/internal/search"
"cmd/go/internal/str"
"cmd/internal/pkgpattern"
"golang.org/x/mod/module"
)
// A query describes a command-line argument and the modules and/or packages
// to which that argument may resolve..
type query struct {
// raw is the original argument, to be printed in error messages.
raw string
// rawVersion is the portion of raw corresponding to version, if any
rawVersion string
// pattern is the part of the argument before "@" (or the whole argument
// if there is no "@"), which may match either packages (preferred) or
// modules (if no matching packages).
//
// The pattern may also be "-u", for the synthetic query representing the -u
// (“upgrade”)flag.
pattern string
// patternIsLocal indicates whether pattern is restricted to match only paths
// local to the main module, such as absolute filesystem paths or paths
// beginning with './'.
//
// A local pattern must resolve to one or more packages in the main module.
patternIsLocal bool
// version is the part of the argument after "@", or an implied
// "upgrade" or "patch" if there is no "@". version specifies the
// module version to get.
version string
// matchWildcard, if non-nil, reports whether pattern, which must be a
// wildcard (with the substring "..."), matches the given package or module
// path.
matchWildcard func(path string) bool
// canMatchWildcardInModule, if non-nil, reports whether the module with the given
// path could lexically contain a package matching pattern, which must be a
// wildcard.
canMatchWildcardInModule func(mPath string) bool
// conflict is the first query identified as incompatible with this one.
// conflict forces one or more of the modules matching this query to a
// version that does not match version.
conflict *query
// candidates is a list of sets of alternatives for a path that matches (or
// contains packages that match) the pattern. The query can be resolved by
// choosing exactly one alternative from each set in the list.
//
// A path-literal query results in only one set: the path itself, which
// may resolve to either a package path or a module path.
//
// A wildcard query results in one set for each matching module path, each
// module for which the matching version contains at least one matching
// package, and (if no other modules match) one candidate set for the pattern
// overall if no existing match is identified in the build list.
//
// A query for pattern "all" results in one set for each package transitively
// imported by the main module.
//
// The special query for the "-u" flag results in one set for each
// otherwise-unconstrained package that has available upgrades.
candidates []pathSet
candidatesMu sync.Mutex
// pathSeen ensures that only one pathSet is added to the query per
// unique path.
pathSeen sync.Map
// resolved contains the set of modules whose versions have been determined by
// this query, in the order in which they were determined.
//
// The resolver examines the candidate sets for each query, resolving one
// module per candidate set in a way that attempts to avoid obvious conflicts
// between the versions resolved by different queries.
resolved []module.Version
// matchesPackages is true if the resolved modules provide at least one
// package matching q.pattern.
matchesPackages bool
}
// A pathSet describes the possible options for resolving a specific path
// to a package and/or module.
type pathSet struct {
// path is a package (if "all" or "-u" or a non-wildcard) or module (if
// wildcard) path that could be resolved by adding any of the modules in this
// set. For a wildcard pattern that so far matches no packages, the path is
// the wildcard pattern itself.
//
// Each path must occur only once in a query's candidate sets, and the path is
// added implicitly to each pathSet returned to pathOnce.
path string
// pkgMods is a set of zero or more modules, each of which contains the
// package with the indicated path. Due to the requirement that imports be
// unambiguous, only one such module can be in the build list, and all others
// must be excluded.
pkgMods []module.Version
// mod is either the zero Version, or a module that does not contain any
// packages matching the query but for which the module path itself
// matches the query pattern.
//
// We track this module separately from pkgMods because, all else equal, we
// prefer to match a query to a package rather than just a module. Also,
// unlike the modules in pkgMods, this module does not inherently exclude
// any other module in pkgMods.
mod module.Version
err error
}
// errSet returns a pathSet containing the given error.
func errSet(err error) pathSet { return pathSet{err: err} }
// newQuery returns a new query parsed from the raw argument,
// which must be either path or path@version.
func newQuery(raw string) (*query, error) {
pattern, rawVers, found := strings.Cut(raw, "@")
if found && (strings.Contains(rawVers, "@") || rawVers == "") {
return nil, fmt.Errorf("invalid module version syntax %q", raw)
}
// If no version suffix is specified, assume @upgrade.
// If -u=patch was specified, assume @patch instead.
version := rawVers
if version == "" {
if getU.version == "" {
version = "upgrade"
} else {
version = getU.version
}
}
q := &query{
raw: raw,
rawVersion: rawVers,
pattern: pattern,
patternIsLocal: filepath.IsAbs(pattern) || search.IsRelativePath(pattern),
version: version,
}
if strings.Contains(q.pattern, "...") {
q.matchWildcard = pkgpattern.MatchPattern(q.pattern)
q.canMatchWildcardInModule = pkgpattern.TreeCanMatchPattern(q.pattern)
}
if err := q.validate(); err != nil {
return q, err
}
return q, nil
}
// validate reports a non-nil error if q is not sensible and well-formed.
func (q *query) validate() error {
if q.patternIsLocal {
if q.rawVersion != "" {
return fmt.Errorf("can't request explicit version %q of path %q in main module", q.rawVersion, q.pattern)
}
return nil
}
if q.pattern == "all" {
// If there is no main module, "all" is not meaningful.
if !modload.HasModRoot() {
return fmt.Errorf(`cannot match "all": %v`, modload.ErrNoModRoot)
}
if !versionOkForMainModule(q.version) {
// TODO(bcmills): "all@none" seems like a totally reasonable way to
// request that we remove all module requirements, leaving only the main
// module and standard library. Perhaps we should implement that someday.
return &modload.QueryUpgradesAllError{
MainModules: modload.MainModules.Versions(),
Query: q.version,
}
}
}
if search.IsMetaPackage(q.pattern) && q.pattern != "all" {
if q.pattern != q.raw {
return fmt.Errorf("can't request explicit version of standard-library pattern %q", q.pattern)
}
}
return nil
}
// String returns the original argument from which q was parsed.
func (q *query) String() string { return q.raw }
// ResolvedString returns a string describing m as a resolved match for q.
func (q *query) ResolvedString(m module.Version) string {
if m.Path != q.pattern {
if m.Version != q.version {
return fmt.Sprintf("%v (matching %s@%s)", m, q.pattern, q.version)
}
return fmt.Sprintf("%v (matching %v)", m, q)
}
if m.Version != q.version {
return fmt.Sprintf("%s@%s (%s)", q.pattern, q.version, m.Version)
}
return q.String()
}
// isWildcard reports whether q is a pattern that can match multiple paths.
func (q *query) isWildcard() bool {
return q.matchWildcard != nil || (q.patternIsLocal && strings.Contains(q.pattern, "..."))
}
// matchesPath reports whether the given path matches q.pattern.
func (q *query) matchesPath(path string) bool {
if q.matchWildcard != nil && !gover.IsToolchain(path) {
return q.matchWildcard(path)
}
return path == q.pattern
}
// canMatchInModule reports whether the given module path can potentially
// contain q.pattern.
func (q *query) canMatchInModule(mPath string) bool {
if gover.IsToolchain(mPath) {
return false
}
if q.canMatchWildcardInModule != nil {
return q.canMatchWildcardInModule(mPath)
}
return str.HasPathPrefix(q.pattern, mPath)
}
// pathOnce invokes f to generate the pathSet for the given path,
// if one is still needed.
//
// Note that, unlike sync.Once, pathOnce does not guarantee that a concurrent
// call to f for the given path has completed on return.
//
// pathOnce is safe for concurrent use by multiple goroutines, but note that
// multiple concurrent calls will result in the sets being added in
// nondeterministic order.
func (q *query) pathOnce(path string, f func() pathSet) {
if _, dup := q.pathSeen.LoadOrStore(path, nil); dup {
return
}
cs := f()
if len(cs.pkgMods) > 0 || cs.mod != (module.Version{}) || cs.err != nil {
cs.path = path
q.candidatesMu.Lock()
q.candidates = append(q.candidates, cs)
q.candidatesMu.Unlock()
}
}
// reportError logs err concisely using base.Errorf.
func reportError(q *query, err error) {
errStr := err.Error()
// If err already mentions all of the relevant parts of q, just log err to
// reduce stutter. Otherwise, log both q and err.
//
// TODO(bcmills): Use errors.As to unpack these errors instead of parsing
// strings with regular expressions.
patternRE := regexp.MustCompile("(?m)(?:[ \t(\"`]|^)" + regexp.QuoteMeta(q.pattern) + "(?:[ @:;)\"`]|$)")
if patternRE.MatchString(errStr) {
if q.rawVersion == "" {
base.Errorf("go: %s", errStr)
return
}
versionRE := regexp.MustCompile("(?m)(?:[ @(\"`]|^)" + regexp.QuoteMeta(q.version) + "(?:[ :;)\"`]|$)")
if versionRE.MatchString(errStr) {
base.Errorf("go: %s", errStr)
return
}
}
if qs := q.String(); qs != "" {
base.Errorf("go: %s: %s", qs, errStr)
} else {
base.Errorf("go: %s", errStr)
}
}
func reportConflict(pq *query, m module.Version, conflict versionReason) {
if pq.conflict != nil {
// We've already reported a conflict for the proposed query.
// Don't report it again, even if it has other conflicts.
return
}
pq.conflict = conflict.reason
proposed := versionReason{
version: m.Version,
reason: pq,
}
if pq.isWildcard() && !conflict.reason.isWildcard() {
// Prefer to report the specific path first and the wildcard second.
proposed, conflict = conflict, proposed
}
reportError(pq, &conflictError{
mPath: m.Path,
proposed: proposed,
conflict: conflict,
})
}
type conflictError struct {
mPath string
proposed versionReason
conflict versionReason
}
func (e *conflictError) Error() string {
argStr := func(q *query, v string) string {
if v != q.version {
return fmt.Sprintf("%s@%s (%s)", q.pattern, q.version, v)
}
return q.String()
}
pq := e.proposed.reason
rq := e.conflict.reason
modDetail := ""
if e.mPath != pq.pattern {
modDetail = fmt.Sprintf("for module %s, ", e.mPath)
}
return fmt.Sprintf("%s%s conflicts with %s",
modDetail,
argStr(pq, e.proposed.version),
argStr(rq, e.conflict.version))
}
func versionOkForMainModule(version string) bool {
return version == "upgrade" || version == "patch"
}