Source file src/cmd/go/internal/modget/query.go

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

View as plain text