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.
4
5// This file contains the code dealing with package directory trees.
6
7package godoc
8
9import (
10	"go/doc"
11	"go/parser"
12	"go/token"
13	"log"
14	"os"
15	pathpkg "path"
16	"runtime"
17	"sort"
18	"strings"
19
20	"golang.org/x/tools/godoc/vfs"
21)
22
23// Conventional name for directories containing test data.
24// Excluded from directory trees.
25//
26const testdataDirName = "testdata"
27
28type Directory struct {
29	Depth    int
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
36}
37
38func isGoFile(fi os.FileInfo) bool {
39	name := fi.Name()
40	return !fi.IsDir() &&
41		len(name) > 0 && name[0] != '.' && // ignore .files
42		pathpkg.Ext(name) == ".go"
43}
44
45func isPkgFile(fi os.FileInfo) bool {
46	return isGoFile(fi) &&
47		!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
48}
49
50func isPkgDir(fi os.FileInfo) bool {
51	name := fi.Name()
52	return fi.IsDir() && len(name) > 0 &&
53		name[0] != '_' && name[0] != '.' // ignore _files and .files
54}
55
56type treeBuilder struct {
57	c        *Corpus
58	maxDepth int
59}
60
61// ioGate is a semaphore controlling VFS activity (ReadDir, parseFile, etc).
62// Send before an operation and receive after.
63var ioGate = make(chan struct{}, 20)
64
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.
69var workGate = make(chan struct{}, runtime.NumCPU()*4)
70
71func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
72	if name == testdataDirName {
73		return nil
74	}
75
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
79		// directory depth
80		return &Directory{
81			Depth: depth,
82			Path:  path,
83			Name:  name,
84		}
85	}
86
87	var synopses [3]string // prioritized package documentation (0 == highest priority)
88
89	show := true // show in package listing
90	hasPkgFiles := false
91	haveSummary := false
92
93	if hook := b.c.SummarizePackage; hook != nil {
94		if summary, show0, ok := hook(strings.TrimPrefix(path, "/src/")); ok {
95			hasPkgFiles = true
96			show = show0
97			synopses[0] = summary
98			haveSummary = true
99		}
100	}
101
102	ioGate <- struct{}{}
103	list, err := b.c.fs.ReadDir(path)
104	<-ioGate
105	if err != nil {
106		// TODO: propagate more. See golang.org/issue/14252.
107		// For now:
108		if b.c.Verbose {
109			log.Printf("newDirTree reading %s: %v", path, err)
110		}
111	}
112
113	// determine number of subdirectories and if there are package files
114	var dirchs []chan *Directory
115	var dirs []*Directory
116
117	for _, d := range list {
118		filename := pathpkg.Join(path, d.Name())
119		switch {
120		case isPkgDir(d):
121			name := d.Name()
122			select {
123			case workGate <- struct{}{}:
124				ch := make(chan *Directory, 1)
125				dirchs = append(dirchs, ch)
126				go func() {
127					ch <- b.newDirTree(fset, filename, name, depth+1)
128					<-workGate
129				}()
130			default:
131				// no free workers, do work synchronously
132				dir := b.newDirTree(fset, filename, name, depth+1)
133				if dir != nil {
134					dirs = append(dirs, dir)
135				}
136			}
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
142			ioGate <- struct{}{}
143			const flags = parser.ParseComments | parser.PackageClauseOnly
144			file, err := b.c.parseFile(fset, filename, flags)
145			<-ioGate
146			if err != nil {
147				if b.c.Verbose {
148					log.Printf("Error parsing %v: %v", filename, err)
149				}
150				break
151			}
152
153			hasPkgFiles = true
154			if file.Doc != nil {
155				// prioritize documentation
156				i := -1
157				switch file.Name.Name {
158				case name:
159					i = 0 // normal case: directory name matches package name
160				case "main":
161					i = 1 // directory contains a main package
162				default:
163					i = 2 // none of the above
164				}
165				if 0 <= i && i < len(synopses) && synopses[i] == "" {
166					synopses[i] = doc.Synopsis(file.Doc.Text())
167				}
168			}
169			haveSummary = synopses[0] != ""
170		}
171	}
172
173	// create subdirectory tree
174	for _, ch := range dirchs {
175		if d := <-ch; d != nil {
176			dirs = append(dirs, d)
177		}
178	}
179
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
184	})
185
186	// if there are no package files and no subdirectories
187	// containing package files, ignore the directory
188	if !hasPkgFiles && len(dirs) == 0 {
189		return nil
190	}
191
192	// select the highest-priority synopsis for the directory entry, if any
193	synopsis := ""
194	for _, synopsis = range synopses {
195		if synopsis != "" {
196			break
197		}
198	}
199
200	return &Directory{
201		Depth:    depth,
202		Path:     path,
203		Name:     name,
204		HasPkg:   hasPkgFiles && show, // TODO(bradfitz): add proper Hide field?
205		Synopsis: synopsis,
206		RootType: b.c.fs.RootType(path),
207		Dirs:     dirs,
208	}
209}
210
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).
220//
221func (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.
226	switch {
227	case err != nil:
228		log.Printf("newDirectory(%s): %s", root, err)
229		return nil
230	case root != "/" && !isPkgDir(d):
231		log.Printf("newDirectory(%s): not a package directory", root)
232		return nil
233	case root == "/" && !d.IsDir():
234		log.Printf("newDirectory(%s): not a directory", root)
235		return nil
236	}
237	if maxDepth < 0 {
238		maxDepth = 1e6 // "infinity"
239	}
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)
244}
245
246func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
247	if dir != nil {
248		if !skipRoot {
249			c <- dir
250		}
251		for _, d := range dir.Dirs {
252			d.walk(c, false)
253		}
254	}
255}
256
257func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
258	c := make(chan *Directory)
259	go func() {
260		dir.walk(c, skipRoot)
261		close(c)
262	}()
263	return c
264}
265
266func (dir *Directory) lookupLocal(name string) *Directory {
267	for _, d := range dir.Dirs {
268		if d.Name == name {
269			return d
270		}
271	}
272	return nil
273}
274
275func splitPath(p string) []string {
276	p = strings.TrimPrefix(p, "/")
277	if p == "" {
278		return nil
279	}
280	return strings.Split(p, "/")
281}
282
283// lookup looks for the *Directory for a given path, relative to dir.
284func (dir *Directory) lookup(path string) *Directory {
285	d := splitPath(dir.Path)
286	p := splitPath(path)
287	i := 0
288	for i < len(d) {
289		if i >= len(p) || d[i] != p[i] {
290			return nil
291		}
292		i++
293	}
294	for dir != nil && i < len(p) {
295		dir = dir.lookupLocal(p[i])
296		i++
297	}
298	return dir
299}
300
301// DirEntry describes a directory entry. The Depth and Height values
302// are useful for presenting an entry in an indented fashion.
303//
304type DirEntry struct {
305	Depth    int          // >= 0
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
312}
313
314type DirList struct {
315	MaxHeight int // directory tree height, > 0
316	List      []DirEntry
317}
318
319// hasThirdParty checks whether a list of directory entries has packages outside
320// the standard library or not.
321func hasThirdParty(list []DirEntry) bool {
322	for _, entry := range list {
323		if entry.RootType == vfs.RootTypeGoPath {
324			return true
325		}
326	}
327	return false
328}
329
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
333// are included.
334//
335func (dir *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
336	if dir == nil {
337		return nil
338	}
339
340	// determine number of entries n and maximum height
341	n := 0
342	minDepth := 1 << 30 // infinity
343	maxDepth := 0
344	for d := range dir.iter(skipRoot) {
345		n++
346		if minDepth > d.Depth {
347			minDepth = d.Depth
348		}
349		if maxDepth < d.Depth {
350			maxDepth = d.Depth
351		}
352	}
353	maxHeight := maxDepth - minDepth + 1
354
355	if n == 0 {
356		return nil
357	}
358
359	// create list
360	list := make([]DirEntry, 0, n)
361	for d := range dir.iter(skipRoot) {
362		if filter != nil && !filter(d.Path) {
363			continue
364		}
365		var p DirEntry
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, "/")
374		p.Path = path
375		p.Name = d.Name
376		p.HasPkg = d.HasPkg
377		p.Synopsis = d.Synopsis
378		p.RootType = d.RootType
379		list = append(list, p)
380	}
381
382	return &DirList{maxHeight, list}
383}
384