// 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 main import ( "go/doc" "go/parser" "io/ioutil" "os" pathutil "path" "strings" "unicode" ) type Directory struct { Depth int Path string // includes Name Name string Text string // package documentation, if any Dirs []*Directory // subdirectories } func isGoFile(f *os.FileInfo) bool { return f.IsRegular() && !strings.HasPrefix(f.Name, ".") && // ignore .files pathutil.Ext(f.Name) == ".go" } func isPkgFile(f *os.FileInfo) bool { return isGoFile(f) && !strings.HasSuffix(f.Name, "_test.go") // ignore test files } func isPkgDir(f *os.FileInfo) bool { return f.IsDirectory() && len(f.Name) > 0 && f.Name[0] != '_' } func firstSentence(s string) string { i := -1 // index+1 of first terminator (punctuation ending a sentence) j := -1 // index+1 of first terminator followed by white space prev := 'A' for k, ch := range s { k1 := k + 1 if ch == '.' || ch == '!' || ch == '?' { if i < 0 { i = k1 // first terminator } if k1 < len(s) && s[k1] <= ' ' { if j < 0 { j = k1 // first terminator followed by white space } if !unicode.IsUpper(prev) { j = k1 break } } } prev = ch } if j < 0 { // use the next best terminator j = i if j < 0 { // no terminator at all, use the entire string j = len(s) } } return s[0:j] } func newDirTree(path, name string, depth, maxDepth int) *Directory { if depth >= 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, path, name, "", nil} } list, _ := ioutil.ReadDir(path) // ignore errors // determine number of subdirectories and package files ndirs := 0 nfiles := 0 var synopses [4]string // prioritized package documentation (0 == highest priority) for _, d := range list { switch { case isPkgDir(d): ndirs++ case isPkgFile(d): nfiles++ if synopses[0] == "" { // no "optimal" package synopsis yet; continue to collect synopses file, err := parser.ParseFile(pathutil.Join(path, d.Name), nil, parser.ParseComments|parser.PackageClauseOnly) if err == nil && file.Doc != nil { // prioritize documentation i := -1 switch file.Name.Name { case name: i = 0 // normal case: directory name matches package name case fakePkgName: i = 1 // synopses for commands case "main": i = 2 // directory contains a main package default: i = 3 // none of the above } if 0 <= i && i < len(synopses) && synopses[i] == "" { synopses[i] = firstSentence(doc.CommentText(file.Doc)) } } } } } // create subdirectory tree var dirs []*Directory if ndirs > 0 { dirs = make([]*Directory, ndirs) i := 0 for _, d := range list { if isPkgDir(d) { dd := newDirTree(pathutil.Join(path, d.Name), d.Name, depth+1, maxDepth) if dd != nil { dirs[i] = dd i++ } } } dirs = dirs[0:i] } // if there are no package files and no subdirectories // (with package files), ignore the directory if nfiles == 0 && 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, path, name, synopsis, 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 maxDepth is // too shallow, the leaf nodes 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 newDirectory(root string, maxDepth int) *Directory { d, err := os.Lstat(root) if err != nil || !isPkgDir(d) { return nil } return newDirTree(root, d.Name, 0, maxDepth) } 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 } // lookup looks for the *Directory for a given path, relative to dir. func (dir *Directory) lookup(path string) *Directory { d := strings.Split(dir.Path, "/", -1) p := strings.Split(path, "/", -1) 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 // includes Name, relative to DirList root Name string Synopsis string } type DirList struct { MaxHeight int // directory tree height, > 0 List []DirEntry } // listing creates a (linear) directory listing from a directory tree. // If skipRoot is set, the root directory itself is excluded from the list. // func (root *Directory) listing(skipRoot bool) *DirList { if root == nil { return nil } // determine number of entries n and maximum height n := 0 minDepth := 1 << 30 // infinity maxDepth := 0 for d := range root.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, n) i := 0 for d := range root.iter(skipRoot) { p := &list[i] 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 := d.Path if strings.HasPrefix(d.Path, root.Path) { path = d.Path[len(root.Path):] } // remove trailing '/' if any - path must be relative if len(path) > 0 && path[0] == '/' { path = path[1:] } p.Path = path p.Name = d.Name p.Synopsis = d.Text i++ } return &DirList{maxHeight, list} }