| // Copyright 2010 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. |
| |
| // This file contains the code dealing with package directory trees. |
| |
| package godoc |
| |
| import ( |
| "go/doc" |
| "go/parser" |
| "go/token" |
| "log" |
| "os" |
| pathpkg "path" |
| "runtime" |
| "sort" |
| "strings" |
| |
| "golang.org/x/tools/godoc/vfs" |
| ) |
| |
| // Conventional name for directories containing test data. |
| // Excluded from directory trees. |
| // |
| const testdataDirName = "testdata" |
| |
| type Directory struct { |
| Depth int |
| Path string // directory path; includes Name |
| Name string // directory name |
| HasPkg bool // true if the directory contains at least one package |
| Synopsis string // package documentation, if any |
| RootType vfs.RootType // root type of the filesystem containing the directory |
| Dirs []*Directory // subdirectories |
| } |
| |
| func isGoFile(fi os.FileInfo) bool { |
| name := fi.Name() |
| return !fi.IsDir() && |
| len(name) > 0 && name[0] != '.' && // ignore .files |
| pathpkg.Ext(name) == ".go" |
| } |
| |
| func isPkgFile(fi os.FileInfo) bool { |
| return isGoFile(fi) && |
| !strings.HasSuffix(fi.Name(), "_test.go") // ignore test files |
| } |
| |
| func isPkgDir(fi os.FileInfo) bool { |
| name := fi.Name() |
| return fi.IsDir() && len(name) > 0 && |
| name[0] != '_' && name[0] != '.' // ignore _files and .files |
| } |
| |
| type treeBuilder struct { |
| c *Corpus |
| maxDepth int |
| } |
| |
| // ioGate is a semaphore controlling VFS activity (ReadDir, parseFile, etc). |
| // Send before an operation and receive after. |
| var ioGate = make(chan struct{}, 20) |
| |
| // workGate controls the number of concurrent workers. Too many concurrent |
| // workers and performance degrades and the race detector gets overwhelmed. If |
| // we cannot check out a concurrent worker, work is performed by the main thread |
| // instead of spinning up another goroutine. |
| var workGate = make(chan struct{}, runtime.NumCPU()*4) |
| |
| func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory { |
| if name == testdataDirName { |
| return nil |
| } |
| |
| if depth >= b.maxDepth { |
| // return a dummy directory so that the parent directory |
| // doesn't get discarded just because we reached the max |
| // directory depth |
| return &Directory{ |
| Depth: depth, |
| Path: path, |
| Name: name, |
| } |
| } |
| |
| var synopses [3]string // prioritized package documentation (0 == highest priority) |
| |
| show := true // show in package listing |
| hasPkgFiles := false |
| haveSummary := false |
| |
| if hook := b.c.SummarizePackage; hook != nil { |
| if summary, show0, ok := hook(strings.TrimPrefix(path, "/src/")); ok { |
| hasPkgFiles = true |
| show = show0 |
| synopses[0] = summary |
| haveSummary = true |
| } |
| } |
| |
| ioGate <- struct{}{} |
| list, err := b.c.fs.ReadDir(path) |
| <-ioGate |
| if err != nil { |
| // TODO: propagate more. See golang.org/issue/14252. |
| // For now: |
| if b.c.Verbose { |
| log.Printf("newDirTree reading %s: %v", path, err) |
| } |
| } |
| |
| // determine number of subdirectories and if there are package files |
| var dirchs []chan *Directory |
| var dirs []*Directory |
| |
| for _, d := range list { |
| filename := pathpkg.Join(path, d.Name()) |
| switch { |
| case isPkgDir(d): |
| name := d.Name() |
| select { |
| case workGate <- struct{}{}: |
| ch := make(chan *Directory, 1) |
| dirchs = append(dirchs, ch) |
| go func() { |
| ch <- b.newDirTree(fset, filename, name, depth+1) |
| <-workGate |
| }() |
| default: |
| // no free workers, do work synchronously |
| dir := b.newDirTree(fset, filename, name, depth+1) |
| if dir != nil { |
| dirs = append(dirs, dir) |
| } |
| } |
| case !haveSummary && isPkgFile(d): |
| // looks like a package file, but may just be a file ending in ".go"; |
| // don't just count it yet (otherwise we may end up with hasPkgFiles even |
| // though the directory doesn't contain any real package files - was bug) |
| // no "optimal" package synopsis yet; continue to collect synopses |
| ioGate <- struct{}{} |
| const flags = parser.ParseComments | parser.PackageClauseOnly |
| file, err := b.c.parseFile(fset, filename, flags) |
| <-ioGate |
| if err != nil { |
| if b.c.Verbose { |
| log.Printf("Error parsing %v: %v", filename, err) |
| } |
| break |
| } |
| |
| hasPkgFiles = true |
| if file.Doc != nil { |
| // prioritize documentation |
| i := -1 |
| switch file.Name.Name { |
| case name: |
| i = 0 // normal case: directory name matches package name |
| case "main": |
| i = 1 // directory contains a main package |
| default: |
| i = 2 // none of the above |
| } |
| if 0 <= i && i < len(synopses) && synopses[i] == "" { |
| synopses[i] = doc.Synopsis(file.Doc.Text()) |
| } |
| } |
| haveSummary = synopses[0] != "" |
| } |
| } |
| |
| // create subdirectory tree |
| for _, ch := range dirchs { |
| if d := <-ch; d != nil { |
| dirs = append(dirs, d) |
| } |
| } |
| |
| // We need to sort the dirs slice because |
| // it is appended again after reading from dirchs. |
| sort.Slice(dirs, func(i, j int) bool { |
| return dirs[i].Name < dirs[j].Name |
| }) |
| |
| // if there are no package files and no subdirectories |
| // containing package files, ignore the directory |
| if !hasPkgFiles && len(dirs) == 0 { |
| return nil |
| } |
| |
| // select the highest-priority synopsis for the directory entry, if any |
| synopsis := "" |
| for _, synopsis = range synopses { |
| if synopsis != "" { |
| break |
| } |
| } |
| |
| return &Directory{ |
| Depth: depth, |
| Path: path, |
| Name: name, |
| HasPkg: hasPkgFiles && show, // TODO(bradfitz): add proper Hide field? |
| Synopsis: synopsis, |
| RootType: b.c.fs.RootType(path), |
| Dirs: dirs, |
| } |
| } |
| |
| // newDirectory creates a new package directory tree with at most maxDepth |
| // levels, anchored at root. The result tree is pruned such that it only |
| // contains directories that contain package files or that contain |
| // subdirectories containing package files (transitively). If a non-nil |
| // pathFilter is provided, directory paths additionally must be accepted |
| // by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is |
| // provided for maxDepth, nodes at larger depths are pruned as well; they |
| // are assumed to contain package files even if their contents are not known |
| // (i.e., in this case the tree may contain directories w/o any package files). |
| // |
| func (c *Corpus) newDirectory(root string, maxDepth int) *Directory { |
| // The root could be a symbolic link so use Stat not Lstat. |
| d, err := c.fs.Stat(root) |
| // If we fail here, report detailed error messages; otherwise |
| // is is hard to see why a directory tree was not built. |
| switch { |
| case err != nil: |
| log.Printf("newDirectory(%s): %s", root, err) |
| return nil |
| case root != "/" && !isPkgDir(d): |
| log.Printf("newDirectory(%s): not a package directory", root) |
| return nil |
| case root == "/" && !d.IsDir(): |
| log.Printf("newDirectory(%s): not a directory", root) |
| return nil |
| } |
| if maxDepth < 0 { |
| maxDepth = 1e6 // "infinity" |
| } |
| b := treeBuilder{c, maxDepth} |
| // the file set provided is only for local parsing, no position |
| // information escapes and thus we don't need to save the set |
| return b.newDirTree(token.NewFileSet(), root, d.Name(), 0) |
| } |
| |
| func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) { |
| if dir != nil { |
| if !skipRoot { |
| c <- dir |
| } |
| for _, d := range dir.Dirs { |
| d.walk(c, false) |
| } |
| } |
| } |
| |
| func (dir *Directory) iter(skipRoot bool) <-chan *Directory { |
| c := make(chan *Directory) |
| go func() { |
| dir.walk(c, skipRoot) |
| close(c) |
| }() |
| return c |
| } |
| |
| func (dir *Directory) lookupLocal(name string) *Directory { |
| for _, d := range dir.Dirs { |
| if d.Name == name { |
| return d |
| } |
| } |
| return nil |
| } |
| |
| func splitPath(p string) []string { |
| p = strings.TrimPrefix(p, "/") |
| if p == "" { |
| return nil |
| } |
| return strings.Split(p, "/") |
| } |
| |
| // lookup looks for the *Directory for a given path, relative to dir. |
| func (dir *Directory) lookup(path string) *Directory { |
| d := splitPath(dir.Path) |
| p := splitPath(path) |
| i := 0 |
| for i < len(d) { |
| if i >= len(p) || d[i] != p[i] { |
| return nil |
| } |
| i++ |
| } |
| for dir != nil && i < len(p) { |
| dir = dir.lookupLocal(p[i]) |
| i++ |
| } |
| return dir |
| } |
| |
| // DirEntry describes a directory entry. The Depth and Height values |
| // are useful for presenting an entry in an indented fashion. |
| // |
| type DirEntry struct { |
| Depth int // >= 0 |
| Height int // = DirList.MaxHeight - Depth, > 0 |
| Path string // directory path; includes Name, relative to DirList root |
| Name string // directory name |
| HasPkg bool // true if the directory contains at least one package |
| Synopsis string // package documentation, if any |
| RootType vfs.RootType // root type of the filesystem containing the direntry |
| } |
| |
| type DirList struct { |
| MaxHeight int // directory tree height, > 0 |
| List []DirEntry |
| } |
| |
| // hasThirdParty checks whether a list of directory entries has packages outside |
| // the standard library or not. |
| func hasThirdParty(list []DirEntry) bool { |
| for _, entry := range list { |
| if entry.RootType == vfs.RootTypeGoPath { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // listing creates a (linear) directory listing from a directory tree. |
| // If skipRoot is set, the root directory itself is excluded from the list. |
| // If filter is set, only the directory entries whose paths match the filter |
| // are included. |
| // |
| func (dir *Directory) listing(skipRoot bool, filter func(string) bool) *DirList { |
| if dir == nil { |
| return nil |
| } |
| |
| // determine number of entries n and maximum height |
| n := 0 |
| minDepth := 1 << 30 // infinity |
| maxDepth := 0 |
| for d := range dir.iter(skipRoot) { |
| n++ |
| if minDepth > d.Depth { |
| minDepth = d.Depth |
| } |
| if maxDepth < d.Depth { |
| maxDepth = d.Depth |
| } |
| } |
| maxHeight := maxDepth - minDepth + 1 |
| |
| if n == 0 { |
| return nil |
| } |
| |
| // create list |
| list := make([]DirEntry, 0, n) |
| for d := range dir.iter(skipRoot) { |
| if filter != nil && !filter(d.Path) { |
| continue |
| } |
| var p DirEntry |
| p.Depth = d.Depth - minDepth |
| p.Height = maxHeight - p.Depth |
| // the path is relative to root.Path - remove the root.Path |
| // prefix (the prefix should always be present but avoid |
| // crashes and check) |
| path := strings.TrimPrefix(d.Path, dir.Path) |
| // remove leading separator if any - path must be relative |
| path = strings.TrimPrefix(path, "/") |
| p.Path = path |
| p.Name = d.Name |
| p.HasPkg = d.HasPkg |
| p.Synopsis = d.Synopsis |
| p.RootType = d.RootType |
| list = append(list, p) |
| } |
| |
| return &DirList{maxHeight, list} |
| } |