github.com/v2fly/tools@v0.100.0/godoc/dirtrees.go (about)

     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  
     7  package godoc
     8  
     9  import (
    10  	"go/doc"
    11  	"go/parser"
    12  	"go/token"
    13  	"log"
    14  	"os"
    15  	pathpkg "path"
    16  	"runtime"
    17  	"sort"
    18  	"strings"
    19  
    20  	"github.com/v2fly/tools/godoc/vfs"
    21  )
    22  
    23  // Conventional name for directories containing test data.
    24  // Excluded from directory trees.
    25  //
    26  const testdataDirName = "testdata"
    27  
    28  type 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  
    38  func 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  
    45  func isPkgFile(fi os.FileInfo) bool {
    46  	return isGoFile(fi) &&
    47  		!strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
    48  }
    49  
    50  func 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  
    56  type 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.
    63  var 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.
    69  var workGate = make(chan struct{}, runtime.NumCPU()*4)
    70  
    71  func (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  //
   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.
   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  
   246  func (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  
   257  func (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  
   266  func (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  
   275  func 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.
   284  func (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  //
   304  type 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  
   314  type 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.
   321  func 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  //
   335  func (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  }