gopkg.in/alecthomas/gometalinter.v3@v3.0.0/_linters/src/github.com/kisielk/gotool/internal/load/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 // +build go1.9 6 7 package load 8 9 import ( 10 "fmt" 11 "go/build" 12 "log" 13 "os" 14 "path" 15 "path/filepath" 16 "regexp" 17 "strings" 18 ) 19 20 // Context specifies values for operation of ImportPaths that would 21 // otherwise come from cmd/go/internal/cfg package. 22 // 23 // This is a construct added for gotool purposes and doesn't have 24 // an equivalent upstream in cmd/go. 25 type Context struct { 26 // BuildContext is the build context to use. 27 BuildContext build.Context 28 29 // GOROOTsrc is the location of the src directory in GOROOT. 30 // At this time, it's used only in MatchPackages to skip 31 // GOOROOT/src entry from BuildContext.SrcDirs output. 32 GOROOTsrc string 33 } 34 35 // allPackages returns all the packages that can be found 36 // under the $GOPATH directories and $GOROOT matching pattern. 37 // The pattern is either "all" (all packages), "std" (standard packages), 38 // "cmd" (standard commands), or a path including "...". 39 func (c *Context) allPackages(pattern string) []string { 40 pkgs := c.MatchPackages(pattern) 41 if len(pkgs) == 0 { 42 fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) 43 } 44 return pkgs 45 } 46 47 // allPackagesInFS is like allPackages but is passed a pattern 48 // beginning ./ or ../, meaning it should scan the tree rooted 49 // at the given directory. There are ... in the pattern too. 50 func (c *Context) allPackagesInFS(pattern string) []string { 51 pkgs := c.MatchPackagesInFS(pattern) 52 if len(pkgs) == 0 { 53 fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) 54 } 55 return pkgs 56 } 57 58 // MatchPackages returns a list of package paths matching pattern 59 // (see go help packages for pattern syntax). 60 func (c *Context) MatchPackages(pattern string) []string { 61 match := func(string) bool { return true } 62 treeCanMatch := func(string) bool { return true } 63 if !IsMetaPackage(pattern) { 64 match = matchPattern(pattern) 65 treeCanMatch = treeCanMatchPattern(pattern) 66 } 67 68 have := map[string]bool{ 69 "builtin": true, // ignore pseudo-package that exists only for documentation 70 } 71 if !c.BuildContext.CgoEnabled { 72 have["runtime/cgo"] = true // ignore during walk 73 } 74 var pkgs []string 75 76 for _, src := range c.BuildContext.SrcDirs() { 77 if (pattern == "std" || pattern == "cmd") && src != c.GOROOTsrc { 78 continue 79 } 80 src = filepath.Clean(src) + string(filepath.Separator) 81 root := src 82 if pattern == "cmd" { 83 root += "cmd" + string(filepath.Separator) 84 } 85 filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { 86 if err != nil || path == src { 87 return nil 88 } 89 90 want := true 91 // Avoid .foo, _foo, and testdata directory trees. 92 _, elem := filepath.Split(path) 93 if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { 94 want = false 95 } 96 97 name := filepath.ToSlash(path[len(src):]) 98 if pattern == "std" && (!isStandardImportPath(name) || name == "cmd") { 99 // The name "std" is only the standard library. 100 // If the name is cmd, it's the root of the command tree. 101 want = false 102 } 103 if !treeCanMatch(name) { 104 want = false 105 } 106 107 if !fi.IsDir() { 108 if fi.Mode()&os.ModeSymlink != 0 && want { 109 if target, err := os.Stat(path); err == nil && target.IsDir() { 110 fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path) 111 } 112 } 113 return nil 114 } 115 if !want { 116 return filepath.SkipDir 117 } 118 119 if have[name] { 120 return nil 121 } 122 have[name] = true 123 if !match(name) { 124 return nil 125 } 126 pkg, err := c.BuildContext.ImportDir(path, 0) 127 if err != nil { 128 if _, noGo := err.(*build.NoGoError); noGo { 129 return nil 130 } 131 } 132 133 // If we are expanding "cmd", skip main 134 // packages under cmd/vendor. At least as of 135 // March, 2017, there is one there for the 136 // vendored pprof tool. 137 if pattern == "cmd" && strings.HasPrefix(pkg.ImportPath, "cmd/vendor") && pkg.Name == "main" { 138 return nil 139 } 140 141 pkgs = append(pkgs, name) 142 return nil 143 }) 144 } 145 return pkgs 146 } 147 148 // MatchPackagesInFS returns a list of package paths matching pattern, 149 // which must begin with ./ or ../ 150 // (see go help packages for pattern syntax). 151 func (c *Context) MatchPackagesInFS(pattern string) []string { 152 // Find directory to begin the scan. 153 // Could be smarter but this one optimization 154 // is enough for now, since ... is usually at the 155 // end of a path. 156 i := strings.Index(pattern, "...") 157 dir, _ := path.Split(pattern[:i]) 158 159 // pattern begins with ./ or ../. 160 // path.Clean will discard the ./ but not the ../. 161 // We need to preserve the ./ for pattern matching 162 // and in the returned import paths. 163 prefix := "" 164 if strings.HasPrefix(pattern, "./") { 165 prefix = "./" 166 } 167 match := matchPattern(pattern) 168 169 var pkgs []string 170 filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { 171 if err != nil || !fi.IsDir() { 172 return nil 173 } 174 if path == dir { 175 // filepath.Walk starts at dir and recurses. For the recursive case, 176 // the path is the result of filepath.Join, which calls filepath.Clean. 177 // The initial case is not Cleaned, though, so we do this explicitly. 178 // 179 // This converts a path like "./io/" to "io". Without this step, running 180 // "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io 181 // package, because prepending the prefix "./" to the unclean path would 182 // result in "././io", and match("././io") returns false. 183 path = filepath.Clean(path) 184 } 185 186 // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". 187 _, elem := filepath.Split(path) 188 dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." 189 if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { 190 return filepath.SkipDir 191 } 192 193 name := prefix + filepath.ToSlash(path) 194 if !match(name) { 195 return nil 196 } 197 198 // We keep the directory if we can import it, or if we can't import it 199 // due to invalid Go source files. This means that directories containing 200 // parse errors will be built (and fail) instead of being silently skipped 201 // as not matching the pattern. Go 1.5 and earlier skipped, but that 202 // behavior means people miss serious mistakes. 203 // See golang.org/issue/11407. 204 if p, err := c.BuildContext.ImportDir(path, 0); err != nil && (p == nil || len(p.InvalidGoFiles) == 0) { 205 if _, noGo := err.(*build.NoGoError); !noGo { 206 log.Print(err) 207 } 208 return nil 209 } 210 pkgs = append(pkgs, name) 211 return nil 212 }) 213 return pkgs 214 } 215 216 // treeCanMatchPattern(pattern)(name) reports whether 217 // name or children of name can possibly match pattern. 218 // Pattern is the same limited glob accepted by matchPattern. 219 func treeCanMatchPattern(pattern string) func(name string) bool { 220 wildCard := false 221 if i := strings.Index(pattern, "..."); i >= 0 { 222 wildCard = true 223 pattern = pattern[:i] 224 } 225 return func(name string) bool { 226 return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || 227 wildCard && strings.HasPrefix(name, pattern) 228 } 229 } 230 231 // matchPattern(pattern)(name) reports whether 232 // name matches pattern. Pattern is a limited glob 233 // pattern in which '...' means 'any string' and there 234 // is no other special syntax. 235 // Unfortunately, there are two special cases. Quoting "go help packages": 236 // 237 // First, /... at the end of the pattern can match an empty string, 238 // so that net/... matches both net and packages in its subdirectories, like net/http. 239 // Second, any slash-separted pattern element containing a wildcard never 240 // participates in a match of the "vendor" element in the path of a vendored 241 // package, so that ./... does not match packages in subdirectories of 242 // ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do. 243 // Note, however, that a directory named vendor that itself contains code 244 // is not a vendored package: cmd/vendor would be a command named vendor, 245 // and the pattern cmd/... matches it. 246 func matchPattern(pattern string) func(name string) bool { 247 // Convert pattern to regular expression. 248 // The strategy for the trailing /... is to nest it in an explicit ? expression. 249 // The strategy for the vendor exclusion is to change the unmatchable 250 // vendor strings to a disallowed code point (vendorChar) and to use 251 // "(anything but that codepoint)*" as the implementation of the ... wildcard. 252 // This is a bit complicated but the obvious alternative, 253 // namely a hand-written search like in most shell glob matchers, 254 // is too easy to make accidentally exponential. 255 // Using package regexp guarantees linear-time matching. 256 257 const vendorChar = "\x00" 258 259 if strings.Contains(pattern, vendorChar) { 260 return func(name string) bool { return false } 261 } 262 263 re := regexp.QuoteMeta(pattern) 264 re = replaceVendor(re, vendorChar) 265 switch { 266 case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`): 267 re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)` 268 case re == vendorChar+`/\.\.\.`: 269 re = `(/vendor|/` + vendorChar + `/\.\.\.)` 270 case strings.HasSuffix(re, `/\.\.\.`): 271 re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?` 272 } 273 re = strings.Replace(re, `\.\.\.`, `[^`+vendorChar+`]*`, -1) 274 275 reg := regexp.MustCompile(`^` + re + `$`) 276 277 return func(name string) bool { 278 if strings.Contains(name, vendorChar) { 279 return false 280 } 281 return reg.MatchString(replaceVendor(name, vendorChar)) 282 } 283 } 284 285 // replaceVendor returns the result of replacing 286 // non-trailing vendor path elements in x with repl. 287 func replaceVendor(x, repl string) string { 288 if !strings.Contains(x, "vendor") { 289 return x 290 } 291 elem := strings.Split(x, "/") 292 for i := 0; i < len(elem)-1; i++ { 293 if elem[i] == "vendor" { 294 elem[i] = repl 295 } 296 } 297 return strings.Join(elem, "/") 298 } 299 300 // ImportPaths returns the import paths to use for the given command line. 301 func (c *Context) ImportPaths(args []string) []string { 302 args = c.ImportPathsNoDotExpansion(args) 303 var out []string 304 for _, a := range args { 305 if strings.Contains(a, "...") { 306 if build.IsLocalImport(a) { 307 out = append(out, c.allPackagesInFS(a)...) 308 } else { 309 out = append(out, c.allPackages(a)...) 310 } 311 continue 312 } 313 out = append(out, a) 314 } 315 return out 316 } 317 318 // ImportPathsNoDotExpansion returns the import paths to use for the given 319 // command line, but it does no ... expansion. 320 func (c *Context) ImportPathsNoDotExpansion(args []string) []string { 321 if len(args) == 0 { 322 return []string{"."} 323 } 324 var out []string 325 for _, a := range args { 326 // Arguments are supposed to be import paths, but 327 // as a courtesy to Windows developers, rewrite \ to / 328 // in command-line arguments. Handles .\... and so on. 329 if filepath.Separator == '\\' { 330 a = strings.Replace(a, `\`, `/`, -1) 331 } 332 333 // Put argument in canonical form, but preserve leading ./. 334 if strings.HasPrefix(a, "./") { 335 a = "./" + path.Clean(a) 336 if a == "./." { 337 a = "." 338 } 339 } else { 340 a = path.Clean(a) 341 } 342 if IsMetaPackage(a) { 343 out = append(out, c.allPackages(a)...) 344 continue 345 } 346 out = append(out, a) 347 } 348 return out 349 } 350 351 // IsMetaPackage checks if name is a reserved package name that expands to multiple packages. 352 func IsMetaPackage(name string) bool { 353 return name == "std" || name == "cmd" || name == "all" 354 }