Source file src/cmd/go/internal/search/search.go

     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  	"cmd/go/internal/base"
     9  	"cmd/go/internal/cfg"
    10  	"cmd/go/internal/fsys"
    11  	"cmd/go/internal/str"
    12  	"cmd/internal/pkgpattern"
    13  	"fmt"
    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", "tool", 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 == "tool" || 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  	patterns = CleanPatterns(patterns)
   365  	out := make([]*Match, 0, len(patterns))
   366  	for _, a := range patterns {
   367  		m := NewMatch(a)
   368  		if m.IsLocal() {
   369  			m.MatchDirs(modRoots)
   370  
   371  			// Change the file import path to a regular import path if the package
   372  			// is in GOPATH or GOROOT. We don't report errors here; LoadImport
   373  			// (or something similar) will report them later.
   374  			m.Pkgs = make([]string, len(m.Dirs))
   375  			for i, dir := range m.Dirs {
   376  				absDir := dir
   377  				if !filepath.IsAbs(dir) {
   378  					absDir = filepath.Join(base.Cwd(), dir)
   379  				}
   380  				if bp, _ := cfg.BuildContext.ImportDir(absDir, build.FindOnly); bp.ImportPath != "" && bp.ImportPath != "." {
   381  					m.Pkgs[i] = bp.ImportPath
   382  				} else {
   383  					m.Pkgs[i] = dir
   384  				}
   385  			}
   386  		} else {
   387  			m.MatchPackages()
   388  		}
   389  
   390  		out = append(out, m)
   391  	}
   392  	return out
   393  }
   394  
   395  // CleanPatterns returns the patterns to use for the given command line. It
   396  // canonicalizes the patterns but does not evaluate any matches. For patterns
   397  // that are not local or absolute paths, it preserves text after '@' to avoid
   398  // modifying version queries.
   399  func CleanPatterns(patterns []string) []string {
   400  	if len(patterns) == 0 {
   401  		return []string{"."}
   402  	}
   403  	out := make([]string, 0, len(patterns))
   404  	for _, a := range patterns {
   405  		var p, v string
   406  		if build.IsLocalImport(a) || filepath.IsAbs(a) {
   407  			p = a
   408  		} else if i := strings.IndexByte(a, '@'); i < 0 {
   409  			p = a
   410  		} else {
   411  			p = a[:i]
   412  			v = a[i:]
   413  		}
   414  
   415  		// Arguments may be either file paths or import paths.
   416  		// As a courtesy to Windows developers, rewrite \ to /
   417  		// in arguments that look like import paths.
   418  		// Don't replace slashes in absolute paths.
   419  		if filepath.IsAbs(p) {
   420  			p = filepath.Clean(p)
   421  		} else {
   422  			if filepath.Separator == '\\' {
   423  				p = strings.ReplaceAll(p, `\`, `/`)
   424  			}
   425  
   426  			// Put argument in canonical form, but preserve leading ./.
   427  			if strings.HasPrefix(p, "./") {
   428  				p = "./" + path.Clean(p)
   429  				if p == "./." {
   430  					p = "."
   431  				}
   432  			} else {
   433  				p = path.Clean(p)
   434  			}
   435  		}
   436  
   437  		out = append(out, p+v)
   438  	}
   439  	return out
   440  }
   441  
   442  // IsStandardImportPath reports whether $GOROOT/src/path should be considered
   443  // part of the standard distribution. For historical reasons we allow people to add
   444  // their own code to $GOROOT instead of using $GOPATH, but we assume that
   445  // code will start with a domain name (dot in the first element).
   446  //
   447  // Note that this function is meant to evaluate whether a directory found in GOROOT
   448  // should be treated as part of the standard library. It should not be used to decide
   449  // that a directory found in GOPATH should be rejected: directories in GOPATH
   450  // need not have dots in the first element, and they just take their chances
   451  // with future collisions in the standard library.
   452  func IsStandardImportPath(path string) bool {
   453  	i := strings.Index(path, "/")
   454  	if i < 0 {
   455  		i = len(path)
   456  	}
   457  	elem := path[:i]
   458  	return !strings.Contains(elem, ".")
   459  }
   460  
   461  // IsRelativePath reports whether pattern should be interpreted as a directory
   462  // path relative to the current directory, as opposed to a pattern matching
   463  // import paths.
   464  func IsRelativePath(pattern string) bool {
   465  	return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".."
   466  }
   467  
   468  // InDir checks whether path is in the file tree rooted at dir.
   469  // If so, InDir returns an equivalent path relative to dir.
   470  // If not, InDir returns an empty string.
   471  // InDir makes some effort to succeed even in the presence of symbolic links.
   472  func InDir(path, dir string) string {
   473  	// inDirLex reports whether path is lexically in dir,
   474  	// without considering symbolic or hard links.
   475  	inDirLex := func(path, dir string) (string, bool) {
   476  		if dir == "" {
   477  			return path, true
   478  		}
   479  		rel := str.TrimFilePathPrefix(path, dir)
   480  		if rel == path {
   481  			return "", false
   482  		}
   483  		if rel == "" {
   484  			return ".", true
   485  		}
   486  		return rel, true
   487  	}
   488  
   489  	if rel, ok := inDirLex(path, dir); ok {
   490  		return rel
   491  	}
   492  	xpath, err := filepath.EvalSymlinks(path)
   493  	if err != nil || xpath == path {
   494  		xpath = ""
   495  	} else {
   496  		if rel, ok := inDirLex(xpath, dir); ok {
   497  			return rel
   498  		}
   499  	}
   500  
   501  	xdir, err := filepath.EvalSymlinks(dir)
   502  	if err == nil && xdir != dir {
   503  		if rel, ok := inDirLex(path, xdir); ok {
   504  			return rel
   505  		}
   506  		if xpath != "" {
   507  			if rel, ok := inDirLex(xpath, xdir); ok {
   508  				return rel
   509  			}
   510  		}
   511  	}
   512  	return ""
   513  }
   514  

View as plain text