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 }