github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/modget/query.go (about) 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 14 "github.com/go-asm/go/cmd/go/base" 15 "github.com/go-asm/go/cmd/go/gover" 16 "github.com/go-asm/go/cmd/go/modload" 17 "github.com/go-asm/go/cmd/go/search" 18 "github.com/go-asm/go/cmd/go/str" 19 "github.com/go-asm/go/cmd/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.. 26 type 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. 107 type 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. 137 func 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. 141 func 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. 176 func (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. 210 func (q *query) String() string { return q.raw } 211 212 // ResolvedString returns a string describing m as a resolved match for q. 213 func (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. 227 func (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. 232 func (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. 241 func (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. 260 func (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. 276 func 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 306 func 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 329 type conflictError struct { 330 mPath string 331 proposed versionReason 332 conflict versionReason 333 } 334 335 func (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 356 func versionOkForMainModule(version string) bool { 357 return version == "upgrade" || version == "patch" 358 }