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  }