github.com/octohelm/cuemod@v0.9.4/internal/cmd/go/internals/search/search.go (about) 1 // Copyright 2017 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 search 6 7 import ( 8 "fmt" 9 "github.com/octohelm/cuemod/internal/cmd/go/internals/base" 10 "github.com/octohelm/cuemod/internal/cmd/go/internals/cfg" 11 "github.com/octohelm/cuemod/internal/cmd/go/internals/fsys" 12 "github.com/octohelm/cuemod/internal/cmd/go/internals/str" 13 "github.com/octohelm/cuemod/internal/cmd/internals/pkgpattern" 14 "go/build" 15 "io/fs" 16 "os" 17 "path" 18 "path/filepath" 19 "strings" 20 ) 21 22 // A Match represents the result of matching a single package pattern. 23 type Match struct { 24 pattern string // the pattern itself 25 Dirs []string // if the pattern is local, directories that potentially contain matching packages 26 Pkgs []string // matching packages (import paths) 27 Errs []error // errors matching the patterns to packages, NOT errors loading those packages 28 29 // Errs may be non-empty even if len(Pkgs) > 0, indicating that some matching 30 // packages could be located but results may be incomplete. 31 // If len(Pkgs) == 0 && len(Errs) == 0, the pattern is well-formed but did not 32 // match any packages. 33 } 34 35 // NewMatch returns a Match describing the given pattern, 36 // without resolving its packages or errors. 37 func NewMatch(pattern string) *Match { 38 return &Match{pattern: pattern} 39 } 40 41 // Pattern returns the pattern to be matched. 42 func (m *Match) Pattern() string { return m.pattern } 43 44 // AddError appends a MatchError wrapping err to m.Errs. 45 func (m *Match) AddError(err error) { 46 m.Errs = append(m.Errs, &MatchError{Match: m, Err: err}) 47 } 48 49 // IsLiteral reports whether the pattern is free of wildcards and meta-patterns. 50 // 51 // A literal pattern must match at most one package. 52 func (m *Match) IsLiteral() bool { 53 return !strings.Contains(m.pattern, "...") && !m.IsMeta() 54 } 55 56 // IsLocal reports whether the pattern must be resolved from a specific root or 57 // directory, such as a filesystem path or a single module. 58 func (m *Match) IsLocal() bool { 59 return build.IsLocalImport(m.pattern) || filepath.IsAbs(m.pattern) 60 } 61 62 // IsMeta reports whether the pattern is a “meta-package” keyword that represents 63 // multiple packages, such as "std", "cmd", or "all". 64 func (m *Match) IsMeta() bool { 65 return IsMetaPackage(m.pattern) 66 } 67 68 // IsMetaPackage checks if name is a reserved package name that expands to multiple packages. 69 func IsMetaPackage(name string) bool { 70 return name == "std" || name == "cmd" || name == "all" 71 } 72 73 // A MatchError indicates an error that occurred while attempting to match a 74 // pattern. 75 type MatchError struct { 76 Match *Match 77 Err error 78 } 79 80 func (e *MatchError) Error() string { 81 if e.Match.IsLiteral() { 82 return fmt.Sprintf("%s: %v", e.Match.Pattern(), e.Err) 83 } 84 return fmt.Sprintf("pattern %s: %v", e.Match.Pattern(), e.Err) 85 } 86 87 func (e *MatchError) Unwrap() error { 88 return e.Err 89 } 90 91 // MatchPackages sets m.Pkgs to a non-nil slice containing all the packages that 92 // can be found under the $GOPATH directories and $GOROOT that match the 93 // pattern. The pattern must be either "all" (all packages), "std" (standard 94 // packages), "cmd" (standard commands), or a path including "...". 95 // 96 // If any errors may have caused the set of packages to be incomplete, 97 // MatchPackages appends those errors to m.Errs. 98 func (m *Match) MatchPackages() { 99 m.Pkgs = []string{} 100 if m.IsLocal() { 101 m.AddError(fmt.Errorf("internal error: MatchPackages: %s is not a valid package pattern", m.pattern)) 102 return 103 } 104 105 if m.IsLiteral() { 106 m.Pkgs = []string{m.pattern} 107 return 108 } 109 110 match := func(string) bool { return true } 111 treeCanMatch := func(string) bool { return true } 112 if !m.IsMeta() { 113 match = pkgpattern.MatchPattern(m.pattern) 114 treeCanMatch = pkgpattern.TreeCanMatchPattern(m.pattern) 115 } 116 117 have := map[string]bool{ 118 "builtin": true, // ignore pseudo-package that exists only for documentation 119 } 120 if !cfg.BuildContext.CgoEnabled { 121 have["runtime/cgo"] = true // ignore during walk 122 } 123 124 for _, src := range cfg.BuildContext.SrcDirs() { 125 if (m.pattern == "std" || m.pattern == "cmd") && src != cfg.GOROOTsrc { 126 continue 127 } 128 129 // If the root itself is a symlink to a directory, 130 // we want to follow it (see https://go.dev/issue/50807). 131 // Add a trailing separator to force that to happen. 132 src = str.WithFilePathSeparator(filepath.Clean(src)) 133 root := src 134 if m.pattern == "cmd" { 135 root += "cmd" + string(filepath.Separator) 136 } 137 138 err := fsys.Walk(root, func(path string, fi fs.FileInfo, err error) error { 139 if err != nil { 140 return err // Likely a permission error, which could interfere with matching. 141 } 142 if path == src { 143 return nil // GOROOT/src and GOPATH/src cannot contain packages. 144 } 145 146 want := true 147 // Avoid .foo, _foo, and testdata directory trees. 148 _, elem := filepath.Split(path) 149 if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { 150 want = false 151 } 152 153 name := filepath.ToSlash(path[len(src):]) 154 if m.pattern == "std" && (!IsStandardImportPath(name) || name == "cmd") { 155 // The name "std" is only the standard library. 156 // If the name is cmd, it's the root of the command tree. 157 want = false 158 } 159 if !treeCanMatch(name) { 160 want = false 161 } 162 163 if !fi.IsDir() { 164 if fi.Mode()&fs.ModeSymlink != 0 && want && strings.Contains(m.pattern, "...") { 165 if target, err := fsys.Stat(path); err == nil && target.IsDir() { 166 fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path) 167 } 168 } 169 return nil 170 } 171 if !want { 172 return filepath.SkipDir 173 } 174 175 if have[name] { 176 return nil 177 } 178 have[name] = true 179 if !match(name) { 180 return nil 181 } 182 pkg, err := cfg.BuildContext.ImportDir(path, 0) 183 if err != nil { 184 if _, noGo := err.(*build.NoGoError); noGo { 185 // The package does not actually exist, so record neither the package 186 // nor the error. 187 return nil 188 } 189 // There was an error importing path, but not matching it, 190 // which is all that Match promises to do. 191 // Ignore the import error. 192 } 193 194 // If we are expanding "cmd", skip main 195 // packages under cmd/vendor. At least as of 196 // March, 2017, there is one there for the 197 // vendored pprof tool. 198 if m.pattern == "cmd" && pkg != nil && strings.HasPrefix(pkg.ImportPath, "cmd/vendor") && pkg.Name == "main" { 199 return nil 200 } 201 202 m.Pkgs = append(m.Pkgs, name) 203 return nil 204 }) 205 if err != nil { 206 m.AddError(err) 207 } 208 } 209 } 210 211 // MatchDirs sets m.Dirs to a non-nil slice containing all directories that 212 // potentially match a local pattern. The pattern must begin with an absolute 213 // path, or "./", or "../". On Windows, the pattern may use slash or backslash 214 // separators or a mix of both. 215 // 216 // If any errors may have caused the set of directories to be incomplete, 217 // MatchDirs appends those errors to m.Errs. 218 func (m *Match) MatchDirs(modRoots []string) { 219 m.Dirs = []string{} 220 if !m.IsLocal() { 221 m.AddError(fmt.Errorf("internal error: MatchDirs: %s is not a valid filesystem pattern", m.pattern)) 222 return 223 } 224 225 if m.IsLiteral() { 226 m.Dirs = []string{m.pattern} 227 return 228 } 229 230 // Clean the path and create a matching predicate. 231 // filepath.Clean removes "./" prefixes (and ".\" on Windows). We need to 232 // preserve these, since they are meaningful in MatchPattern and in 233 // returned import paths. 234 cleanPattern := filepath.Clean(m.pattern) 235 isLocal := strings.HasPrefix(m.pattern, "./") || (os.PathSeparator == '\\' && strings.HasPrefix(m.pattern, `.\`)) 236 prefix := "" 237 if cleanPattern != "." && isLocal { 238 prefix = "./" 239 cleanPattern = "." + string(os.PathSeparator) + cleanPattern 240 } 241 slashPattern := filepath.ToSlash(cleanPattern) 242 match := pkgpattern.MatchPattern(slashPattern) 243 244 // Find directory to begin the scan. 245 // Could be smarter but this one optimization 246 // is enough for now, since ... is usually at the 247 // end of a path. 248 i := strings.Index(cleanPattern, "...") 249 dir, _ := filepath.Split(cleanPattern[:i]) 250 251 // pattern begins with ./ or ../. 252 // path.Clean will discard the ./ but not the ../. 253 // We need to preserve the ./ for pattern matching 254 // and in the returned import paths. 255 256 if len(modRoots) > 1 { 257 abs, err := filepath.Abs(dir) 258 if err != nil { 259 m.AddError(err) 260 return 261 } 262 var found bool 263 for _, modRoot := range modRoots { 264 if modRoot != "" && str.HasFilePathPrefix(abs, modRoot) { 265 found = true 266 } 267 } 268 if !found { 269 plural := "" 270 if len(modRoots) > 1 { 271 plural = "s" 272 } 273 m.AddError(fmt.Errorf("directory %s is outside module root%s (%s)", abs, plural, strings.Join(modRoots, ", "))) 274 } 275 } 276 277 // If dir is actually a symlink to a directory, 278 // we want to follow it (see https://go.dev/issue/50807). 279 // Add a trailing separator to force that to happen. 280 dir = str.WithFilePathSeparator(dir) 281 err := fsys.Walk(dir, func(path string, fi fs.FileInfo, err error) error { 282 if err != nil { 283 return err // Likely a permission error, which could interfere with matching. 284 } 285 if !fi.IsDir() { 286 return nil 287 } 288 top := false 289 if path == dir { 290 // Walk starts at dir and recurses. For the recursive case, 291 // the path is the result of filepath.Join, which calls filepath.Clean. 292 // The initial case is not Cleaned, though, so we do this explicitly. 293 // 294 // This converts a path like "./io/" to "io". Without this step, running 295 // "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io 296 // package, because prepending the prefix "./" to the unclean path would 297 // result in "././io", and match("././io") returns false. 298 top = true 299 path = filepath.Clean(path) 300 } 301 302 // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". 303 _, elem := filepath.Split(path) 304 dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." 305 if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { 306 return filepath.SkipDir 307 } 308 309 if !top && cfg.ModulesEnabled { 310 // Ignore other modules found in subdirectories. 311 if fi, err := fsys.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() { 312 return filepath.SkipDir 313 } 314 } 315 316 name := prefix + filepath.ToSlash(path) 317 if !match(name) { 318 return nil 319 } 320 321 // We keep the directory if we can import it, or if we can't import it 322 // due to invalid Go source files. This means that directories containing 323 // parse errors will be built (and fail) instead of being silently skipped 324 // as not matching the pattern. Go 1.5 and earlier skipped, but that 325 // behavior means people miss serious mistakes. 326 // See golang.org/issue/11407. 327 if p, err := cfg.BuildContext.ImportDir(path, 0); err != nil && (p == nil || len(p.InvalidGoFiles) == 0) { 328 if _, noGo := err.(*build.NoGoError); noGo { 329 // The package does not actually exist, so record neither the package 330 // nor the error. 331 return nil 332 } 333 // There was an error importing path, but not matching it, 334 // which is all that Match promises to do. 335 // Ignore the import error. 336 } 337 m.Dirs = append(m.Dirs, name) 338 return nil 339 }) 340 if err != nil { 341 m.AddError(err) 342 } 343 } 344 345 // WarnUnmatched warns about patterns that didn't match any packages. 346 func WarnUnmatched(matches []*Match) { 347 for _, m := range matches { 348 if len(m.Pkgs) == 0 && len(m.Errs) == 0 { 349 fmt.Fprintf(os.Stderr, "go: warning: %q matched no packages\n", m.pattern) 350 } 351 } 352 } 353 354 // ImportPaths returns the matching paths to use for the given command line. 355 // It calls ImportPathsQuiet and then WarnUnmatched. 356 func ImportPaths(patterns, modRoots []string) []*Match { 357 matches := ImportPathsQuiet(patterns, modRoots) 358 WarnUnmatched(matches) 359 return matches 360 } 361 362 // ImportPathsQuiet is like ImportPaths but does not warn about patterns with no matches. 363 func ImportPathsQuiet(patterns, modRoots []string) []*Match { 364 var out []*Match 365 for _, a := range CleanPatterns(patterns) { 366 m := NewMatch(a) 367 if m.IsLocal() { 368 m.MatchDirs(modRoots) 369 370 // Change the file import path to a regular import path if the package 371 // is in GOPATH or GOROOT. We don't report errors here; LoadImport 372 // (or something similar) will report them later. 373 m.Pkgs = make([]string, len(m.Dirs)) 374 for i, dir := range m.Dirs { 375 absDir := dir 376 if !filepath.IsAbs(dir) { 377 absDir = filepath.Join(base.Cwd(), dir) 378 } 379 if bp, _ := cfg.BuildContext.ImportDir(absDir, build.FindOnly); bp.ImportPath != "" && bp.ImportPath != "." { 380 m.Pkgs[i] = bp.ImportPath 381 } else { 382 m.Pkgs[i] = dir 383 } 384 } 385 } else { 386 m.MatchPackages() 387 } 388 389 out = append(out, m) 390 } 391 return out 392 } 393 394 // CleanPatterns returns the patterns to use for the given command line. It 395 // canonicalizes the patterns but does not evaluate any matches. For patterns 396 // that are not local or absolute paths, it preserves text after '@' to avoid 397 // modifying version queries. 398 func CleanPatterns(patterns []string) []string { 399 if len(patterns) == 0 { 400 return []string{"."} 401 } 402 var out []string 403 for _, a := range patterns { 404 var p, v string 405 if build.IsLocalImport(a) || filepath.IsAbs(a) { 406 p = a 407 } else if i := strings.IndexByte(a, '@'); i < 0 { 408 p = a 409 } else { 410 p = a[:i] 411 v = a[i:] 412 } 413 414 // Arguments may be either file paths or import paths. 415 // As a courtesy to Windows developers, rewrite \ to / 416 // in arguments that look like import paths. 417 // Don't replace slashes in absolute paths. 418 if filepath.IsAbs(p) { 419 p = filepath.Clean(p) 420 } else { 421 if filepath.Separator == '\\' { 422 p = strings.ReplaceAll(p, `\`, `/`) 423 } 424 425 // Put argument in canonical form, but preserve leading ./. 426 if strings.HasPrefix(p, "./") { 427 p = "./" + path.Clean(p) 428 if p == "./." { 429 p = "." 430 } 431 } else { 432 p = path.Clean(p) 433 } 434 } 435 436 out = append(out, p+v) 437 } 438 return out 439 } 440 441 // IsStandardImportPath reports whether $GOROOT/src/path should be considered 442 // part of the standard distribution. For historical reasons we allow people to add 443 // their own code to $GOROOT instead of using $GOPATH, but we assume that 444 // code will start with a domain name (dot in the first element). 445 // 446 // Note that this function is meant to evaluate whether a directory found in GOROOT 447 // should be treated as part of the standard library. It should not be used to decide 448 // that a directory found in GOPATH should be rejected: directories in GOPATH 449 // need not have dots in the first element, and they just take their chances 450 // with future collisions in the standard library. 451 func IsStandardImportPath(path string) bool { 452 i := strings.Index(path, "/") 453 if i < 0 { 454 i = len(path) 455 } 456 elem := path[:i] 457 return !strings.Contains(elem, ".") 458 } 459 460 // IsRelativePath reports whether pattern should be interpreted as a directory 461 // path relative to the current directory, as opposed to a pattern matching 462 // import paths. 463 func IsRelativePath(pattern string) bool { 464 return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".." 465 } 466 467 // InDir checks whether path is in the file tree rooted at dir. 468 // If so, InDir returns an equivalent path relative to dir. 469 // If not, InDir returns an empty string. 470 // InDir makes some effort to succeed even in the presence of symbolic links. 471 func InDir(path, dir string) string { 472 // inDirLex reports whether path is lexically in dir, 473 // without considering symbolic or hard links. 474 inDirLex := func(path, dir string) (string, bool) { 475 if dir == "" { 476 return path, true 477 } 478 rel := str.TrimFilePathPrefix(path, dir) 479 if rel == path { 480 return "", false 481 } 482 if rel == "" { 483 return ".", true 484 } 485 return rel, true 486 } 487 488 if rel, ok := inDirLex(path, dir); ok { 489 return rel 490 } 491 xpath, err := filepath.EvalSymlinks(path) 492 if err != nil || xpath == path { 493 xpath = "" 494 } else { 495 if rel, ok := inDirLex(xpath, dir); ok { 496 return rel 497 } 498 } 499 500 xdir, err := filepath.EvalSymlinks(dir) 501 if err == nil && xdir != dir { 502 if rel, ok := inDirLex(path, xdir); ok { 503 return rel 504 } 505 if xpath != "" { 506 if rel, ok := inDirLex(xpath, xdir); ok { 507 return rel 508 } 509 } 510 } 511 return "" 512 }