2018-09-18 13:06:34 -06:00
|
|
|
// Copyright 2018 The Go Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
// Package gopathwalk is like filepath.Walk but specialized for finding Go
|
|
|
|
// packages, particularly in $GOPATH and $GOROOT.
|
|
|
|
package gopathwalk
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"go/build"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2019-10-09 17:05:30 -06:00
|
|
|
"time"
|
2018-11-07 12:55:33 -07:00
|
|
|
|
|
|
|
"golang.org/x/tools/internal/fastwalk"
|
2018-09-18 13:06:34 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
// Options controls the behavior of a Walk call.
|
|
|
|
type Options struct {
|
2020-02-28 15:03:27 -07:00
|
|
|
// If Logf is non-nil, debug logging is enabled through this function.
|
|
|
|
Logf func(format string, args ...interface{})
|
|
|
|
// Search module caches. Also disables legacy goimports ignore rules.
|
|
|
|
ModulesEnabled bool
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
|
2018-09-27 16:21:00 -06:00
|
|
|
// RootType indicates the type of a Root.
|
|
|
|
type RootType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
RootUnknown RootType = iota
|
|
|
|
RootGOROOT
|
|
|
|
RootGOPATH
|
|
|
|
RootCurrentModule
|
|
|
|
RootModuleCache
|
imports: stop using go/packages for modules
go/packages needs to call `go list` multiple times, which causes
redundant work and slows down goimports. If we reimplement `go list` in
memory, we can reuse state, saving time. `go list` also does work we
don't really need, like adding stuff to go.mod, and skipping that saves
more time.
We start with `go list -m`, which does MVS and such. The remaining work
is mostly mapping import paths and directories through the in-scope
modules to make sure we're giving the right answers. Unfortunately this
is quite subtle, and I don't know where all the traps are. I did my
best.
cmd/go already has tests for `go list`, of course, and packagestest is
not well suited to tests of this complexity. So I ripped off the script
tests in cmd/go that seemed relevant and made sure that our logic
returns the right stuff in each case. I'm sure that there are more cases
to cover, but this hit all the stuff I knew about and quite a bit I
didn't.
Since we may want to use the go/packages code path in the future, e.g.
for Bazel, I left that in place. It won't be used unless the magic env
var is set.
Files in internal and imports/testdata/mod were copied verbatim from
cmd/go.
Change-Id: I1248d99c400c1a0c7ef180d4460b9b8a3db0246b
Reviewed-on: https://go-review.googlesource.com/c/158097
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
2019-01-16 14:24:49 -07:00
|
|
|
RootOther
|
2018-09-27 16:21:00 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
// A Root is a starting point for a Walk.
|
|
|
|
type Root struct {
|
|
|
|
Path string
|
|
|
|
Type RootType
|
|
|
|
}
|
|
|
|
|
|
|
|
// SrcDirsRoots returns the roots from build.Default.SrcDirs(). Not modules-compatible.
|
2019-01-17 13:27:01 -07:00
|
|
|
func SrcDirsRoots(ctx *build.Context) []Root {
|
2018-09-27 16:21:00 -06:00
|
|
|
var roots []Root
|
2019-01-17 13:27:01 -07:00
|
|
|
roots = append(roots, Root{filepath.Join(ctx.GOROOT, "src"), RootGOROOT})
|
|
|
|
for _, p := range filepath.SplitList(ctx.GOPATH) {
|
2018-09-27 16:21:00 -06:00
|
|
|
roots = append(roots, Root{filepath.Join(p, "src"), RootGOPATH})
|
|
|
|
}
|
|
|
|
return roots
|
|
|
|
}
|
|
|
|
|
2018-09-18 13:06:34 -06:00
|
|
|
// Walk walks Go source directories ($GOROOT, $GOPATH, etc) to find packages.
|
|
|
|
// For each package found, add will be called (concurrently) with the absolute
|
|
|
|
// paths of the containing source directory and the package directory.
|
2018-09-27 16:21:00 -06:00
|
|
|
// add will be called concurrently.
|
|
|
|
func Walk(roots []Root, add func(root Root, dir string), opts Options) {
|
2019-07-19 10:41:29 -06:00
|
|
|
WalkSkip(roots, add, func(Root, string) bool { return false }, opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WalkSkip walks Go source directories ($GOROOT, $GOPATH, etc) to find packages.
|
|
|
|
// For each package found, add will be called (concurrently) with the absolute
|
|
|
|
// paths of the containing source directory and the package directory.
|
|
|
|
// For each directory that will be scanned, skip will be called (concurrently)
|
|
|
|
// with the absolute paths of the containing source directory and the directory.
|
|
|
|
// If skip returns false on a directory it will be processed.
|
|
|
|
// add will be called concurrently.
|
|
|
|
// skip will be called concurrently.
|
|
|
|
func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root, dir string) bool, opts Options) {
|
2018-09-27 16:21:00 -06:00
|
|
|
for _, root := range roots {
|
2019-07-19 10:41:29 -06:00
|
|
|
walkDir(root, add, skip, opts)
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-22 11:44:52 -07:00
|
|
|
// walkDir creates a walker and starts fastwalk with this walker.
|
2019-07-19 10:41:29 -06:00
|
|
|
func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) {
|
2018-10-17 11:59:53 -06:00
|
|
|
if _, err := os.Stat(root.Path); os.IsNotExist(err) {
|
2020-02-28 15:03:27 -07:00
|
|
|
if opts.Logf != nil {
|
|
|
|
opts.Logf("skipping nonexistent directory: %v", root.Path)
|
2018-10-17 11:59:53 -06:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2019-10-09 17:05:30 -06:00
|
|
|
start := time.Now()
|
2020-02-28 15:03:27 -07:00
|
|
|
if opts.Logf != nil {
|
|
|
|
opts.Logf("gopathwalk: scanning %s", root.Path)
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
w := &walker{
|
2018-09-27 16:21:00 -06:00
|
|
|
root: root,
|
|
|
|
add: add,
|
2019-07-19 10:41:29 -06:00
|
|
|
skip: skip,
|
2018-09-27 16:21:00 -06:00
|
|
|
opts: opts,
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
w.init()
|
2018-09-27 16:21:00 -06:00
|
|
|
if err := fastwalk.Walk(root.Path, w.walk); err != nil {
|
2018-10-17 11:59:53 -06:00
|
|
|
log.Printf("gopathwalk: scanning directory %v: %v", root.Path, err)
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
|
2020-02-28 15:03:27 -07:00
|
|
|
if opts.Logf != nil {
|
|
|
|
opts.Logf("gopathwalk: scanned %s in %v", root.Path, time.Since(start))
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// walker is the callback for fastwalk.Walk.
|
|
|
|
type walker struct {
|
2019-07-19 10:41:29 -06:00
|
|
|
root Root // The source directory to scan.
|
|
|
|
add func(Root, string) // The callback that will be invoked for every possible Go package dir.
|
|
|
|
skip func(Root, string) bool // The callback that will be invoked for every dir. dir is skipped if it returns true.
|
|
|
|
opts Options // Options passed to Walk by the user.
|
2018-09-18 13:06:34 -06:00
|
|
|
|
|
|
|
ignoredDirs []os.FileInfo // The ignored directories, loaded from .goimportsignore files.
|
|
|
|
}
|
|
|
|
|
2019-11-22 11:44:52 -07:00
|
|
|
// init initializes the walker based on its Options
|
2018-09-18 13:06:34 -06:00
|
|
|
func (w *walker) init() {
|
2018-09-27 16:21:00 -06:00
|
|
|
var ignoredPaths []string
|
|
|
|
if w.root.Type == RootModuleCache {
|
|
|
|
ignoredPaths = []string{"cache"}
|
|
|
|
}
|
|
|
|
if !w.opts.ModulesEnabled && w.root.Type == RootGOPATH {
|
|
|
|
ignoredPaths = w.getIgnoredDirs(w.root.Path)
|
|
|
|
ignoredPaths = append(ignoredPaths, "v", "mod")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range ignoredPaths {
|
|
|
|
full := filepath.Join(w.root.Path, p)
|
|
|
|
if fi, err := os.Stat(full); err == nil {
|
|
|
|
w.ignoredDirs = append(w.ignoredDirs, fi)
|
2020-02-28 15:03:27 -07:00
|
|
|
if w.opts.Logf != nil {
|
|
|
|
w.opts.Logf("Directory added to ignore list: %s", full)
|
2018-09-27 16:21:00 -06:00
|
|
|
}
|
2020-02-28 15:03:27 -07:00
|
|
|
} else if w.opts.Logf != nil {
|
|
|
|
w.opts.Logf("Error statting ignored directory: %v", err)
|
2018-09-27 16:21:00 -06:00
|
|
|
}
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// getIgnoredDirs reads an optional config file at <path>/.goimportsignore
|
|
|
|
// of relative directories to ignore when scanning for go files.
|
|
|
|
// The provided path is one of the $GOPATH entries with "src" appended.
|
2018-09-27 16:21:00 -06:00
|
|
|
func (w *walker) getIgnoredDirs(path string) []string {
|
2018-09-18 13:06:34 -06:00
|
|
|
file := filepath.Join(path, ".goimportsignore")
|
|
|
|
slurp, err := ioutil.ReadFile(file)
|
2020-02-28 15:03:27 -07:00
|
|
|
if w.opts.Logf != nil {
|
2018-09-18 13:06:34 -06:00
|
|
|
if err != nil {
|
2020-02-28 15:03:27 -07:00
|
|
|
w.opts.Logf("%v", err)
|
2018-09-18 13:06:34 -06:00
|
|
|
} else {
|
2020-02-28 15:03:27 -07:00
|
|
|
w.opts.Logf("Read %s", file)
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-27 16:21:00 -06:00
|
|
|
var ignoredDirs []string
|
2018-09-18 13:06:34 -06:00
|
|
|
bs := bufio.NewScanner(bytes.NewReader(slurp))
|
|
|
|
for bs.Scan() {
|
|
|
|
line := strings.TrimSpace(bs.Text())
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
|
|
continue
|
|
|
|
}
|
2018-09-27 16:21:00 -06:00
|
|
|
ignoredDirs = append(ignoredDirs, line)
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
return ignoredDirs
|
|
|
|
}
|
|
|
|
|
2019-11-22 11:44:52 -07:00
|
|
|
// shouldSkipDir reports whether the file should be skipped or not.
|
2019-07-19 10:41:29 -06:00
|
|
|
func (w *walker) shouldSkipDir(fi os.FileInfo, dir string) bool {
|
2018-09-18 13:06:34 -06:00
|
|
|
for _, ignoredDir := range w.ignoredDirs {
|
|
|
|
if os.SameFile(fi, ignoredDir) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2019-07-19 10:41:29 -06:00
|
|
|
if w.skip != nil {
|
|
|
|
// Check with the user specified callback.
|
|
|
|
return w.skip(w.root, dir)
|
|
|
|
}
|
2018-09-18 13:06:34 -06:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-11-22 11:44:52 -07:00
|
|
|
// walk walks through the given path.
|
2018-09-18 13:06:34 -06:00
|
|
|
func (w *walker) walk(path string, typ os.FileMode) error {
|
|
|
|
dir := filepath.Dir(path)
|
|
|
|
if typ.IsRegular() {
|
imports: stop using go/packages for modules
go/packages needs to call `go list` multiple times, which causes
redundant work and slows down goimports. If we reimplement `go list` in
memory, we can reuse state, saving time. `go list` also does work we
don't really need, like adding stuff to go.mod, and skipping that saves
more time.
We start with `go list -m`, which does MVS and such. The remaining work
is mostly mapping import paths and directories through the in-scope
modules to make sure we're giving the right answers. Unfortunately this
is quite subtle, and I don't know where all the traps are. I did my
best.
cmd/go already has tests for `go list`, of course, and packagestest is
not well suited to tests of this complexity. So I ripped off the script
tests in cmd/go that seemed relevant and made sure that our logic
returns the right stuff in each case. I'm sure that there are more cases
to cover, but this hit all the stuff I knew about and quite a bit I
didn't.
Since we may want to use the go/packages code path in the future, e.g.
for Bazel, I left that in place. It won't be used unless the magic env
var is set.
Files in internal and imports/testdata/mod were copied verbatim from
cmd/go.
Change-Id: I1248d99c400c1a0c7ef180d4460b9b8a3db0246b
Reviewed-on: https://go-review.googlesource.com/c/158097
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
2019-01-16 14:24:49 -07:00
|
|
|
if dir == w.root.Path && (w.root.Type == RootGOROOT || w.root.Type == RootGOPATH) {
|
2018-09-18 13:06:34 -06:00
|
|
|
// Doesn't make sense to have regular files
|
|
|
|
// directly in your $GOPATH/src or $GOROOT/src.
|
2019-11-20 20:43:00 -07:00
|
|
|
return fastwalk.ErrSkipFiles
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
if !strings.HasSuffix(path, ".go") {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-27 16:21:00 -06:00
|
|
|
w.add(w.root, dir)
|
2019-11-20 20:43:00 -07:00
|
|
|
return fastwalk.ErrSkipFiles
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
if typ == os.ModeDir {
|
|
|
|
base := filepath.Base(path)
|
|
|
|
if base == "" || base[0] == '.' || base[0] == '_' ||
|
2018-11-07 12:55:33 -07:00
|
|
|
base == "testdata" ||
|
|
|
|
(w.root.Type == RootGOROOT && w.opts.ModulesEnabled && base == "vendor") ||
|
|
|
|
(!w.opts.ModulesEnabled && base == "node_modules") {
|
2018-09-18 13:06:34 -06:00
|
|
|
return filepath.SkipDir
|
|
|
|
}
|
|
|
|
fi, err := os.Lstat(path)
|
2019-07-19 10:41:29 -06:00
|
|
|
if err == nil && w.shouldSkipDir(fi, path) {
|
2018-09-18 13:06:34 -06:00
|
|
|
return filepath.SkipDir
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if typ == os.ModeSymlink {
|
|
|
|
base := filepath.Base(path)
|
|
|
|
if strings.HasPrefix(base, ".#") {
|
|
|
|
// Emacs noise.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
fi, err := os.Lstat(path)
|
|
|
|
if err != nil {
|
|
|
|
// Just ignore it.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if w.shouldTraverse(dir, fi) {
|
2019-11-20 20:43:00 -07:00
|
|
|
return fastwalk.ErrTraverseLink
|
2018-09-18 13:06:34 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// shouldTraverse reports whether the symlink fi, found in dir,
|
|
|
|
// should be followed. It makes sure symlinks were never visited
|
|
|
|
// before to avoid symlink loops.
|
|
|
|
func (w *walker) shouldTraverse(dir string, fi os.FileInfo) bool {
|
|
|
|
path := filepath.Join(dir, fi.Name())
|
|
|
|
target, err := filepath.EvalSymlinks(path)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
ts, err := os.Stat(target)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if !ts.IsDir() {
|
|
|
|
return false
|
|
|
|
}
|
2019-07-19 10:41:29 -06:00
|
|
|
if w.shouldSkipDir(ts, dir) {
|
2018-09-18 13:06:34 -06:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
// Check for symlink loops by statting each directory component
|
|
|
|
// and seeing if any are the same file as ts.
|
|
|
|
for {
|
|
|
|
parent := filepath.Dir(path)
|
|
|
|
if parent == path {
|
|
|
|
// Made it to the root without seeing a cycle.
|
|
|
|
// Use this symlink.
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
parentInfo, err := os.Stat(parent)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if os.SameFile(ts, parentInfo) {
|
|
|
|
// Cycle. Don't traverse.
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
path = parent
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|