github.com/gernest/nezuko@v0.1.2/internal/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  	"go/build"
    10  	"log"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  
    17  	"github.com/gernest/nezuko/internal/base"
    18  	"github.com/gernest/nezuko/internal/cfg"
    19  )
    20  
    21  // A Match represents the result of matching a single package pattern.
    22  type Match struct {
    23  	Pattern string   // the pattern itself
    24  	Literal bool     // whether it is a literal (no wildcards)
    25  	Pkgs    []string // matching packages (dirs or import paths)
    26  }
    27  
    28  // MatchPackages returns all the packages that can be found
    29  // under the $GOPATH directories and $GOROOT matching pattern.
    30  // The pattern is either "all" (all packages), "std" (standard packages),
    31  // "cmd" (standard commands), or a path including "...".
    32  func MatchPackages(pattern string) *Match {
    33  	m := &Match{
    34  		Pattern: pattern,
    35  		Literal: false,
    36  	}
    37  	return m
    38  }
    39  
    40  var modRoot string
    41  
    42  func SetModRoot(dir string) {
    43  	modRoot = dir
    44  }
    45  
    46  // MatchPackagesInFS is like allPackages but is passed a pattern
    47  // beginning ./ or ../, meaning it should scan the tree rooted
    48  // at the given directory. There are ... in the pattern too.
    49  // (See go help packages for pattern syntax.)
    50  func MatchPackagesInFS(pattern string) *Match {
    51  	m := &Match{
    52  		Pattern: pattern,
    53  		Literal: false,
    54  	}
    55  
    56  	// Find directory to begin the scan.
    57  	// Could be smarter but this one optimization
    58  	// is enough for now, since ... is usually at the
    59  	// end of a path.
    60  	i := strings.Index(pattern, "...")
    61  	dir, _ := path.Split(pattern[:i])
    62  
    63  	// pattern begins with ./ or ../.
    64  	// path.Clean will discard the ./ but not the ../.
    65  	// We need to preserve the ./ for pattern matching
    66  	// and in the returned import paths.
    67  	prefix := ""
    68  	if strings.HasPrefix(pattern, "./") {
    69  		prefix = "./"
    70  	}
    71  	match := MatchPattern(pattern)
    72  
    73  	if modRoot != "" {
    74  		abs, err := filepath.Abs(dir)
    75  		if err != nil {
    76  			base.Fatalf("z: %v", err)
    77  		}
    78  		if !hasFilepathPrefix(abs, modRoot) {
    79  			base.Fatalf("z: pattern %s refers to dir %s, outside module root %s", pattern, abs, modRoot)
    80  			return nil
    81  		}
    82  	}
    83  
    84  	filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
    85  		if err != nil || !fi.IsDir() {
    86  			return nil
    87  		}
    88  		top := false
    89  		if path == dir {
    90  			// filepath.Walk starts at dir and recurses. For the recursive case,
    91  			// the path is the result of filepath.Join, which calls filepath.Clean.
    92  			// The initial case is not Cleaned, though, so we do this explicitly.
    93  			//
    94  			// This converts a path like "./io/" to "io". Without this step, running
    95  			// "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io
    96  			// package, because prepending the prefix "./" to the unclean path would
    97  			// result in "././io", and match("././io") returns false.
    98  			top = true
    99  			path = filepath.Clean(path)
   100  		}
   101  
   102  		// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
   103  		_, elem := filepath.Split(path)
   104  		dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
   105  		if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
   106  			return filepath.SkipDir
   107  		}
   108  
   109  		if !top && cfg.ModulesEnabled {
   110  			// Ignore other modules found in subdirectories.
   111  			if _, err := os.Stat(filepath.Join(path, "z.mod")); err == nil {
   112  				return filepath.SkipDir
   113  			}
   114  		}
   115  
   116  		name := prefix + filepath.ToSlash(path)
   117  		if !match(name) {
   118  			return nil
   119  		}
   120  
   121  		// We keep the directory if we can import it, or if we can't import it
   122  		// due to invalid Go source files. This means that directories containing
   123  		// parse errors will be built (and fail) instead of being silently skipped
   124  		// as not matching the pattern. Go 1.5 and earlier skipped, but that
   125  		// behavior means people miss serious mistakes.
   126  		// See golang.org/issue/11407.
   127  		if _, err := cfg.BuildContext.ImportDir(path, 0); err != nil {
   128  			log.Print(err)
   129  			return nil
   130  		}
   131  		m.Pkgs = append(m.Pkgs, name)
   132  		return nil
   133  	})
   134  	return m
   135  }
   136  
   137  // TreeCanMatchPattern(pattern)(name) reports whether
   138  // name or children of name can possibly match pattern.
   139  // Pattern is the same limited glob accepted by matchPattern.
   140  func TreeCanMatchPattern(pattern string) func(name string) bool {
   141  	wildCard := false
   142  	if i := strings.Index(pattern, "..."); i >= 0 {
   143  		wildCard = true
   144  		pattern = pattern[:i]
   145  	}
   146  	return func(name string) bool {
   147  		return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
   148  			wildCard && strings.HasPrefix(name, pattern)
   149  	}
   150  }
   151  
   152  // MatchPattern(pattern)(name) reports whether
   153  // name matches pattern. Pattern is a limited glob
   154  // pattern in which '...' means 'any string' and there
   155  // is no other special syntax.
   156  // Unfortunately, there are two special cases. Quoting "go help packages":
   157  //
   158  // First, /... at the end of the pattern can match an empty string,
   159  // so that net/... matches both net and packages in its subdirectories, like net/http.
   160  // Second, any slash-separted pattern element containing a wildcard never
   161  // participates in a match of the "vendor" element in the path of a vendored
   162  // package, so that ./... does not match packages in subdirectories of
   163  // ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do.
   164  // Note, however, that a directory named vendor that itself contains code
   165  // is not a vendored package: cmd/vendor would be a command named vendor,
   166  // and the pattern cmd/... matches it.
   167  func MatchPattern(pattern string) func(name string) bool {
   168  	// Convert pattern to regular expression.
   169  	// The strategy for the trailing /... is to nest it in an explicit ? expression.
   170  	// The strategy for the vendor exclusion is to change the unmatchable
   171  	// vendor strings to a disallowed code point (vendorChar) and to use
   172  	// "(anything but that codepoint)*" as the implementation of the ... wildcard.
   173  	// This is a bit complicated but the obvious alternative,
   174  	// namely a hand-written search like in most shell glob matchers,
   175  	// is too easy to make accidentally exponential.
   176  	// Using package regexp guarantees linear-time matching.
   177  
   178  	const vendorChar = "\x00"
   179  
   180  	if strings.Contains(pattern, vendorChar) {
   181  		return func(name string) bool { return false }
   182  	}
   183  
   184  	re := regexp.QuoteMeta(pattern)
   185  	re = replaceVendor(re, vendorChar)
   186  	switch {
   187  	case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`):
   188  		re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)`
   189  	case re == vendorChar+`/\.\.\.`:
   190  		re = `(/vendor|/` + vendorChar + `/\.\.\.)`
   191  	case strings.HasSuffix(re, `/\.\.\.`):
   192  		re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?`
   193  	}
   194  	re = strings.ReplaceAll(re, `\.\.\.`, `[^`+vendorChar+`]*`)
   195  
   196  	reg := regexp.MustCompile(`^` + re + `$`)
   197  
   198  	return func(name string) bool {
   199  		if strings.Contains(name, vendorChar) {
   200  			return false
   201  		}
   202  		return reg.MatchString(replaceVendor(name, vendorChar))
   203  	}
   204  }
   205  
   206  // replaceVendor returns the result of replacing
   207  // non-trailing vendor path elements in x with repl.
   208  func replaceVendor(x, repl string) string {
   209  	if !strings.Contains(x, "vendor") {
   210  		return x
   211  	}
   212  	elem := strings.Split(x, "/")
   213  	for i := 0; i < len(elem)-1; i++ {
   214  		if elem[i] == "vendor" {
   215  			elem[i] = repl
   216  		}
   217  	}
   218  	return strings.Join(elem, "/")
   219  }
   220  
   221  // WarnUnmatched warns about patterns that didn't match any packages.
   222  func WarnUnmatched(matches []*Match) {
   223  	for _, m := range matches {
   224  		if len(m.Pkgs) == 0 {
   225  			fmt.Fprintf(os.Stderr, "z: warning: %q matched no packages\n", m.Pattern)
   226  		}
   227  	}
   228  }
   229  
   230  // ImportPaths returns the matching paths to use for the given command line.
   231  // It calls ImportPathsQuiet and then WarnUnmatched.
   232  func ImportPaths(patterns []string) []*Match {
   233  	matches := ImportPathsQuiet(patterns)
   234  	WarnUnmatched(matches)
   235  	return matches
   236  }
   237  
   238  // ImportPathsQuiet is like ImportPaths but does not warn about patterns with no matches.
   239  func ImportPathsQuiet(patterns []string) []*Match {
   240  	var out []*Match
   241  	for _, a := range CleanPatterns(patterns) {
   242  		if IsMetaPackage(a) {
   243  			out = append(out, MatchPackages(a))
   244  			continue
   245  		}
   246  		if strings.Contains(a, "...") {
   247  			if build.IsLocalImport(a) {
   248  				out = append(out, MatchPackagesInFS(a))
   249  			} else {
   250  				out = append(out, MatchPackages(a))
   251  			}
   252  			continue
   253  		}
   254  		out = append(out, &Match{Pattern: a, Literal: true, Pkgs: []string{a}})
   255  	}
   256  	return out
   257  }
   258  
   259  // CleanPatterns returns the patterns to use for the given
   260  // command line. It canonicalizes the patterns but does not
   261  // evaluate any matches.
   262  func CleanPatterns(patterns []string) []string {
   263  	if len(patterns) == 0 {
   264  		return []string{"."}
   265  	}
   266  	var out []string
   267  	for _, a := range patterns {
   268  		// Arguments are supposed to be import paths, but
   269  		// as a courtesy to Windows developers, rewrite \ to /
   270  		// in command-line arguments. Handles .\... and so on.
   271  		if filepath.Separator == '\\' {
   272  			a = strings.ReplaceAll(a, `\`, `/`)
   273  		}
   274  
   275  		// Put argument in canonical form, but preserve leading ./.
   276  		if strings.HasPrefix(a, "./") {
   277  			a = "./" + path.Clean(a)
   278  			if a == "./." {
   279  				a = "."
   280  			}
   281  		} else {
   282  			a = path.Clean(a)
   283  		}
   284  		out = append(out, a)
   285  	}
   286  	return out
   287  }
   288  
   289  // IsMetaPackage checks if name is a reserved package name that expands to multiple packages.
   290  func IsMetaPackage(name string) bool {
   291  	return name == "std" || name == "cmd" || name == "all"
   292  }
   293  
   294  // hasPathPrefix reports whether the path s begins with the
   295  // elements in prefix.
   296  func hasPathPrefix(s, prefix string) bool {
   297  	switch {
   298  	default:
   299  		return false
   300  	case len(s) == len(prefix):
   301  		return s == prefix
   302  	case len(s) > len(prefix):
   303  		if prefix != "" && prefix[len(prefix)-1] == '/' {
   304  			return strings.HasPrefix(s, prefix)
   305  		}
   306  		return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
   307  	}
   308  }
   309  
   310  // hasFilepathPrefix reports whether the path s begins with the
   311  // elements in prefix.
   312  func hasFilepathPrefix(s, prefix string) bool {
   313  	switch {
   314  	default:
   315  		return false
   316  	case len(s) == len(prefix):
   317  		return s == prefix
   318  	case len(s) > len(prefix):
   319  		if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
   320  			return strings.HasPrefix(s, prefix)
   321  		}
   322  		return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
   323  	}
   324  }
   325  
   326  // IsStandardImportPath reports whether $GOROOT/src/path should be considered
   327  // part of the standard distribution. For historical reasons we allow people to add
   328  // their own code to $GOROOT instead of using $GOPATH, but we assume that
   329  // code will start with a domain name (dot in the first element).
   330  //
   331  // Note that this function is meant to evaluate whether a directory found in GOROOT
   332  // should be treated as part of the standard library. It should not be used to decide
   333  // that a directory found in GOPATH should be rejected: directories in GOPATH
   334  // need not have dots in the first element, and they just take their chances
   335  // with future collisions in the standard library.
   336  func IsStandardImportPath(path string) bool {
   337  	i := strings.Index(path, "/")
   338  	if i < 0 {
   339  		i = len(path)
   340  	}
   341  	elem := path[:i]
   342  	return !strings.Contains(elem, ".")
   343  }
   344  
   345  // IsRelativePath reports whether pattern should be interpreted as a directory
   346  // path relative to the current directory, as opposed to a pattern matching
   347  // import paths.
   348  func IsRelativePath(pattern string) bool {
   349  	return strings.HasPrefix(pattern, "./") || strings.HasPrefix(pattern, "../") || pattern == "." || pattern == ".."
   350  }
   351  
   352  // InDir checks whether path is in the file tree rooted at dir.
   353  // If so, InDir returns an equivalent path relative to dir.
   354  // If not, InDir returns an empty string.
   355  // InDir makes some effort to succeed even in the presence of symbolic links.
   356  // TODO(rsc): Replace internal/test.inDir with a call to this function for Go 1.12.
   357  func InDir(path, dir string) string {
   358  	if rel := inDirLex(path, dir); rel != "" {
   359  		return rel
   360  	}
   361  	xpath, err := filepath.EvalSymlinks(path)
   362  	if err != nil || xpath == path {
   363  		xpath = ""
   364  	} else {
   365  		if rel := inDirLex(xpath, dir); rel != "" {
   366  			return rel
   367  		}
   368  	}
   369  
   370  	xdir, err := filepath.EvalSymlinks(dir)
   371  	if err == nil && xdir != dir {
   372  		if rel := inDirLex(path, xdir); rel != "" {
   373  			return rel
   374  		}
   375  		if xpath != "" {
   376  			if rel := inDirLex(xpath, xdir); rel != "" {
   377  				return rel
   378  			}
   379  		}
   380  	}
   381  	return ""
   382  }
   383  
   384  // inDirLex is like inDir but only checks the lexical form of the file names.
   385  // It does not consider symbolic links.
   386  // TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to
   387  // return the suffix. Most uses of str.HasFilePathPrefix should probably
   388  // be calling InDir instead.
   389  func inDirLex(path, dir string) string {
   390  	pv := strings.ToUpper(filepath.VolumeName(path))
   391  	dv := strings.ToUpper(filepath.VolumeName(dir))
   392  	path = path[len(pv):]
   393  	dir = dir[len(dv):]
   394  	switch {
   395  	default:
   396  		return ""
   397  	case pv != dv:
   398  		return ""
   399  	case len(path) == len(dir):
   400  		if path == dir {
   401  			return "."
   402  		}
   403  		return ""
   404  	case dir == "":
   405  		return path
   406  	case len(path) > len(dir):
   407  		if dir[len(dir)-1] == filepath.Separator {
   408  			if path[:len(dir)] == dir {
   409  				return path[len(dir):]
   410  			}
   411  			return ""
   412  		}
   413  		if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
   414  			if len(path) == len(dir)+1 {
   415  				return "."
   416  			}
   417  			return path[len(dir)+1:]
   418  		}
   419  		return ""
   420  	}
   421  }