github.com/tools/godep@v0.0.0-20180126220526-ce0bfadeb516/list.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "go/build" 7 "go/parser" 8 "go/token" 9 "log" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strconv" 14 "strings" 15 "unicode" 16 17 pathpkg "path" 18 ) 19 20 var ( 21 gorootSrc = filepath.Join(build.Default.GOROOT, "src") 22 ignoreTags = []string{"appengine", "ignore"} //TODO: appengine is a special case for now: https://github.com/tools/godep/issues/353 23 versionMatch = regexp.MustCompile(`\Ago\d+\.\d+\z`) 24 versionNegativeMatch = regexp.MustCompile(`\A\!go\d+\.\d+\z`) 25 ) 26 27 type errorMissingDep struct { 28 i, dir string // import, dir 29 } 30 31 func (e errorMissingDep) Error() string { 32 return "Unable to find dependent package " + e.i + " in context of " + e.dir 33 } 34 35 // packageContext is used to track an import and which package imported it. 36 type packageContext struct { 37 pkg *build.Package // package that imports the import 38 imp string // import 39 } 40 41 // depScanner tracks the processed and to be processed packageContexts 42 type depScanner struct { 43 processed []packageContext 44 todo []packageContext 45 } 46 47 // Next package and import to process 48 func (ds *depScanner) Next() (*build.Package, string) { 49 c := ds.todo[0] 50 ds.processed = append(ds.processed, c) 51 ds.todo = ds.todo[1:] 52 return c.pkg, c.imp 53 } 54 55 // Continue looping? 56 func (ds *depScanner) Continue() bool { 57 return len(ds.todo) > 0 58 } 59 60 // Add a package and imports to the depScanner. Skips already processed/pending package/import combos 61 func (ds *depScanner) Add(pkg *build.Package, imports ...string) { 62 NextImport: 63 for _, i := range imports { 64 if i == "C" { 65 i = "runtime/cgo" 66 } 67 for _, epc := range ds.processed { 68 if pkg.Dir == epc.pkg.Dir && i == epc.imp { 69 debugln("ctxts epc.pkg.Dir == pkg.Dir && i == epc.imp, skipping", epc.pkg.Dir, i) 70 continue NextImport 71 } 72 } 73 for _, epc := range ds.todo { 74 if pkg.Dir == epc.pkg.Dir && i == epc.imp { 75 debugln("ctxts epc.pkg.Dir == pkg.Dir && i == epc.imp, skipping", epc.pkg.Dir, i) 76 continue NextImport 77 } 78 } 79 pc := packageContext{pkg, i} 80 debugln("Adding pc:", pc.pkg.Dir, pc.imp) 81 ds.todo = append(ds.todo, pc) 82 } 83 } 84 85 var ( 86 pkgCache = make(map[string]*build.Package) // dir => *build.Package 87 ) 88 89 // returns the package in dir either from a cache or by importing it and then caching it 90 func fullPackageInDir(dir string) (*build.Package, error) { 91 var err error 92 pkg, ok := pkgCache[dir] 93 if !ok { 94 pkg, _ = build.ImportDir(dir, build.FindOnly) 95 if pkg.Goroot { 96 pkg, err = build.ImportDir(pkg.Dir, 0) 97 } else { 98 err = fillPackage(pkg) 99 } 100 if err == nil { 101 pkgCache[dir] = pkg 102 } 103 } 104 return pkg, err 105 } 106 107 // listPackage specified by path 108 func listPackage(path string) (*Package, error) { 109 debugln("listPackage", path) 110 var lp *build.Package 111 dir, err := findDirForPath(path, nil) 112 if err != nil { 113 return nil, err 114 } 115 lp, err = fullPackageInDir(dir) 116 p := &Package{ 117 Dir: lp.Dir, 118 Root: lp.Root, 119 ImportPath: lp.ImportPath, 120 XTestImports: lp.XTestImports, 121 TestImports: lp.TestImports, 122 GoFiles: lp.GoFiles, 123 CgoFiles: lp.CgoFiles, 124 TestGoFiles: lp.TestGoFiles, 125 XTestGoFiles: lp.XTestGoFiles, 126 IgnoredGoFiles: lp.IgnoredGoFiles, 127 } 128 p.Standard = lp.Goroot && lp.ImportPath != "" && !strings.Contains(lp.ImportPath, ".") 129 if err != nil || p.Standard { 130 return p, err 131 } 132 debugln("Looking For Package:", path, "in", dir) 133 ppln(lp) 134 135 ds := depScanner{} 136 ds.Add(lp, lp.Imports...) 137 for ds.Continue() { 138 ip, i := ds.Next() 139 140 debugf("Processing import %s for %s\n", i, ip.Dir) 141 pdir, err := findDirForPath(i, ip) 142 if err != nil { 143 return nil, err 144 } 145 dp, err := fullPackageInDir(pdir) 146 if err != nil { // This really should happen in this context though 147 ppln(err) 148 return nil, errorMissingDep{i: i, dir: ip.Dir} 149 } 150 ppln(dp) 151 if !dp.Goroot { 152 // Don't bother adding packages in GOROOT to the dependency scanner, they don't import things from outside of it. 153 ds.Add(dp, dp.Imports...) 154 } 155 debugln("lp:") 156 ppln(lp) 157 debugln("ip:") 158 ppln(ip) 159 if lp == ip { 160 debugln("lp == ip") 161 p.Imports = append(p.Imports, dp.ImportPath) 162 } 163 p.Deps = append(p.Deps, dp.ImportPath) 164 p.Dependencies = addDependency(p.Dependencies, dp) 165 } 166 p.Imports = uniq(p.Imports) 167 p.Deps = uniq(p.Deps) 168 debugln("Done Looking For Package:", path, "in", dir) 169 ppln(p) 170 return p, nil 171 } 172 173 func addDependency(deps []build.Package, d *build.Package) []build.Package { 174 for i := range deps { 175 if deps[i].Dir == d.Dir { 176 return deps 177 } 178 } 179 return append(deps, *d) 180 } 181 182 // finds the directory for the given import path in the context of the provided build.Package (if provided) 183 func findDirForPath(path string, ip *build.Package) (string, error) { 184 debugln("findDirForPath", path, ip) 185 var search []string 186 187 if build.IsLocalImport(path) { 188 dir := path 189 if !filepath.IsAbs(dir) { 190 if abs, err := filepath.Abs(dir); err == nil { 191 // interpret relative to current directory 192 dir = abs 193 } 194 } 195 return dir, nil 196 } 197 198 // We need to check to see if the import exists in vendor/ folders up the hierarchy of the importing package 199 if VendorExperiment && ip != nil { 200 debugln("resolving vendor posibilities:", ip.Dir, ip.Root) 201 cr := cleanPath(ip.Root) 202 203 for base := cleanPath(ip.Dir); !pathEqual(base, cr); base = cleanPath(filepath.Dir(base)) { 204 s := filepath.Join(base, "vendor", path) 205 debugln("Adding search dir:", s) 206 search = append(search, s) 207 } 208 } 209 210 for _, base := range build.Default.SrcDirs() { 211 search = append(search, filepath.Join(base, path)) 212 } 213 214 for _, dir := range search { 215 debugln("searching", dir) 216 fi, err := stat(dir) 217 if err == nil && fi.IsDir() { 218 return dir, nil 219 } 220 } 221 222 return "", errPackageNotFound{path} 223 } 224 225 type statEntry struct { 226 fi os.FileInfo 227 err error 228 } 229 230 var ( 231 statCache = make(map[string]statEntry) 232 ) 233 234 func clearStatCache() { 235 statCache = make(map[string]statEntry) 236 } 237 238 func stat(p string) (os.FileInfo, error) { 239 if e, ok := statCache[p]; ok { 240 return e.fi, e.err 241 } 242 fi, err := os.Stat(p) 243 statCache[p] = statEntry{fi, err} 244 return fi, err 245 } 246 247 // fillPackage full of info. Assumes p.Dir is set at a minimum 248 func fillPackage(p *build.Package) error { 249 if p.Goroot { 250 return nil 251 } 252 253 if p.SrcRoot == "" { 254 for _, base := range build.Default.SrcDirs() { 255 if strings.HasPrefix(p.Dir, base) { 256 p.SrcRoot = base 257 } 258 } 259 } 260 261 if p.SrcRoot == "" { 262 return errors.New("Unable to find SrcRoot for package " + p.ImportPath) 263 } 264 265 if p.Root == "" { 266 p.Root = filepath.Dir(p.SrcRoot) 267 } 268 269 var buildMatch = "+build " 270 var buildFieldSplit = func(r rune) bool { 271 return unicode.IsSpace(r) || r == ',' 272 } 273 274 debugln("Filling package:", p.ImportPath, "from", p.Dir) 275 gofiles, err := filepath.Glob(filepath.Join(p.Dir, "*.go")) 276 if err != nil { 277 debugln("Error globbing", err) 278 return err 279 } 280 281 if len(gofiles) == 0 { 282 return &build.NoGoError{Dir: p.Dir} 283 } 284 285 var testImports []string 286 var imports []string 287 NextFile: 288 for _, file := range gofiles { 289 debugln(file) 290 pf, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ImportsOnly|parser.ParseComments) 291 if err != nil { 292 return err 293 } 294 testFile := strings.HasSuffix(file, "_test.go") 295 fname := filepath.Base(file) 296 for _, c := range pf.Comments { 297 ct := c.Text() 298 if i := strings.Index(ct, buildMatch); i != -1 { 299 for _, t := range strings.FieldsFunc(ct[i+len(buildMatch):], buildFieldSplit) { 300 for _, tag := range ignoreTags { 301 if t == tag { 302 p.IgnoredGoFiles = append(p.IgnoredGoFiles, fname) 303 continue NextFile 304 } 305 } 306 307 if versionMatch.MatchString(t) && !isSameOrNewer(t, majorGoVersion) { 308 debugln("Adding", fname, "to ignored list because of version tag", t) 309 p.IgnoredGoFiles = append(p.IgnoredGoFiles, fname) 310 continue NextFile 311 } 312 if versionNegativeMatch.MatchString(t) && isSameOrNewer(t[1:], majorGoVersion) { 313 debugln("Adding", fname, "to ignored list because of version tag", t) 314 p.IgnoredGoFiles = append(p.IgnoredGoFiles, fname) 315 continue NextFile 316 } 317 } 318 } 319 } 320 if testFile { 321 p.TestGoFiles = append(p.TestGoFiles, fname) 322 } else { 323 p.GoFiles = append(p.GoFiles, fname) 324 } 325 for _, is := range pf.Imports { 326 name, err := strconv.Unquote(is.Path.Value) 327 if err != nil { 328 return err // can't happen? 329 } 330 if testFile { 331 testImports = append(testImports, name) 332 } else { 333 imports = append(imports, name) 334 } 335 } 336 } 337 imports = uniq(imports) 338 testImports = uniq(testImports) 339 p.Imports = imports 340 p.TestImports = testImports 341 return nil 342 } 343 344 // All of the following functions were vendored from go proper. Locations are noted in comments, but may change in future Go versions. 345 346 // importPaths returns the import paths to use for the given command line. 347 // $GOROOT/src/cmd/main.go:366 348 func importPaths(args []string) []string { 349 debugf("importPathsNoDotExpansion(%q) == ", args) 350 args = importPathsNoDotExpansion(args) 351 debugf("%q\n", args) 352 var out []string 353 for _, a := range args { 354 if strings.Contains(a, "...") { 355 if build.IsLocalImport(a) { 356 debugf("build.IsLocalImport(%q) == true\n", a) 357 pkgs := allPackagesInFS(a) 358 debugf("allPackagesInFS(%q) == %q\n", a, pkgs) 359 out = append(out, pkgs...) 360 } else { 361 debugf("build.IsLocalImport(%q) == false\n", a) 362 pkgs := allPackages(a) 363 debugf("allPackages(%q) == %q\n", a, pkgs) 364 out = append(out, allPackages(a)...) 365 } 366 continue 367 } 368 out = append(out, a) 369 } 370 return out 371 } 372 373 // importPathsNoDotExpansion returns the import paths to use for the given 374 // command line, but it does no ... expansion. 375 // $GOROOT/src/cmd/main.go:332 376 func importPathsNoDotExpansion(args []string) []string { 377 if len(args) == 0 { 378 return []string{"."} 379 } 380 var out []string 381 for _, a := range args { 382 // Arguments are supposed to be import paths, but 383 // as a courtesy to Windows developers, rewrite \ to / 384 // in command-line arguments. Handles .\... and so on. 385 if filepath.Separator == '\\' { 386 a = strings.Replace(a, `\`, `/`, -1) 387 } 388 389 // Put argument in canonical form, but preserve leading ./. 390 if strings.HasPrefix(a, "./") { 391 a = "./" + pathpkg.Clean(a) 392 if a == "./." { 393 a = "." 394 } 395 } else { 396 a = pathpkg.Clean(a) 397 } 398 if a == "all" || a == "std" || a == "cmd" { 399 out = append(out, allPackages(a)...) 400 continue 401 } 402 out = append(out, a) 403 } 404 return out 405 } 406 407 // allPackagesInFS is like allPackages but is passed a pattern 408 // beginning ./ or ../, meaning it should scan the tree rooted 409 // at the given directory. There are ... in the pattern too. 410 // $GOROOT/src/cmd/main.go:620 411 func allPackagesInFS(pattern string) []string { 412 pkgs := matchPackagesInFS(pattern) 413 if len(pkgs) == 0 { 414 fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) 415 } 416 return pkgs 417 } 418 419 // allPackages returns all the packages that can be found 420 // under the $GOPATH directories and $GOROOT matching pattern. 421 // The pattern is either "all" (all packages), "std" (standard packages), 422 // "cmd" (standard commands), or a path including "...". 423 // $GOROOT/src/cmd/main.go:542 424 func allPackages(pattern string) []string { 425 pkgs := matchPackages(pattern) 426 if len(pkgs) == 0 { 427 fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) 428 } 429 return pkgs 430 } 431 432 // $GOROOT/src/cmd/main.go:554 433 // This has been changed to not use build.ImportDir 434 func matchPackages(pattern string) []string { 435 match := func(string) bool { return true } 436 treeCanMatch := func(string) bool { return true } 437 if pattern != "all" && pattern != "std" && pattern != "cmd" { 438 match = matchPattern(pattern) 439 treeCanMatch = treeCanMatchPattern(pattern) 440 } 441 442 have := map[string]bool{ 443 "builtin": true, // ignore pseudo-package that exists only for documentation 444 } 445 if !build.Default.CgoEnabled { 446 have["runtime/cgo"] = true // ignore during walk 447 } 448 var pkgs []string 449 450 for _, src := range build.Default.SrcDirs() { 451 if (pattern == "std" || pattern == "cmd") && src != gorootSrc { 452 continue 453 } 454 src = filepath.Clean(src) + string(filepath.Separator) 455 root := src 456 if pattern == "cmd" { 457 root += "cmd" + string(filepath.Separator) 458 } 459 filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { 460 if err != nil || !fi.IsDir() || path == src { 461 return nil 462 } 463 464 // Avoid .foo, _foo, and testdata directory trees. 465 _, elem := filepath.Split(path) 466 if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { 467 return filepath.SkipDir 468 } 469 470 name := filepath.ToSlash(path[len(src):]) 471 if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") { 472 // The name "std" is only the standard library. 473 // If the name has a dot, assume it's a domain name for go get, 474 // and if the name is cmd, it's the root of the command tree. 475 return filepath.SkipDir 476 } 477 if !treeCanMatch(name) { 478 return filepath.SkipDir 479 } 480 if have[name] { 481 return nil 482 } 483 have[name] = true 484 if !match(name) { 485 return nil 486 } 487 488 ap, err := filepath.Abs(path) 489 if err != nil { 490 return nil 491 } 492 if _, err = fullPackageInDir(ap); err != nil { 493 debugf("matchPackage(%q) ap=%q Error: %q\n", ap, pattern, err) 494 if _, noGo := err.(*build.NoGoError); noGo { 495 return nil 496 } 497 } 498 pkgs = append(pkgs, name) 499 return nil 500 }) 501 } 502 return pkgs 503 } 504 505 // treeCanMatchPattern(pattern)(name) reports whether 506 // name or children of name can possibly match pattern. 507 // Pattern is the same limited glob accepted by matchPattern. 508 // $GOROOT/src/cmd/main.go:527 509 func treeCanMatchPattern(pattern string) func(name string) bool { 510 wildCard := false 511 if i := strings.Index(pattern, "..."); i >= 0 { 512 wildCard = true 513 pattern = pattern[:i] 514 } 515 return func(name string) bool { 516 return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || 517 wildCard && strings.HasPrefix(name, pattern) 518 } 519 } 520 521 // hasPathPrefix reports whether the path s begins with the 522 // elements in prefix. 523 // $GOROOT/src/cmd/main.go:489 524 func hasPathPrefix(s, prefix string) bool { 525 switch { 526 default: 527 return false 528 case len(s) == len(prefix): 529 return s == prefix 530 case len(s) > len(prefix): 531 if prefix != "" && prefix[len(prefix)-1] == '/' { 532 return strings.HasPrefix(s, prefix) 533 } 534 return s[len(prefix)] == '/' && s[:len(prefix)] == prefix 535 } 536 } 537 538 // $GOROOT/src/cmd/go/main.go:631 539 // This has been changed to not use build.ImportDir 540 func matchPackagesInFS(pattern string) []string { 541 // Find directory to begin the scan. 542 // Could be smarter but this one optimization 543 // is enough for now, since ... is usually at the 544 // end of a path. 545 i := strings.Index(pattern, "...") 546 dir, _ := pathpkg.Split(pattern[:i]) 547 548 // pattern begins with ./ or ../. 549 // path.Clean will discard the ./ but not the ../. 550 // We need to preserve the ./ for pattern matching 551 // and in the returned import paths. 552 prefix := "" 553 if strings.HasPrefix(pattern, "./") { 554 prefix = "./" 555 } 556 match := matchPattern(pattern) 557 558 var pkgs []string 559 filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { 560 if err != nil || !fi.IsDir() { 561 return nil 562 } 563 if path == dir { 564 // filepath.Walk starts at dir and recurses. For the recursive case, 565 // the path is the result of filepath.Join, which calls filepath.Clean. 566 // The initial case is not Cleaned, though, so we do this explicitly. 567 // 568 // This converts a path like "./io/" to "io". Without this step, running 569 // "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io 570 // package, because prepending the prefix "./" to the unclean path would 571 // result in "././io", and match("././io") returns false. 572 path = filepath.Clean(path) 573 } 574 575 // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". 576 _, elem := filepath.Split(path) 577 dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." 578 if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { 579 return filepath.SkipDir 580 } 581 582 name := prefix + filepath.ToSlash(path) 583 if !match(name) { 584 return nil 585 } 586 ap, err := filepath.Abs(path) 587 if err != nil { 588 return nil 589 } 590 if _, err = fullPackageInDir(ap); err != nil { 591 debugf("matchPackageInFS(%q) ap=%q Error: %q\n", ap, pattern, err) 592 if _, noGo := err.(*build.NoGoError); !noGo { 593 log.Print(err) 594 } 595 return nil 596 } 597 pkgs = append(pkgs, name) 598 return nil 599 }) 600 return pkgs 601 }