github.com/sdboyer/gps@v0.16.3/pkgtree/pkgtree.go (about) 1 package pkgtree 2 3 import ( 4 "fmt" 5 "go/build" 6 "go/parser" 7 gscan "go/scanner" 8 "go/token" 9 "os" 10 "path/filepath" 11 "sort" 12 "strconv" 13 "strings" 14 "unicode" 15 ) 16 17 // Package represents a Go package. It contains a subset of the information 18 // go/build.Package does. 19 type Package struct { 20 Name string // Package name, as declared in the package statement 21 ImportPath string // Full import path, including the prefix provided to ListPackages() 22 CommentPath string // Import path given in the comment on the package statement 23 Imports []string // Imports from all go and cgo files 24 TestImports []string // Imports from all go test files (in go/build parlance: both TestImports and XTestImports) 25 } 26 27 // ListPackages reports Go package information about all directories in the tree 28 // at or below the provided fileRoot. 29 // 30 // The importRoot parameter is prepended to the relative path when determining 31 // the import path for each package. The obvious case is for something typical, 32 // like: 33 // 34 // fileRoot = "/home/user/go/src/github.com/foo/bar" 35 // importRoot = "github.com/foo/bar" 36 // 37 // where the fileRoot and importRoot align. However, if you provide: 38 // 39 // fileRoot = "/home/user/workspace/path/to/repo" 40 // importRoot = "github.com/foo/bar" 41 // 42 // then the root package at path/to/repo will be ascribed import path 43 // "github.com/foo/bar", and the package at 44 // "/home/user/workspace/path/to/repo/baz" will be "github.com/foo/bar/baz". 45 // 46 // A PackageTree is returned, which contains the ImportRoot and map of import path 47 // to PackageOrErr - each path under the root that exists will have either a 48 // Package, or an error describing why the directory is not a valid package. 49 func ListPackages(fileRoot, importRoot string) (PackageTree, error) { 50 ptree := PackageTree{ 51 ImportRoot: importRoot, 52 Packages: make(map[string]PackageOrErr), 53 } 54 55 var err error 56 fileRoot, err = filepath.Abs(fileRoot) 57 if err != nil { 58 return PackageTree{}, err 59 } 60 61 err = filepath.Walk(fileRoot, func(wp string, fi os.FileInfo, err error) error { 62 if err != nil && err != filepath.SkipDir { 63 return err 64 } 65 if !fi.IsDir() { 66 return nil 67 } 68 69 // Skip dirs that are known to hold non-local/dependency code. 70 // 71 // We don't skip _*, or testdata dirs because, while it may be poor 72 // form, importing them is not a compilation error. 73 switch fi.Name() { 74 case "vendor", "Godeps": 75 return filepath.SkipDir 76 } 77 // We do skip dot-dirs, though, because it's such a ubiquitous standard 78 // that they not be visited by normal commands, and because things get 79 // really weird if we don't. 80 if strings.HasPrefix(fi.Name(), ".") { 81 return filepath.SkipDir 82 } 83 84 // The entry error is nil when visiting a directory that itself is 85 // untraversable, as it's still governed by the parent directory's 86 // perms. We have to check readability of the dir here, because 87 // otherwise we'll have an empty package entry when we fail to read any 88 // of the dir's contents. 89 // 90 // If we didn't check here, then the next time this closure is called it 91 // would have an err with the same path as is called this time, as only 92 // then will filepath.Walk have attempted to descend into the directory 93 // and encountered an error. 94 var f *os.File 95 f, err = os.Open(wp) 96 if err != nil { 97 if os.IsPermission(err) { 98 return filepath.SkipDir 99 } 100 return err 101 } 102 f.Close() 103 104 // Compute the import path. Run the result through ToSlash(), so that 105 // windows file paths are normalized to slashes, as is expected of 106 // import paths. 107 ip := filepath.ToSlash(filepath.Join(importRoot, strings.TrimPrefix(wp, fileRoot))) 108 109 // Find all the imports, across all os/arch combos 110 //p, err := fullPackageInDir(wp) 111 p := &build.Package{ 112 Dir: wp, 113 } 114 err = fillPackage(p) 115 116 var pkg Package 117 if err == nil { 118 pkg = Package{ 119 ImportPath: ip, 120 CommentPath: p.ImportComment, 121 Name: p.Name, 122 Imports: p.Imports, 123 TestImports: dedupeStrings(p.TestImports, p.XTestImports), 124 } 125 } else { 126 switch err.(type) { 127 case gscan.ErrorList, *gscan.Error, *build.NoGoError: 128 // This happens if we encounter malformed or nonexistent Go 129 // source code 130 ptree.Packages[ip] = PackageOrErr{ 131 Err: err, 132 } 133 return nil 134 default: 135 return err 136 } 137 } 138 139 // This area has some...fuzzy rules, but check all the imports for 140 // local/relative/dot-ness, and record an error for the package if we 141 // see any. 142 var lim []string 143 for _, imp := range append(pkg.Imports, pkg.TestImports...) { 144 switch { 145 // Do allow the single-dot, at least for now 146 case imp == "..": 147 lim = append(lim, imp) 148 case strings.HasPrefix(imp, "./"): 149 lim = append(lim, imp) 150 case strings.HasPrefix(imp, "../"): 151 lim = append(lim, imp) 152 } 153 } 154 155 if len(lim) > 0 { 156 ptree.Packages[ip] = PackageOrErr{ 157 Err: &LocalImportsError{ 158 Dir: wp, 159 ImportPath: ip, 160 LocalImports: lim, 161 }, 162 } 163 } else { 164 ptree.Packages[ip] = PackageOrErr{ 165 P: pkg, 166 } 167 } 168 169 return nil 170 }) 171 172 if err != nil { 173 return PackageTree{}, err 174 } 175 176 return ptree, nil 177 } 178 179 // fillPackage full of info. Assumes p.Dir is set at a minimum 180 func fillPackage(p *build.Package) error { 181 var buildPrefix = "// +build " 182 var buildFieldSplit = func(r rune) bool { 183 return unicode.IsSpace(r) || r == ',' 184 } 185 186 gofiles, err := filepath.Glob(filepath.Join(p.Dir, "*.go")) 187 if err != nil { 188 return err 189 } 190 191 if len(gofiles) == 0 { 192 return &build.NoGoError{Dir: p.Dir} 193 } 194 195 var testImports []string 196 var imports []string 197 for _, file := range gofiles { 198 // Skip underscore-led files, in keeping with the rest of the toolchain. 199 if filepath.Base(file)[0] == '_' { 200 continue 201 } 202 pf, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ImportsOnly|parser.ParseComments) 203 if err != nil { 204 if os.IsPermission(err) { 205 continue 206 } 207 return err 208 } 209 testFile := strings.HasSuffix(file, "_test.go") 210 fname := filepath.Base(file) 211 212 var ignored bool 213 for _, c := range pf.Comments { 214 if c.Pos() > pf.Package { // +build comment must come before package 215 continue 216 } 217 218 var ct string 219 for _, cl := range c.List { 220 if strings.HasPrefix(cl.Text, buildPrefix) { 221 ct = cl.Text 222 break 223 } 224 } 225 if ct == "" { 226 continue 227 } 228 229 for _, t := range strings.FieldsFunc(ct[len(buildPrefix):], buildFieldSplit) { 230 // hardcoded (for now) handling for the "ignore" build tag 231 // We "soft" ignore the files tagged with ignore so that we pull in their imports. 232 if t == "ignore" { 233 ignored = true 234 } 235 } 236 } 237 238 if testFile { 239 p.TestGoFiles = append(p.TestGoFiles, fname) 240 if p.Name == "" && !ignored { 241 p.Name = strings.TrimSuffix(pf.Name.Name, "_test") 242 } 243 } else { 244 if p.Name == "" && !ignored { 245 p.Name = pf.Name.Name 246 } 247 p.GoFiles = append(p.GoFiles, fname) 248 } 249 250 for _, is := range pf.Imports { 251 name, err := strconv.Unquote(is.Path.Value) 252 if err != nil { 253 return err // can't happen? 254 } 255 if testFile { 256 testImports = append(testImports, name) 257 } else { 258 imports = append(imports, name) 259 } 260 } 261 } 262 263 imports = uniq(imports) 264 testImports = uniq(testImports) 265 p.Imports = imports 266 p.TestImports = testImports 267 return nil 268 } 269 270 // LocalImportsError indicates that a package contains at least one relative 271 // import that will prevent it from compiling. 272 // 273 // TODO(sdboyer) add a Files property once we're doing our own per-file parsing 274 type LocalImportsError struct { 275 ImportPath string 276 Dir string 277 LocalImports []string 278 } 279 280 func (e *LocalImportsError) Error() string { 281 switch len(e.LocalImports) { 282 case 0: 283 // shouldn't be possible, but just cover the case 284 return fmt.Sprintf("import path %s had bad local imports", e.ImportPath) 285 case 1: 286 return fmt.Sprintf("import path %s had a local import: %q", e.ImportPath, e.LocalImports[0]) 287 default: 288 return fmt.Sprintf("import path %s had local imports: %q", e.ImportPath, strings.Join(e.LocalImports, "\", \"")) 289 } 290 } 291 292 type wm struct { 293 err error 294 ex map[string]bool 295 in map[string]bool 296 } 297 298 // PackageOrErr stores the results of attempting to parse a single directory for 299 // Go source code. 300 type PackageOrErr struct { 301 P Package 302 Err error 303 } 304 305 // ProblemImportError describes the reason that a particular import path is 306 // not safely importable. 307 type ProblemImportError struct { 308 // The import path of the package with some problem rendering it 309 // unimportable. 310 ImportPath string 311 // The path to the internal package the problem package imports that is the 312 // original cause of this issue. If empty, the package itself is the 313 // problem. 314 Cause []string 315 // The actual error from ListPackages that is undermining importability for 316 // this package. 317 Err error 318 } 319 320 // Error formats the ProblemImportError as a string, reflecting whether the 321 // error represents a direct or transitive problem. 322 func (e *ProblemImportError) Error() string { 323 switch len(e.Cause) { 324 case 0: 325 return fmt.Sprintf("%q contains malformed code: %s", e.ImportPath, e.Err.Error()) 326 case 1: 327 return fmt.Sprintf("%q imports %q, which contains malformed code: %s", e.ImportPath, e.Cause[0], e.Err.Error()) 328 default: 329 return fmt.Sprintf("%q transitively (through %v packages) imports %q, which contains malformed code: %s", e.ImportPath, len(e.Cause)-1, e.Cause[len(e.Cause)-1], e.Err.Error()) 330 } 331 } 332 333 // Helper func to create an error when a package is missing. 334 func missingPkgErr(pkg string) error { 335 return fmt.Errorf("no package exists at %q", pkg) 336 } 337 338 // A PackageTree represents the results of recursively parsing a tree of 339 // packages, starting at the ImportRoot. The results of parsing the files in the 340 // directory identified by each import path - a Package or an error - are stored 341 // in the Packages map, keyed by that import path. 342 type PackageTree struct { 343 ImportRoot string 344 Packages map[string]PackageOrErr 345 } 346 347 // ToReachMap looks through a PackageTree and computes the list of external 348 // import statements (that is, import statements pointing to packages that are 349 // not logical children of PackageTree.ImportRoot) that are transitively 350 // imported by the internal packages in the tree. 351 // 352 // main indicates whether (true) or not (false) to include main packages in the 353 // analysis. When utilized by gps' solver, main packages are generally excluded 354 // from analyzing anything other than the root project, as they necessarily can't 355 // be imported. 356 // 357 // tests indicates whether (true) or not (false) to include imports from test 358 // files in packages when computing the reach map. 359 // 360 // backprop indicates whether errors (an actual PackageOrErr.Err, or an import 361 // to a nonexistent internal package) should be backpropagated, transitively 362 // "poisoning" all corresponding importers to all importers. 363 // 364 // ignore is a map of import paths that, if encountered, should be excluded from 365 // analysis. This exclusion applies to both internal and external packages. If 366 // an external import path is ignored, it is simply omitted from the results. 367 // 368 // If an internal path is ignored, then it not only does not appear in the final 369 // map, but it is also excluded from the transitive calculations of other 370 // internal packages. That is, if you ignore A/foo, then the external package 371 // list for all internal packages that import A/foo will not include external 372 // packages that are only reachable through A/foo. 373 // 374 // Visually, this means that, given a PackageTree with root A and packages at A, 375 // A/foo, and A/bar, and the following import chain: 376 // 377 // A -> A/foo -> A/bar -> B/baz 378 // 379 // In this configuration, all of A's packages transitively import B/baz, so the 380 // returned map would be: 381 // 382 // map[string][]string{ 383 // "A": []string{"B/baz"}, 384 // "A/foo": []string{"B/baz"} 385 // "A/bar": []string{"B/baz"}, 386 // } 387 // 388 // However, if you ignore A/foo, then A's path to B/baz is broken, and A/foo is 389 // omitted entirely. Thus, the returned map would be: 390 // 391 // map[string][]string{ 392 // "A": []string{}, 393 // "A/bar": []string{"B/baz"}, 394 // } 395 // 396 // If there are no packages to ignore, it is safe to pass a nil map. 397 // 398 // Finally, if an internal PackageOrErr contains an error, it is always omitted 399 // from the result set. If backprop is true, then the error from that internal 400 // package will be transitively propagated back to any other internal 401 // PackageOrErrs that import it, causing them to also be omitted. So, with the 402 // same import chain: 403 // 404 // A -> A/foo -> A/bar -> B/baz 405 // 406 // If A/foo has an error, then it would backpropagate to A, causing both to be 407 // omitted, and the returned map to contain only A/bar: 408 // 409 // map[string][]string{ 410 // "A/bar": []string{"B/baz"}, 411 // } 412 // 413 // If backprop is false, then errors will not backpropagate to internal 414 // importers. So, with an error in A/foo, this would be the result map: 415 // 416 // map[string][]string{ 417 // "A": []string{}, 418 // "A/bar": []string{"B/baz"}, 419 // } 420 func (t PackageTree) ToReachMap(main, tests, backprop bool, ignore map[string]bool) (ReachMap, map[string]*ProblemImportError) { 421 if ignore == nil { 422 ignore = make(map[string]bool) 423 } 424 425 // world's simplest adjacency list 426 workmap := make(map[string]wm) 427 428 var imps []string 429 for ip, perr := range t.Packages { 430 if perr.Err != nil { 431 workmap[ip] = wm{ 432 err: perr.Err, 433 } 434 continue 435 } 436 p := perr.P 437 438 // Skip main packages, unless param says otherwise 439 if p.Name == "main" && !main { 440 continue 441 } 442 // Skip ignored packages 443 if ignore[ip] { 444 continue 445 } 446 447 imps = imps[:0] 448 if tests { 449 imps = dedupeStrings(p.Imports, p.TestImports) 450 } else { 451 imps = p.Imports 452 } 453 454 w := wm{ 455 ex: make(map[string]bool), 456 in: make(map[string]bool), 457 } 458 459 // For each import, decide whether it should be ignored, or if it 460 // belongs in the external or internal imports list. 461 for _, imp := range imps { 462 if ignore[imp] { 463 continue 464 } 465 466 if !eqOrSlashedPrefix(imp, t.ImportRoot) { 467 w.ex[imp] = true 468 } else { 469 w.in[imp] = true 470 } 471 } 472 473 workmap[ip] = w 474 } 475 476 return wmToReach(workmap, backprop) 477 } 478 479 // Copy copies the PackageTree. 480 // 481 // This is really only useful as a defensive measure to prevent external state 482 // mutations. 483 func (t PackageTree) Copy() PackageTree { 484 t2 := PackageTree{ 485 ImportRoot: t.ImportRoot, 486 Packages: map[string]PackageOrErr{}, 487 } 488 489 for path, poe := range t.Packages { 490 poe2 := PackageOrErr{ 491 Err: poe.Err, 492 P: poe.P, 493 } 494 if len(poe.P.Imports) > 0 { 495 poe2.P.Imports = make([]string, len(poe.P.Imports)) 496 copy(poe2.P.Imports, poe.P.Imports) 497 } 498 if len(poe.P.TestImports) > 0 { 499 poe2.P.TestImports = make([]string, len(poe.P.TestImports)) 500 copy(poe2.P.TestImports, poe.P.TestImports) 501 } 502 503 t2.Packages[path] = poe2 504 } 505 506 return t2 507 } 508 509 // wmToReach takes an internal "workmap" constructed by 510 // PackageTree.ExternalReach(), transitively walks (via depth-first traversal) 511 // all internal imports until they reach an external path or terminate, then 512 // translates the results into a slice of external imports for each internal 513 // pkg. 514 // 515 // It drops any packages with errors, and - if backprop is true - backpropagates 516 // those errors, causing internal packages that (transitively) import other 517 // internal packages having errors to also be dropped. 518 func wmToReach(workmap map[string]wm, backprop bool) (ReachMap, map[string]*ProblemImportError) { 519 // Uses depth-first exploration to compute reachability into external 520 // packages, dropping any internal packages on "poisoned paths" - a path 521 // containing a package with an error, or with a dep on an internal package 522 // that's missing. 523 524 const ( 525 white uint8 = iota 526 grey 527 black 528 ) 529 530 colors := make(map[string]uint8) 531 exrsets := make(map[string]map[string]struct{}) 532 inrsets := make(map[string]map[string]struct{}) 533 errmap := make(map[string]*ProblemImportError) 534 535 // poison is a helper func to eliminate specific reachsets from exrsets and 536 // inrsets, and populate error information along the way. 537 poison := func(path []string, err *ProblemImportError) { 538 for k, ppkg := range path { 539 delete(exrsets, ppkg) 540 delete(inrsets, ppkg) 541 542 // Duplicate the err for this package 543 kerr := &ProblemImportError{ 544 ImportPath: ppkg, 545 Err: err.Err, 546 } 547 548 // Shift the slice bounds on the incoming err.Cause. 549 // 550 // This check will only be false on the final path element when 551 // entering via poisonWhite, where the last pkg is the underlying 552 // cause of the problem, and is thus expected to have an empty Cause 553 // slice. 554 if k+1 < len(err.Cause) { 555 // reuse the slice 556 kerr.Cause = err.Cause[k+1:] 557 } 558 559 // Both black and white cases can have the final element be a 560 // package that doesn't exist. If that's the case, don't write it 561 // directly to the errmap, as presence in the errmap indicates the 562 // package was present in the input PackageTree. 563 if k == len(path)-1 { 564 if _, exists := workmap[path[len(path)-1]]; !exists { 565 continue 566 } 567 } 568 569 // Direct writing to the errmap means that if multiple errors affect 570 // a given package, only the last error visited will be reported. 571 // But that should be sufficient; presumably, the user can 572 // iteratively resolve the errors. 573 errmap[ppkg] = kerr 574 } 575 } 576 577 // poisonWhite wraps poison for error recording in the white-poisoning case, 578 // where we're constructing a new poison path. 579 poisonWhite := func(path []string) { 580 err := &ProblemImportError{ 581 Cause: make([]string, len(path)), 582 } 583 copy(err.Cause, path) 584 585 // find the tail err 586 tail := path[len(path)-1] 587 if w, exists := workmap[tail]; exists { 588 // If we make it to here, the dfe guarantees that the workmap 589 // will contain an error for this pkg. 590 err.Err = w.err 591 } else { 592 err.Err = missingPkgErr(tail) 593 } 594 595 poison(path, err) 596 } 597 // poisonBlack wraps poison for error recording in the black-poisoning case, 598 // where we're connecting to an existing poison path. 599 poisonBlack := func(path []string, from string) { 600 // Because the outer dfe loop ensures we never directly re-visit a pkg 601 // that was already completed (black), we don't have to defend against 602 // an empty path here. 603 604 fromErr := errmap[from] 605 err := &ProblemImportError{ 606 Err: fromErr.Err, 607 Cause: make([]string, 0, len(path)+len(fromErr.Cause)+1), 608 } 609 err.Cause = append(err.Cause, path...) 610 err.Cause = append(err.Cause, from) 611 err.Cause = append(err.Cause, fromErr.Cause...) 612 613 poison(path, err) 614 } 615 616 var dfe func(string, []string) bool 617 618 // dfe is the depth-first-explorer that computes a safe, error-free external 619 // reach map. 620 // 621 // pkg is the import path of the pkg currently being visited; path is the 622 // stack of parent packages we've visited to get to pkg. The return value 623 // indicates whether the level completed successfully (true) or if it was 624 // poisoned (false). 625 dfe = func(pkg string, path []string) bool { 626 // white is the zero value of uint8, which is what we want if the pkg 627 // isn't in the colors map, so this works fine 628 switch colors[pkg] { 629 case white: 630 // first visit to this pkg; mark it as in-process (grey) 631 colors[pkg] = grey 632 633 // make sure it's present and w/out errs 634 w, exists := workmap[pkg] 635 636 // Push current visitee onto the path slice. Passing path through 637 // recursion levels as a value has the effect of auto-popping the 638 // slice, while also giving us safe memory reuse. 639 path = append(path, pkg) 640 641 if !exists || w.err != nil { 642 if backprop { 643 // Does not exist or has an err; poison self and all parents 644 poisonWhite(path) 645 } else if exists { 646 // Only record something in the errmap if there's actually a 647 // package there, per the semantics of the errmap 648 errmap[pkg] = &ProblemImportError{ 649 ImportPath: pkg, 650 Err: w.err, 651 } 652 } 653 654 // we know we're done here, so mark it black 655 colors[pkg] = black 656 return false 657 } 658 // pkg exists with no errs; start internal and external reachsets for it. 659 rs := make(map[string]struct{}) 660 irs := make(map[string]struct{}) 661 662 // Dump this package's external pkgs into its own reachset. Separate 663 // loop from the parent dump to avoid nested map loop lookups. 664 for ex := range w.ex { 665 rs[ex] = struct{}{} 666 } 667 exrsets[pkg] = rs 668 // Same deal for internal imports 669 for in := range w.in { 670 irs[in] = struct{}{} 671 } 672 inrsets[pkg] = irs 673 674 // Push this pkg's imports into all parent reachsets. Not all 675 // parents will necessarily have a reachset; none, some, or all 676 // could have been poisoned by a different path than what we're on 677 // right now. 678 for _, ppkg := range path { 679 if prs, exists := exrsets[ppkg]; exists { 680 for ex := range w.ex { 681 prs[ex] = struct{}{} 682 } 683 } 684 685 if prs, exists := inrsets[ppkg]; exists { 686 for in := range w.in { 687 prs[in] = struct{}{} 688 } 689 } 690 } 691 692 // Now, recurse until done, or a false bubbles up, indicating the 693 // path is poisoned. 694 for in := range w.in { 695 // It's possible, albeit weird, for a package to import itself. 696 // If we try to visit self, though, then it erroneously poisons 697 // the path, as it would be interpreted as grey. In practice, 698 // self-imports are a no-op, so we can just skip it. 699 if in == pkg { 700 continue 701 } 702 703 clean := dfe(in, path) 704 if !clean && backprop { 705 // Path is poisoned. If we're backpropagating errors, then 706 // the reachmap for the visitee was already deleted by the 707 // path we're returning from; mark the visitee black, then 708 // return false to bubble up the poison. This is OK to do 709 // early, before exploring all internal imports, because the 710 // outer loop visits all internal packages anyway. 711 // 712 // In fact, stopping early is preferable - white subpackages 713 // won't have to iterate pointlessly through a parent path 714 // with no reachset. 715 colors[pkg] = black 716 return false 717 } 718 } 719 720 // Fully done with this pkg; no transitive problems. 721 colors[pkg] = black 722 return true 723 724 case grey: 725 // Import cycles can arise in healthy situations through xtests, so 726 // allow them for now. 727 // 728 // FIXME(sdboyer) we need an improved model that allows us to 729 // accurately reject real import cycles. 730 return true 731 // grey means an import cycle; guaranteed badness right here. You'd 732 // hope we never encounter it in a dependency (really? you published 733 // that code?), but we have to defend against it. 734 //colors[pkg] = black 735 //poison(append(path, pkg)) // poison self and parents 736 737 case black: 738 // black means we're revisiting a package that was already 739 // completely explored. If it has an entry in exrsets, it completed 740 // successfully. If not, it was poisoned, and we need to bubble the 741 // poison back up. 742 rs, exists := exrsets[pkg] 743 if !exists { 744 if backprop { 745 // just poison parents; self was necessarily already poisoned 746 poisonBlack(path, pkg) 747 } 748 return false 749 } 750 // If external reachset existed, internal must (even if empty) 751 irs := inrsets[pkg] 752 753 // It's good; pull over the imports from its reachset into all 754 // non-poisoned parent reachsets 755 for _, ppkg := range path { 756 if prs, exists := exrsets[ppkg]; exists { 757 for ex := range rs { 758 prs[ex] = struct{}{} 759 } 760 } 761 762 if prs, exists := inrsets[ppkg]; exists { 763 for in := range irs { 764 prs[in] = struct{}{} 765 } 766 } 767 } 768 return true 769 770 default: 771 panic(fmt.Sprintf("invalid color marker %v for %s", colors[pkg], pkg)) 772 } 773 } 774 775 // Run the depth-first exploration. 776 // 777 // Don't bother computing graph sources, this straightforward loop works 778 // comparably well, and fits nicely with an escape hatch in the dfe. 779 var path []string 780 for pkg := range workmap { 781 // However, at least check that the package isn't already fully visited; 782 // this saves a bit of time and implementation complexity inside the 783 // closures. 784 if colors[pkg] != black { 785 dfe(pkg, path) 786 } 787 } 788 789 type ie struct { 790 Internal, External []string 791 } 792 793 // Flatten exrsets into reachmap 794 rm := make(ReachMap) 795 for pkg, rs := range exrsets { 796 rlen := len(rs) 797 if rlen == 0 { 798 rm[pkg] = ie{} 799 continue 800 } 801 802 edeps := make([]string, 0, rlen) 803 for opkg := range rs { 804 edeps = append(edeps, opkg) 805 } 806 807 sort.Strings(edeps) 808 809 sets := rm[pkg] 810 sets.External = edeps 811 rm[pkg] = sets 812 } 813 814 // Flatten inrsets into reachmap 815 for pkg, rs := range inrsets { 816 rlen := len(rs) 817 if rlen == 0 { 818 continue 819 } 820 821 ideps := make([]string, 0, rlen) 822 for opkg := range rs { 823 ideps = append(ideps, opkg) 824 } 825 826 sort.Strings(ideps) 827 828 sets := rm[pkg] 829 sets.Internal = ideps 830 rm[pkg] = sets 831 } 832 833 return rm, errmap 834 } 835 836 // eqOrSlashedPrefix checks to see if the prefix is either equal to the string, 837 // or that it is a prefix and the next char in the string is "/". 838 func eqOrSlashedPrefix(s, prefix string) bool { 839 if !strings.HasPrefix(s, prefix) { 840 return false 841 } 842 843 prflen, pathlen := len(prefix), len(s) 844 return prflen == pathlen || strings.Index(s[prflen:], "/") == 0 845 } 846 847 // helper func to merge, dedupe, and sort strings 848 func dedupeStrings(s1, s2 []string) (r []string) { 849 dedupe := make(map[string]bool) 850 851 if len(s1) > 0 && len(s2) > 0 { 852 for _, i := range s1 { 853 dedupe[i] = true 854 } 855 for _, i := range s2 { 856 dedupe[i] = true 857 } 858 859 for i := range dedupe { 860 r = append(r, i) 861 } 862 // And then re-sort them 863 sort.Strings(r) 864 } else if len(s1) > 0 { 865 r = s1 866 } else if len(s2) > 0 { 867 r = s2 868 } 869 870 return 871 } 872 873 func uniq(a []string) []string { 874 if a == nil { 875 return make([]string, 0) 876 } 877 var s string 878 var i int 879 if !sort.StringsAreSorted(a) { 880 sort.Strings(a) 881 } 882 for _, t := range a { 883 if t != s { 884 a[i] = t 885 i++ 886 s = t 887 } 888 } 889 return a[:i] 890 }