You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

342 lines
8.7 KiB

  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. // This file contains the code dealing with package directory trees.
  5. package godoc
  6. import (
  7. "go/doc"
  8. "go/parser"
  9. "go/token"
  10. "log"
  11. "os"
  12. pathpkg "path"
  13. "strings"
  14. )
  15. // Conventional name for directories containing test data.
  16. // Excluded from directory trees.
  17. //
  18. const testdataDirName = "testdata"
  19. type Directory struct {
  20. Depth int
  21. Path string // directory path; includes Name
  22. Name string // directory name
  23. HasPkg bool // true if the directory contains at least one package
  24. Synopsis string // package documentation, if any
  25. Dirs []*Directory // subdirectories
  26. }
  27. func isGoFile(fi os.FileInfo) bool {
  28. name := fi.Name()
  29. return !fi.IsDir() &&
  30. len(name) > 0 && name[0] != '.' && // ignore .files
  31. pathpkg.Ext(name) == ".go"
  32. }
  33. func isPkgFile(fi os.FileInfo) bool {
  34. return isGoFile(fi) &&
  35. !strings.HasSuffix(fi.Name(), "_test.go") // ignore test files
  36. }
  37. func isPkgDir(fi os.FileInfo) bool {
  38. name := fi.Name()
  39. return fi.IsDir() && len(name) > 0 &&
  40. name[0] != '_' && name[0] != '.' // ignore _files and .files
  41. }
  42. type treeBuilder struct {
  43. c *Corpus
  44. maxDepth int
  45. }
  46. // ioGate is a semaphore controlling VFS activity (ReadDir, parseFile, etc).
  47. // Send before an operation and receive after.
  48. var ioGate = make(chan bool, 20)
  49. func (b *treeBuilder) newDirTree(fset *token.FileSet, path, name string, depth int) *Directory {
  50. if name == testdataDirName {
  51. return nil
  52. }
  53. if depth >= b.maxDepth {
  54. // return a dummy directory so that the parent directory
  55. // doesn't get discarded just because we reached the max
  56. // directory depth
  57. return &Directory{
  58. Depth: depth,
  59. Path: path,
  60. Name: name,
  61. }
  62. }
  63. var synopses [3]string // prioritized package documentation (0 == highest priority)
  64. show := true // show in package listing
  65. hasPkgFiles := false
  66. haveSummary := false
  67. if hook := b.c.SummarizePackage; hook != nil {
  68. if summary, show0, ok := hook(strings.TrimPrefix(path, "/src/")); ok {
  69. hasPkgFiles = true
  70. show = show0
  71. synopses[0] = summary
  72. haveSummary = true
  73. }
  74. }
  75. ioGate <- true
  76. list, err := b.c.fs.ReadDir(path)
  77. <-ioGate
  78. if err != nil {
  79. // TODO: propagate more. See golang.org/issue/14252.
  80. // For now:
  81. if b.c.Verbose {
  82. log.Printf("newDirTree reading %s: %v", path, err)
  83. }
  84. }
  85. // determine number of subdirectories and if there are package files
  86. var dirchs []chan *Directory
  87. for _, d := range list {
  88. filename := pathpkg.Join(path, d.Name())
  89. switch {
  90. case isPkgDir(d):
  91. ch := make(chan *Directory, 1)
  92. dirchs = append(dirchs, ch)
  93. name := d.Name()
  94. go func() {
  95. ch <- b.newDirTree(fset, filename, name, depth+1)
  96. }()
  97. case !haveSummary && isPkgFile(d):
  98. // looks like a package file, but may just be a file ending in ".go";
  99. // don't just count it yet (otherwise we may end up with hasPkgFiles even
  100. // though the directory doesn't contain any real package files - was bug)
  101. // no "optimal" package synopsis yet; continue to collect synopses
  102. ioGate <- true
  103. const flags = parser.ParseComments | parser.PackageClauseOnly
  104. file, err := b.c.parseFile(fset, filename, flags)
  105. <-ioGate
  106. if err != nil {
  107. if b.c.Verbose {
  108. log.Printf("Error parsing %v: %v", filename, err)
  109. }
  110. break
  111. }
  112. hasPkgFiles = true
  113. if file.Doc != nil {
  114. // prioritize documentation
  115. i := -1
  116. switch file.Name.Name {
  117. case name:
  118. i = 0 // normal case: directory name matches package name
  119. case "main":
  120. i = 1 // directory contains a main package
  121. default:
  122. i = 2 // none of the above
  123. }
  124. if 0 <= i && i < len(synopses) && synopses[i] == "" {
  125. synopses[i] = doc.Synopsis(file.Doc.Text())
  126. }
  127. }
  128. haveSummary = synopses[0] != ""
  129. }
  130. }
  131. // create subdirectory tree
  132. var dirs []*Directory
  133. for _, ch := range dirchs {
  134. if d := <-ch; d != nil {
  135. dirs = append(dirs, d)
  136. }
  137. }
  138. // if there are no package files and no subdirectories
  139. // containing package files, ignore the directory
  140. if !hasPkgFiles && len(dirs) == 0 {
  141. return nil
  142. }
  143. // select the highest-priority synopsis for the directory entry, if any
  144. synopsis := ""
  145. for _, synopsis = range synopses {
  146. if synopsis != "" {
  147. break
  148. }
  149. }
  150. return &Directory{
  151. Depth: depth,
  152. Path: path,
  153. Name: name,
  154. HasPkg: hasPkgFiles && show, // TODO(bradfitz): add proper Hide field?
  155. Synopsis: synopsis,
  156. Dirs: dirs,
  157. }
  158. }
  159. // newDirectory creates a new package directory tree with at most maxDepth
  160. // levels, anchored at root. The result tree is pruned such that it only
  161. // contains directories that contain package files or that contain
  162. // subdirectories containing package files (transitively). If a non-nil
  163. // pathFilter is provided, directory paths additionally must be accepted
  164. // by the filter (i.e., pathFilter(path) must be true). If a value >= 0 is
  165. // provided for maxDepth, nodes at larger depths are pruned as well; they
  166. // are assumed to contain package files even if their contents are not known
  167. // (i.e., in this case the tree may contain directories w/o any package files).
  168. //
  169. func (c *Corpus) newDirectory(root string, maxDepth int) *Directory {
  170. // The root could be a symbolic link so use Stat not Lstat.
  171. d, err := c.fs.Stat(root)
  172. // If we fail here, report detailed error messages; otherwise
  173. // is is hard to see why a directory tree was not built.
  174. switch {
  175. case err != nil:
  176. log.Printf("newDirectory(%s): %s", root, err)
  177. return nil
  178. case root != "/" && !isPkgDir(d):
  179. log.Printf("newDirectory(%s): not a package directory", root)
  180. return nil
  181. case root == "/" && !d.IsDir():
  182. log.Printf("newDirectory(%s): not a directory", root)
  183. return nil
  184. }
  185. if maxDepth < 0 {
  186. maxDepth = 1e6 // "infinity"
  187. }
  188. b := treeBuilder{c, maxDepth}
  189. // the file set provided is only for local parsing, no position
  190. // information escapes and thus we don't need to save the set
  191. return b.newDirTree(token.NewFileSet(), root, d.Name(), 0)
  192. }
  193. func (dir *Directory) walk(c chan<- *Directory, skipRoot bool) {
  194. if dir != nil {
  195. if !skipRoot {
  196. c <- dir
  197. }
  198. for _, d := range dir.Dirs {
  199. d.walk(c, false)
  200. }
  201. }
  202. }
  203. func (dir *Directory) iter(skipRoot bool) <-chan *Directory {
  204. c := make(chan *Directory)
  205. go func() {
  206. dir.walk(c, skipRoot)
  207. close(c)
  208. }()
  209. return c
  210. }
  211. func (dir *Directory) lookupLocal(name string) *Directory {
  212. for _, d := range dir.Dirs {
  213. if d.Name == name {
  214. return d
  215. }
  216. }
  217. return nil
  218. }
  219. func splitPath(p string) []string {
  220. p = strings.TrimPrefix(p, "/")
  221. if p == "" {
  222. return nil
  223. }
  224. return strings.Split(p, "/")
  225. }
  226. // lookup looks for the *Directory for a given path, relative to dir.
  227. func (dir *Directory) lookup(path string) *Directory {
  228. d := splitPath(dir.Path)
  229. p := splitPath(path)
  230. i := 0
  231. for i < len(d) {
  232. if i >= len(p) || d[i] != p[i] {
  233. return nil
  234. }
  235. i++
  236. }
  237. for dir != nil && i < len(p) {
  238. dir = dir.lookupLocal(p[i])
  239. i++
  240. }
  241. return dir
  242. }
  243. // DirEntry describes a directory entry. The Depth and Height values
  244. // are useful for presenting an entry in an indented fashion.
  245. //
  246. type DirEntry struct {
  247. Depth int // >= 0
  248. Height int // = DirList.MaxHeight - Depth, > 0
  249. Path string // directory path; includes Name, relative to DirList root
  250. Name string // directory name
  251. HasPkg bool // true if the directory contains at least one package
  252. Synopsis string // package documentation, if any
  253. }
  254. type DirList struct {
  255. MaxHeight int // directory tree height, > 0
  256. List []DirEntry
  257. }
  258. // listing creates a (linear) directory listing from a directory tree.
  259. // If skipRoot is set, the root directory itself is excluded from the list.
  260. // If filter is set, only the directory entries whose paths match the filter
  261. // are included.
  262. //
  263. func (root *Directory) listing(skipRoot bool, filter func(string) bool) *DirList {
  264. if root == nil {
  265. return nil
  266. }
  267. // determine number of entries n and maximum height
  268. n := 0
  269. minDepth := 1 << 30 // infinity
  270. maxDepth := 0
  271. for d := range root.iter(skipRoot) {
  272. n++
  273. if minDepth > d.Depth {
  274. minDepth = d.Depth
  275. }
  276. if maxDepth < d.Depth {
  277. maxDepth = d.Depth
  278. }
  279. }
  280. maxHeight := maxDepth - minDepth + 1
  281. if n == 0 {
  282. return nil
  283. }
  284. // create list
  285. list := make([]DirEntry, 0, n)
  286. for d := range root.iter(skipRoot) {
  287. if filter != nil && !filter(d.Path) {
  288. continue
  289. }
  290. var p DirEntry
  291. p.Depth = d.Depth - minDepth
  292. p.Height = maxHeight - p.Depth
  293. // the path is relative to root.Path - remove the root.Path
  294. // prefix (the prefix should always be present but avoid
  295. // crashes and check)
  296. path := strings.TrimPrefix(d.Path, root.Path)
  297. // remove leading separator if any - path must be relative
  298. path = strings.TrimPrefix(path, "/")
  299. p.Path = path
  300. p.Name = d.Name
  301. p.HasPkg = d.HasPkg
  302. p.Synopsis = d.Synopsis
  303. list = append(list, p)
  304. }
  305. return &DirList{maxHeight, list}
  306. }