1 // Copyright 2010 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 // This file contains the code dealing with package directory trees.
20 "golang.org/x/tools/godoc/vfs"
23 // Conventional name for directories containing test data.
24 // Excluded from directory trees.
26 const testdataDirName = "testdata"
28 type Directory struct {
30 Path string // directory path; includes Name
31 Name string // directory name
32 HasPkg bool // true if the directory contains at least one package
33 Synopsis string // package documentation, if any
34 RootType vfs.RootType // root type of the filesystem containing the directory
35 Dirs []*Directory // subdirectories
38 func isGoFile(fi os.FileInfo) bool {
41 len(name) > 0 && name[0] != '.' && // ignore .files
42 pathpkg.Ext(name) == ".go"
45 func isPkgFile(fi os.FileInfo) bool {
46 return isGoFile(fi) &&
47 !strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
50 func isPkgDir(fi os.FileInfo) bool {
52 return fi.IsDir() && len(name) > 0 &&
53 name[0] != '_' && name[0] != '.' // ignore _files and .files
56 type treeBuilder struct {
61 // ioGate is a semaphore controlling VFS activity (ReadDir, parseFile, etc).
62 // Send before an operation and receive after.
63 var ioGate = make(chan struct{}, 20)
65 // workGate controls the number of concurrent workers. Too many concurrent
66 // workers and performance degrades and the race detector gets overwhelmed. If
67 // we cannot check out a concurrent worker, work is performed by the main thread
68 // instead of spinning up another goroutine.
69 var workGate = make(chan struct{}, runtime.NumCPU()*4)
71 func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
72 if name == testdataDirName {
76 if depth >= b.maxDepth {
77 // return a dummy directory so that the parent directory
78 // doesn't get discarded just because we reached the max
87 var synopses [3]string // prioritized package documentation (0 == highest priority)
89 show := true // show in package listing
93 if hook := b.c.SummarizePackage; hook != nil {
94 if summary, show0, ok := hook(strings.TrimPrefix(path, "/src/")); ok {
103 list, err := b.c.fs.ReadDir(path)
106 // TODO: propagate more. See golang.org/issue/14252.
109 log.Printf("newDirTree reading %s: %v", path, err)
113 // determine number of subdirectories and if there are package files
114 var dirchs []chan *Directory
115 var dirs []*Directory
117 for _, d := range list {
118 filename := pathpkg.Join(path, d.Name())
123 case workGate <- struct{}{}:
124 ch := make(chan *Directory, 1)
125 dirchs = append(dirchs, ch)
127 ch <- b.newDirTree(fset, filename, name, depth+1)
131 // no free workers, do work synchronously
132 dir := b.newDirTree(fset, filename, name, depth+1)
134 dirs = append(dirs, dir)
137 case !haveSummary && isPkgFile(d):
138 // looks like a package file, but may just be a file ending in ".go";
139 // don't just count it yet (otherwise we may end up with hasPkgFiles even
140 // though the directory doesn't contain any real package files - was bug)
141 // no "optimal" package synopsis yet; continue to collect synopses
143 const flags = parser.ParseComments | parser.PackageClauseOnly
144 file, err := b.c.parseFile(fset, filename, flags)
148 log.Printf("Error parsing %v: %v", filename, err)
155 // prioritize documentation
157 switch file.Name.Name {
159 i = 0 // normal case: directory name matches package name
161 i = 1 // directory contains a main package
163 i = 2 // none of the above
165 if 0 <= i && i < len(synopses) && synopses[i] == "" {
166 synopses[i] = doc.Synopsis(file.Doc.Text())
169 haveSummary = synopses[0] != ""
173 // create subdirectory tree
174 for _, ch := range dirchs {
175 if d := <-ch; d != nil {
176 dirs = append(dirs, d)
180 // We need to sort the dirs slice because
181 // it is appended again after reading from dirchs.
182 sort.Slice(dirs, func(i, j int) bool {
183 return dirs[i].Name < dirs[j].Name
186 // if there are no package files and no subdirectories
187 // containing package files, ignore the directory
188 if !hasPkgFiles && len(dirs) == 0 {
192 // select the highest-priority synopsis for the directory entry, if any
194 for _, synopsis = range synopses {
204 HasPkg: hasPkgFiles && show, // TODO(bradfitz): add proper Hide field?
206 RootType: b.c.fs.RootType(path),
211 // newDirectory creates a new package directory tree with at most maxDepth
212 // levels, anchored at root. The result tree is pruned such that it only
213 // contains directories that contain package files or that contain
214 // subdirectories containing package files (transitively). If a non-nil
215 // pathFilter is provided, directory paths additionally must be accepted
216 // by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is
217 // provided for maxDepth, nodes at larger depths are pruned as well; they
218 // are assumed to contain package files even if their contents are not known
219 // (i.e., in this case the tree may contain directories w/o any package files).
221 func (c *Corpus) newDirectory(root string, maxDepth int) *Directory {
222 // The root could be a symbolic link so use Stat not Lstat.
223 d, err := c.fs.Stat(root)
224 // If we fail here, report detailed error messages; otherwise
225 // is is hard to see why a directory tree was not built.
228 log.Printf("newDirectory(%s): %s", root, err)
230 case root != "/" && !isPkgDir(d):
231 log.Printf("newDirectory(%s): not a package directory", root)
233 case root == "/" && !d.IsDir():
234 log.Printf("newDirectory(%s): not a directory", root)
238 maxDepth = 1e6 // "infinity"
240 b := treeBuilder{c, maxDepth}
241 // the file set provided is only for local parsing, no position
242 // information escapes and thus we don't need to save the set
243 return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)
246 func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
251 for _, d := range dir.Dirs {
257 func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
258 c := make(chan *Directory)
260 dir.walk(c, skipRoot)
266 func (dir *Directory) lookupLocal(name string) *Directory {
267 for _, d := range dir.Dirs {
275 func splitPath(p string) []string {
276 p = strings.TrimPrefix(p, "/")
280 return strings.Split(p, "/")
283 // lookup looks for the *Directory for a given path, relative to dir.
284 func (dir *Directory) lookup(path string) *Directory {
285 d := splitPath(dir.Path)
289 if i >= len(p) || d[i] != p[i] {
294 for dir != nil && i < len(p) {
295 dir = dir.lookupLocal(p[i])
301 // DirEntry describes a directory entry. The Depth and Height values
302 // are useful for presenting an entry in an indented fashion.
304 type DirEntry struct {
306 Height int // = DirList.MaxHeight - Depth, > 0
307 Path string // directory path; includes Name, relative to DirList root
308 Name string // directory name
309 HasPkg bool // true if the directory contains at least one package
310 Synopsis string // package documentation, if any
311 RootType vfs.RootType // root type of the filesystem containing the direntry
314 type DirList struct {
315 MaxHeight int // directory tree height, > 0
319 // hasThirdParty checks whether a list of directory entries has packages outside
320 // the standard library or not.
321 func hasThirdParty(list []DirEntry) bool {
322 for _, entry := range list {
323 if entry.RootType == vfs.RootTypeGoPath {
330 // listing creates a (linear) directory listing from a directory tree.
331 // If skipRoot is set, the root directory itself is excluded from the list.
332 // If filter is set, only the directory entries whose paths match the filter
335 func (dir *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
340 // determine number of entries n and maximum height
342 minDepth := 1 << 30 // infinity
344 for d := range dir.iter(skipRoot) {
346 if minDepth > d.Depth {
349 if maxDepth < d.Depth {
353 maxHeight := maxDepth - minDepth + 1
360 list := make([]DirEntry, 0, n)
361 for d := range dir.iter(skipRoot) {
362 if filter != nil && !filter(d.Path) {
366 p.Depth = d.Depth - minDepth
367 p.Height = maxHeight - p.Depth
368 // the path is relative to root.Path - remove the root.Path
369 // prefix (the prefix should always be present but avoid
370 // crashes and check)
371 path := strings.TrimPrefix(d.Path, dir.Path)
372 // remove leading separator if any - path must be relative
373 path = strings.TrimPrefix(path, "/")
377 p.Synopsis = d.Synopsis
378 p.RootType = d.RootType
379 list = append(list, p)
382 return &DirList{maxHeight, list}