1
0
mirror of https://github.com/golang/go synced 2024-11-18 19:54:44 -07:00

imports: refactor directory walking

We plan to reuse goimports' directory walking logic in the
implementation of go/packages. To prepare for that, refactor it to have
fewer global variables and a simpler interface.

This CL makes no functional changes, but may change performance
slightly. It always scans both $GOPATH and $GOROOT, and does so
serially. I expect that fastwalk's internal parallelism is enough to
keep the disk busy, and I don't think it's worth optimizing for people
hacking on Go itself.

Change-Id: Id797e1b8e31d52e2eae07b42761ac136689cec32
Reviewed-on: https://go-review.googlesource.com/c/135678
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Heschi Kreinick 2018-09-17 18:07:46 -04:00
parent 12a7c317e8
commit 1d07bcb7f9
2 changed files with 107 additions and 162 deletions

View File

@ -471,16 +471,8 @@ func init() {
// Directory-scanning state. // Directory-scanning state.
var ( var (
// scanGoRootOnce guards calling scanGoRoot (for $GOROOT) // scanOnce guards calling scanGoDirs and assigning dirScan
scanGoRootOnce sync.Once scanOnce sync.Once
// scanGoPathOnce guards calling scanGoPath (for $GOPATH)
scanGoPathOnce sync.Once
// populateIgnoreOnce guards calling populateIgnore
populateIgnoreOnce sync.Once
ignoredDirs []os.FileInfo
dirScanMu sync.Mutex
dirScan map[string]*pkg // abs dir path => *pkg dirScan map[string]*pkg // abs dir path => *pkg
) )
@ -531,20 +523,10 @@ func distance(basepath, targetpath string) int {
return strings.Count(p, string(filepath.Separator)) + 1 return strings.Count(p, string(filepath.Separator)) + 1
} }
// guarded by populateIgnoreOnce; populates ignoredDirs. // getIgnoredDirs reads an optional config file at <path>/.goimportsignore
func populateIgnore() {
for _, srcDir := range build.Default.SrcDirs() {
if srcDir == filepath.Join(build.Default.GOROOT, "src") {
continue
}
populateIgnoredDirs(srcDir)
}
}
// populateIgnoredDirs reads an optional config file at <path>/.goimportsignore
// of relative directories to ignore when scanning for go files. // of relative directories to ignore when scanning for go files.
// The provided path is one of the $GOPATH entries with "src" appended. // The provided path is one of the $GOPATH entries with "src" appended.
func populateIgnoredDirs(path string) { func getIgnoredDirs(path string) []os.FileInfo {
file := filepath.Join(path, ".goimportsignore") file := filepath.Join(path, ".goimportsignore")
slurp, err := ioutil.ReadFile(file) slurp, err := ioutil.ReadFile(file)
if Debug { if Debug {
@ -555,8 +537,10 @@ func populateIgnoredDirs(path string) {
} }
} }
if err != nil { if err != nil {
return return nil
} }
var ignoredDirs []os.FileInfo
bs := bufio.NewScanner(bytes.NewReader(slurp)) bs := bufio.NewScanner(bytes.NewReader(slurp))
for bs.Scan() { for bs.Scan() {
line := strings.TrimSpace(bs.Text()) line := strings.TrimSpace(bs.Text())
@ -573,9 +557,10 @@ func populateIgnoredDirs(path string) {
log.Printf("Error statting entry in .goimportsignore: %v", err) log.Printf("Error statting entry in .goimportsignore: %v", err)
} }
} }
return ignoredDirs
} }
func skipDir(fi os.FileInfo) bool { func shouldSkipDir(fi os.FileInfo, ignoredDirs []os.FileInfo) bool {
for _, ignoredDir := range ignoredDirs { for _, ignoredDir := range ignoredDirs {
if os.SameFile(fi, ignoredDir) { if os.SameFile(fi, ignoredDir) {
return true return true
@ -587,7 +572,7 @@ func skipDir(fi os.FileInfo) bool {
// shouldTraverse reports whether the symlink fi, found in dir, // shouldTraverse reports whether the symlink fi, found in dir,
// should be followed. It makes sure symlinks were never visited // should be followed. It makes sure symlinks were never visited
// before to avoid symlink loops. // before to avoid symlink loops.
func shouldTraverse(dir string, fi os.FileInfo) bool { func shouldTraverse(dir string, fi os.FileInfo, ignoredDirs []os.FileInfo) bool {
path := filepath.Join(dir, fi.Name()) path := filepath.Join(dir, fi.Name())
target, err := filepath.EvalSymlinks(path) target, err := filepath.EvalSymlinks(path)
if err != nil { if err != nil {
@ -601,7 +586,7 @@ func shouldTraverse(dir string, fi os.FileInfo) bool {
if !ts.IsDir() { if !ts.IsDir() {
return false return false
} }
if skipDir(ts) { if shouldSkipDir(ts, ignoredDirs) {
return false return false
} }
// Check for symlink loops by statting each directory component // Check for symlink loops by statting each directory component
@ -628,38 +613,53 @@ func shouldTraverse(dir string, fi os.FileInfo) bool {
var testHookScanDir = func(dir string) {} var testHookScanDir = func(dir string) {}
type goDirType string // scanGoDirs populates the dirScan map for GOPATH and GOROOT.
func scanGoDirs() map[string]*pkg {
const ( result := make(map[string]*pkg)
goRoot goDirType = "$GOROOT" var mu sync.Mutex
goPath goDirType = "$GOPATH"
)
var scanGoRootDone = make(chan struct{}) // closed when scanGoRoot is done
// scanGoDirs populates the dirScan map for the given directory type. It may be
// called concurrently (and usually is, if both directory types are needed).
func scanGoDirs(which goDirType) {
if Debug {
log.Printf("scanning %s", which)
defer log.Printf("scanned %s", which)
}
for _, srcDir := range build.Default.SrcDirs() { for _, srcDir := range build.Default.SrcDirs() {
isGoroot := srcDir == filepath.Join(build.Default.GOROOT, "src") if Debug {
if isGoroot != (which == goRoot) { log.Printf("scanning %s", srcDir)
continue
} }
testHookScanDir(srcDir) testHookScanDir(srcDir)
srcV := filepath.Join(srcDir, "v") w := &walker{
srcMod := filepath.Join(srcDir, "mod") srcDir: srcDir,
walkFn := func(path string, typ os.FileMode) error { srcV: filepath.Join(srcDir, "v"),
if path == srcV || path == srcMod { srcMod: filepath.Join(srcDir, "mod"),
ignoredDirs: getIgnoredDirs(srcDir),
dirScanMu: &mu,
dirScan: result,
}
if err := fastwalk.Walk(srcDir, w.walk); err != nil {
log.Printf("goimports: scanning directory %v: %v", srcDir, err)
}
if Debug {
defer log.Printf("scanned %s", srcDir)
}
}
return result
}
// walker is the callback for fastwalk.Walk.
type walker struct {
srcDir string // The source directory to scan.
srcV, srcMod string // vgo-style module cache dirs. Optional.
ignoredDirs []os.FileInfo // The ignored directories, loaded from .goimportsignore files.
dirScanMu *sync.Mutex // The shared mutex guarding dirScan.
dirScan map[string]*pkg // The results of the scan, sharable across multiple Walk calls.
}
func (w *walker) walk(path string, typ os.FileMode) error {
if path == w.srcV || path == w.srcMod {
return filepath.SkipDir return filepath.SkipDir
} }
dir := filepath.Dir(path) dir := filepath.Dir(path)
if typ.IsRegular() { if typ.IsRegular() {
if dir == srcDir { if dir == w.srcDir {
// Doesn't make sense to have regular files // Doesn't make sense to have regular files
// directly in your $GOPATH/src or $GOROOT/src. // directly in your $GOPATH/src or $GOROOT/src.
return nil return nil
@ -668,16 +668,13 @@ func scanGoDirs(which goDirType) {
return nil return nil
} }
dirScanMu.Lock() w.dirScanMu.Lock()
defer dirScanMu.Unlock() defer w.dirScanMu.Unlock()
if _, dup := dirScan[dir]; dup { if _, dup := w.dirScan[dir]; dup {
return nil return nil
} }
if dirScan == nil { importpath := filepath.ToSlash(dir[len(w.srcDir)+len("/"):])
dirScan = make(map[string]*pkg) w.dirScan[dir] = &pkg{
}
importpath := filepath.ToSlash(dir[len(srcDir)+len("/"):])
dirScan[dir] = &pkg{
importPath: importpath, importPath: importpath,
importPathShort: VendorlessPath(importpath), importPathShort: VendorlessPath(importpath),
dir: dir, dir: dir,
@ -691,7 +688,7 @@ func scanGoDirs(which goDirType) {
return filepath.SkipDir return filepath.SkipDir
} }
fi, err := os.Lstat(path) fi, err := os.Lstat(path)
if err == nil && skipDir(fi) { if err == nil && shouldSkipDir(fi, w.ignoredDirs) {
if Debug { if Debug {
log.Printf("skipping directory %q under %s", fi.Name(), dir) log.Printf("skipping directory %q under %s", fi.Name(), dir)
} }
@ -710,17 +707,12 @@ func scanGoDirs(which goDirType) {
// Just ignore it. // Just ignore it.
return nil return nil
} }
if shouldTraverse(dir, fi) { if shouldTraverse(dir, fi, w.ignoredDirs) {
return fastwalk.TraverseLink return fastwalk.TraverseLink
} }
} }
return nil return nil
} }
if err := fastwalk.Walk(srcDir, walkFn); err != nil {
log.Printf("goimports: scanning directory %v: %v", srcDir, err)
}
}
}
// VendorlessPath returns the devendorized version of the import path ipath. // VendorlessPath returns the devendorized version of the import path ipath.
// For example, VendorlessPath("foo/bar/vendor/a/b") returns "a/b". // For example, VendorlessPath("foo/bar/vendor/a/b") returns "a/b".
@ -868,25 +860,8 @@ func findImportGoPath(ctx context.Context, pkgName string, symbols map[string]bo
// in the current Go file. Return rename=true when the other Go files // in the current Go file. Return rename=true when the other Go files
// use a renamed package that's also used in the current file. // use a renamed package that's also used in the current file.
// Read all the $GOPATH/src/.goimportsignore files before scanning directories. // Scan $GOROOT and each $GOPATH.
populateIgnoreOnce.Do(populateIgnore) scanOnce.Do(func() { dirScan = scanGoDirs() })
// Start scanning the $GOROOT asynchronously, then run the
// GOPATH scan synchronously if needed, and then wait for the
// $GOROOT to finish.
//
// TODO(bradfitz): run each $GOPATH entry async. But nobody
// really has more than one anyway, so low priority.
scanGoRootOnce.Do(func() {
go func() {
scanGoDirs(goRoot)
close(scanGoRootDone)
}()
})
if !fileInDir(filename, build.Default.GOROOT) {
scanGoPathOnce.Do(func() { scanGoDirs(goPath) })
}
<-scanGoRootDone
// Find candidate packages, looking only at their directory names first. // Find candidate packages, looking only at their directory names first.
var candidates []pkgDistance var candidates []pkgDistance

View File

@ -1552,12 +1552,7 @@ type Buffer2 struct {}
} }
func withEmptyGoPath(fn func()) { func withEmptyGoPath(fn func()) {
populateIgnoreOnce = sync.Once{} scanOnce = sync.Once{}
scanGoRootOnce = sync.Once{}
scanGoPathOnce = sync.Once{}
dirScan = nil
ignoredDirs = nil
scanGoRootDone = make(chan struct{})
oldGOPATH := build.Default.GOPATH oldGOPATH := build.Default.GOPATH
oldGOROOT := build.Default.GOROOT oldGOROOT := build.Default.GOROOT
@ -1918,31 +1913,6 @@ const _ = runtime.GOOS
} }
} }
// Tests that running goimport on files in GOROOT (for people hacking
// on Go itself) don't cause the GOPATH to be scanned (which might be
// much bigger).
func TestOptimizationWhenInGoroot(t *testing.T) {
testConfig{
gopathFiles: map[string]string{
"foo/foo.go": "package foo\nconst X = 1\n",
},
}.test(t, func(t *goimportTest) {
testHookScanDir = func(dir string) {
if dir != filepath.Join(build.Default.GOROOT, "src") {
t.Errorf("unexpected dir scan of %s", dir)
}
}
const in = "package foo\n\nconst Y = bar.X\n"
buf, err := Process(t.goroot+"/src/foo/foo.go", []byte(in), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != in {
t.Errorf("got:\n%q\nwant unchanged:\n%q\n", in, buf)
}
})
}
// Tests that "package documentation" files are ignored. // Tests that "package documentation" files are ignored.
func TestIgnoreDocumentationPackage(t *testing.T) { func TestIgnoreDocumentationPackage(t *testing.T) {
testConfig{ testConfig{
@ -2362,7 +2332,7 @@ func TestShouldTraverse(t *testing.T) {
t.Errorf("%d. Stat = %v", i, err) t.Errorf("%d. Stat = %v", i, err)
continue continue
} }
got := shouldTraverse(tt.dir, fi) got := shouldTraverse(tt.dir, fi, nil)
if got != tt.want { if got != tt.want {
t.Errorf("%d. shouldTraverse(%q, %q) = %v; want %v", i, tt.dir, tt.file, got, tt.want) t.Errorf("%d. shouldTraverse(%q, %q) = %v; want %v", i, tt.dir, tt.file, got, tt.want)
} }