| // Copyright 2017 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 search |
| |
| import ( |
| "cmd/go/internal/base" |
| "cmd/go/internal/cfg" |
| "cmd/go/internal/fsys" |
| "cmd/go/internal/str" |
| "cmd/internal/pkgpattern" |
| "fmt" |
| "go/build" |
| "io/fs" |
| "os" |
| "path" |
| "path/filepath" |
| "strings" |
| ) |
| |
| // A Match represents the result of matching a single package pattern. |
| type Match struct { |
| pattern string // the pattern itself |
| Dirs []string // if the pattern is local, directories that potentially contain matching packages |
| Pkgs []string // matching packages (import paths) |
| Errs []error // errors matching the patterns to packages, NOT errors loading those packages |
| |
| // Errs may be non-empty even if len(Pkgs) > 0, indicating that some matching |
| // packages could be located but results may be incomplete. |
| // If len(Pkgs) == 0 && len(Errs) == 0, the pattern is well-formed but did not |
| // match any packages. |
| } |
| |
| // NewMatch returns a Match describing the given pattern, |
| // without resolving its packages or errors. |
| func NewMatch(pattern string) *Match { |
| return &Match{pattern: pattern} |
| } |
| |
| // Pattern returns the pattern to be matched. |
| func (m *Match) Pattern() string { return m.pattern } |
| |
| // AddError appends a MatchError wrapping err to m.Errs. |
| func (m *Match) AddError(err error) { |
| m.Errs = append(m.Errs, &MatchError{Match: m, Err: err}) |
| } |
| |
| // IsLiteral reports whether the pattern is free of wildcards and meta-patterns. |
| // |
| // A literal pattern must match at most one package. |
| func (m *Match) IsLiteral() bool { |
| return !strings.Contains(m.pattern, "...") && !m.IsMeta() |
| } |
| |
| // IsLocal reports whether the pattern must be resolved from a specific root or |
| // directory, such as a filesystem path or a single module. |
| func (m *Match) IsLocal() bool { |
| return build.IsLocalImport(m.pattern) || filepath.IsAbs(m.pattern) |
| } |
| |
| // IsMeta reports whether the pattern is a “meta-package” keyword that represents |
| // multiple packages, such as "std", "cmd", or "all". |
| func (m *Match) IsMeta() bool { |
| return IsMetaPackage(m.pattern) |
| } |
| |
| // IsMetaPackage checks if name is a reserved package name that expands to multiple packages. |
| func IsMetaPackage(name string) bool { |
| return name == "std" || name == "cmd" || name == "all" |
| } |
| |
| // A MatchError indicates an error that occurred while attempting to match a |
| // pattern. |
| type MatchError struct { |
| Match *Match |
| Err error |
| } |
| |
| func (e *MatchError) Error() string { |
| if e.Match.IsLiteral() { |
| return fmt.Sprintf("%s: %v", e.Match.Pattern(), e.Err) |
| } |
| return fmt.Sprintf("pattern %s: %v", e.Match.Pattern(), e.Err) |
| } |
| |
| func (e *MatchError) Unwrap() error { |
| return e.Err |
| } |
| |
| // MatchPackages sets m.Pkgs to a non-nil slice containing all the packages that |
| // can be found under the $GOPATH directories and $GOROOT that match the |
| // pattern. The pattern must be either "all" (all packages), "std" (standard |
| // packages), "cmd" (standard commands), or a path including "...". |
| // |
| // If any errors may have caused the set of packages to be incomplete, |
| // MatchPackages appends those errors to m.Errs. |
| func (m *Match) MatchPackages() { |
| m.Pkgs = []string{} |
| if m.IsLocal() { |
| m.AddError(fmt.Errorf("internal error: MatchPackages: %s is not a valid package pattern", m.pattern)) |
| return |
| } |
| |
| if m.IsLiteral() { |
| m.Pkgs = []string{m.pattern} |
| return |
| } |
| |
| match := func(string) bool { return true } |
| treeCanMatch := func(string) bool { return true } |
| if !m.IsMeta() { |
| match = pkgpattern.MatchPattern(m.pattern) |
| treeCanMatch = pkgpattern.TreeCanMatchPattern(m.pattern) |
| } |
| |
| have := map[string]bool{ |
| "builtin": true, // ignore pseudo-package that exists only for documentation |
| } |
| if !cfg.BuildContext.CgoEnabled { |
| have["runtime/cgo"] = true // ignore during walk |
| } |
| |
| for _, src := range cfg.BuildContext.SrcDirs() { |
| if (m.pattern == "std" || m.pattern == "cmd") && src != cfg.GOROOTsrc { |
| continue |
| } |
| |
| // If the root itself is a symlink to a directory, |
| // we want to follow it (see https://go.dev/issue/50807). |
| // Add a trailing separator to force that to happen. |
| src = str.WithFilePathSeparator(filepath.Clean(src)) |
| root := src |
| if m.pattern == "cmd" { |
| root += "cmd" + string(filepath.Separator) |
| } |
| |
| err := fsys.Walk(root, func(path string, fi fs.FileInfo, err error) error { |
| if err != nil { |
| return err // Likely a permission error, which could interfere with matching. |
| } |
| if path == src { |
| return nil // GOROOT/src and GOPATH/src cannot contain packages. |
| } |
| |
| want := true |
| // Avoid .foo, _foo, and testdata directory trees. |
| _, elem := filepath.Split(path) |
| if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { |
| want = false |
| } |
| |
| name := filepath.ToSlash(path[len(src):]) |
| if m.pattern == "std" && (!IsStandardImportPath(name) || name == "cmd") { |
| // The name "std" is only the standard library. |
| // If the name is cmd, it's the root of the command tree. |
| want = false |
| } |
| if !treeCanMatch(name) { |
| want = false |
| } |
| |
| if !fi.IsDir() { |
| if fi.Mode()&fs.ModeSymlink != 0 && want && strings.Contains(m.pattern, "...") { |
| if target, err := fsys.Stat(path); err == nil && target.IsDir() { |
| fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path) |
| } |
| } |
| return nil |
| } |
| if !want { |
| return filepath.SkipDir |
| } |
| |
| if have[name] { |
| return nil |
| } |
| have[name] = true |
| if !match(name) { |
| return nil |
| } |
| pkg, err := cfg.BuildContext.ImportDir(path, 0) |
| if err != nil { |
| if _, noGo := err.(*build.NoGoError); noGo { |
| // The package does not actually exist, so record neither the package |
| // nor the error. |
| return nil |
| } |
| // There was an error importing path, but not matching it, |
| // which is all that Match promises to do. |
| // Ignore the import error. |
| } |
| |
| // If we are expanding "cmd", skip main |
| // packages under cmd/vendor. At least as of |
| // March, 2017, there is one there for the |
| // vendored pprof tool. |
| if m.pattern == "cmd" && pkg != nil && strings.HasPrefix(pkg.ImportPath, "cmd/vendor") && pkg.Name == "main" { |
| return nil |
| } |
| |
| m.Pkgs = append(m.Pkgs, name) |
| return nil |
| }) |
| if err != nil { |
| m.AddError(err) |
| } |
| } |
| } |
| |
| // MatchDirs sets m.Dirs to a non-nil slice containing all directories that |
| // potentially match a local pattern. The pattern must begin with an absolute |
| // path, or "./", or "../". On Windows, the pattern may use slash or backslash |
| // separators or a mix of both. |
| // |
| // If any errors may have caused the set of directories to be incomplete, |
| // MatchDirs appends those errors to m.Errs. |
| func (m *Match) MatchDirs(modRoots []string) { |
| m.Dirs = []string{} |
| if !m.IsLocal() { |
| m.AddError(fmt.Errorf("internal error: MatchDirs: %s is not a valid filesystem pattern", m.pattern)) |
| return |
| } |
| |
| if m.IsLiteral() { |
| m.Dirs = []string{m.pattern} |
| return |
| } |
| |
| // Clean the path and create a matching predicate. |
| // filepath.Clean removes "./" prefixes (and ".\" on Windows). We need to |
| // preserve these, since they are meaningful in MatchPattern and in |
| // returned import paths. |
| cleanPattern := filepath.Clean(m.pattern) |
| isLocal := strings.HasPrefix(m.pattern, "./") || (os.PathSeparator == '\\' && strings.HasPrefix(m.pattern, `.\`)) |
| prefix := "" |
| if cleanPattern != "." && isLocal { |
| prefix = "./" |
| cleanPattern = "." + string(os.PathSeparator) + cleanPattern |
| } |
| slashPattern := filepath.ToSlash(cleanPattern) |
| match := pkgpattern.MatchPattern(slashPattern) |
| |
| // Find directory to begin the scan. |
| // Could be smarter but this one optimization |
| // is enough for now, since ... is usually at the |
| // end of a path. |
| i := strings.Index(cleanPattern, "...") |
| dir, _ := filepath.Split(cleanPattern[:i]) |
| |
| // pattern begins with ./ or ../. |
| // path.Clean will discard the ./ but not the ../. |
| // We need to preserve the ./ for pattern matching |
| // and in the returned import paths. |
| |
| if len(modRoots) > 1 { |
| abs, err := filepath.Abs(dir) |
| if err != nil { |
| m.AddError(err) |
| return |
| } |
| var found bool |
| for _, modRoot := range modRoots { |
| if modRoot != "" && str.HasFilePathPrefix(abs, modRoot) { |
| found = true |
| } |
| } |
| if !found { |
| plural := "" |
| if len(modRoots) > 1 { |
| plural = "s" |
| } |
| m.AddError(fmt.Errorf("directory %s is outside module root%s (%s)", abs, plural, strings.Join(modRoots, ", "))) |
| } |
| } |
| |
| // If dir is actually a symlink to a directory, |
| // we want to follow it (see https://go.dev/issue/50807). |
| // Add a trailing separator to force that to happen. |
| dir = str.WithFilePathSeparator(dir) |
| err := fsys.Walk(dir, func(path string, fi fs.FileInfo, err error) error { |
| if err != nil { |
| return err // Likely a permission error, which could interfere with matching. |
| } |
| if !fi.IsDir() { |
| return nil |
| } |
| top := false |
| if path == dir { |
| // Walk starts at dir and recurses. For the recursive case, |
| // the path is the result of filepath.Join, which calls filepath.Clean. |
| // The initial case is not Cleaned, though, so we do this explicitly. |
| // |
| // This converts a path like "./io/" to "io". Without this step, running |
| // "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io |
| // package, because prepending the prefix "./" to the unclean path would |
| // result in "././io", and match("././io") returns false. |
| top = true |
| path = filepath.Clean(path) |
| } |
| |
| // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". |
| _, elem := filepath.Split(path) |
| dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." |
| if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { |
| return filepath.SkipDir |
| } |
| |
| if !top && cfg.ModulesEnabled { |
| // Ignore other modules found in subdirectories. |
| if fi, err := fsys.Stat(filepath.Join(path, "go.mod")); err == nil && !fi.IsDir() { |
| return filepath.SkipDir |
| } |
| } |
| |
| name := prefix + filepath.ToSlash(path) |
| if !match(name) { |
| return nil |
| } |
| |
| // We keep the directory if we can import it, or if we can't import it |
| // due to invalid Go source files. This means that directories containing |
| // parse errors will be built (and fail) instead of being silently skipped |
| // as not matching the pattern. Go 1.5 and earlier skipped, but that |
| // behavior means people miss serious mistakes. |
| // See golang.org/issue/11407. |
| if p, err := cfg.BuildContext.ImportDir(path, 0); err != nil && (p == nil || len(p.InvalidGoFiles) == 0) { |
| if _, noGo := err.(*build.NoGoError); noGo { |
| // The package does not actually exist, so record neither the package |
| // nor the error. |
| return nil |
| } |
| // There was an error importing path, but not matching it, |
| // which is all that Match promises to do. |
| // Ignore the import error. |
| } |
| m.Dirs = append(m.Dirs, name) |
| return nil |
| }) |
| if err != nil { |
| m.AddError(err) |
| } |
| } |
| |
| // WarnUnmatched warns about patterns that didn't match any packages. |
| func WarnUnmatched(matches []*Match) { |
| for _, m := range matches { |
| if len(m.Pkgs) == 0 && len(m.Errs) == 0 { |
| fmt.Fprintf(os.Stderr, "go: warning: %q matched no packages\n", m.pattern) |
| } |
| } |
| } |
| |
| // ImportPaths returns the matching paths to use for the given command line. |
| // It calls ImportPathsQuiet and then WarnUnmatched. |
| func ImportPaths(patterns, modRoots []string) []*Match { |
| matches := ImportPathsQuiet(patterns, modRoots) |
| WarnUnmatched(matches) |
| return matches |
| } |
| |
| // ImportPathsQuiet is like ImportPaths but does not warn about patterns with no matches. |
| func ImportPathsQuiet(patterns, modRoots []string) []*Match { |
| var out []*Match |
| for _, a := range CleanPatterns(patterns) { |
| m := NewMatch(a) |
| if m.IsLocal() { |
| m.MatchDirs(modRoots) |
| |
| // Change the file import path to a regular import path if the package |
| // is in GOPATH or GOROOT. We don't report errors here; LoadImport |
| // (or something similar) will report them later. |
| m.Pkgs = make([]string, len(m.Dirs)) |
| for i, dir := range m.Dirs { |
| absDir := dir |
| if !filepath.IsAbs(dir) { |
| absDir = filepath.Join(base.Cwd(), dir) |
| } |
| if bp, _ := cfg.BuildContext.ImportDir(absDir, build.FindOnly); bp.ImportPath != "" && bp.ImportPath != "." { |
| m.Pkgs[i] = bp.ImportPath |
| } else { |
| m.Pkgs[i] = dir |
| } |
| } |
| } else { |
| m.MatchPackages() |
| } |
| |
| out = append(out, m) |
| } |
| return out |
| } |
| |
| // CleanPatterns returns the patterns to use for the given command line. It |
| // canonicalizes the patterns but does not evaluate any matches. For patterns |
| // that are not local or absolute paths, it preserves text after '@' to avoid |
| // modifying version queries. |
| func CleanPatterns(patterns []string) []string { |
| if len(patterns) == 0 { |
| return []string{"."} |
| } |
| var out []string |
| for _, a := range patterns { |
| var p, v string |
| if build.IsLocalImport(a) || filepath.IsAbs(a) { |
| p = a |
| } else if i := strings.IndexByte(a, '@'); i < 0 { |
| p = a |
| } else { |
| p = a[:i] |
| v = a[i:] |
| } |
| |
| // Arguments may be either file paths or import paths. |
| // As a courtesy to Windows developers, rewrite \ to / |
| // in arguments that look like import paths. |
| // Don't replace slashes in absolute paths. |
| if filepath.IsAbs(p) { |
| p = filepath.Clean(p) |
| } else { |
| if filepath.Separator == '\\' { |
| p = strings.ReplaceAll(p, `\`, `/`) |
| } |
| |
| // Put argument in canonical form, but preserve leading ./. |
| if strings.HasPrefix(p, "./") { |
| p = "./" + path.Clean(p) |
| if p == "./." { |
| p = "." |
| } |
| } else { |
| p = path.Clean(p) |
| } |
| } |
| |
| out = append(out, p+v) |
| } |
| return out |
| } |
| |
| // IsStandardImportPath reports whether $GOROOT/src/path should be considered |
| // part of the standard distribution. For historical reasons we allow people to add |
| // their own code to $GOROOT instead of using $GOPATH, but we assume that |
| // code will start with a domain name (dot in the first element). |
| // |
| // Note that this function is meant to evaluate whether a directory found in GOROOT |
| // should be treated as part of the standard library. It should not be used to decide |
| // that a directory found in GOPATH should be rejected: directories in GOPATH |
| // need not have dots in the first element, and they just take their chances |
| // with future collisions in the standard library. |
| func IsStandardImportPath(path string) bool { |
| i := strings.Index(path, "/") |
| if i < 0 { |
| i = len(path) |
| } |
| elem := path[:i] |
| return !strings.Contains(elem, ".") |
| } |
| |
| // IsRelativePath reports whether pattern should be interpreted as a directory |
| // path relative to the current directory, as opposed to a pattern matching |
| // import paths. |
| func IsRelativePath(pattern string) bool { |
| return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".." |
| } |
| |
| // InDir checks whether path is in the file tree rooted at dir. |
| // If so, InDir returns an equivalent path relative to dir. |
| // If not, InDir returns an empty string. |
| // InDir makes some effort to succeed even in the presence of symbolic links. |
| func InDir(path, dir string) string { |
| // inDirLex reports whether path is lexically in dir, |
| // without considering symbolic or hard links. |
| inDirLex := func(path, dir string) (string, bool) { |
| if dir == "" { |
| return path, true |
| } |
| rel := str.TrimFilePathPrefix(path, dir) |
| if rel == path { |
| return "", false |
| } |
| if rel == "" { |
| return ".", true |
| } |
| return rel, true |
| } |
| |
| if rel, ok := inDirLex(path, dir); ok { |
| return rel |
| } |
| xpath, err := filepath.EvalSymlinks(path) |
| if err != nil || xpath == path { |
| xpath = "" |
| } else { |
| if rel, ok := inDirLex(xpath, dir); ok { |
| return rel |
| } |
| } |
| |
| xdir, err := filepath.EvalSymlinks(dir) |
| if err == nil && xdir != dir { |
| if rel, ok := inDirLex(path, xdir); ok { |
| return rel |
| } |
| if xpath != "" { |
| if rel, ok := inDirLex(xpath, xdir); ok { |
| return rel |
| } |
| } |
| } |
| return "" |
| } |