| // 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 pkgdoc |
| |
| import ( |
| "bytes" |
| "go/ast" |
| "go/doc" |
| "go/parser" |
| "go/token" |
| "io/fs" |
| "log" |
| "path" |
| "strings" |
| ) |
| |
| type Dir struct { |
| Path string // directory path |
| HasPkg bool // true if the directory contains at least one package |
| Synopsis string // package documentation, if any |
| Dirs []*Dir // subdirectories |
| } |
| |
| func (d *Dir) Name() string { |
| return path.Base(d.Path) |
| } |
| |
| // DirEntry describes a directory entry. |
| // The Depth gives the directory depth relative to the overall list, |
| // for use in presenting a hierarchical directory entry. |
| type DirEntry struct { |
| Depth int // >= 0 |
| Path string // relative path to directory from listing start |
| HasPkg bool // true if the directory contains at least one package |
| Synopsis string // package documentation, if any |
| } |
| |
| func (d *DirEntry) Name() string { |
| return path.Base(d.Path) |
| } |
| |
| // lookup looks for the *Dir for a given named path, relative to d. |
| func (d *Dir) lookup(name string) *Dir { |
| name = path.Join(d.Path, name) |
| if name == d.Path { |
| return d |
| } |
| if d.Path != "." { |
| if !strings.HasPrefix(name, d.Path) || name[len(d.Path)] != '/' { |
| return nil |
| } |
| name = name[len(d.Path)+1:] |
| } |
| Walk: |
| for i := 0; i <= len(name); i++ { |
| if i == len(name) || name[i] == '/' { |
| // Find next child along path. |
| for _, sub := range d.Dirs { |
| if sub.Path == name[:i] { |
| d = sub |
| continue Walk |
| } |
| } |
| return nil |
| } |
| } |
| return d |
| } |
| |
| // list creates a (linear) directory list from a directory tree. |
| // If filter is set, only the directory entries whose paths match the filter |
| // are included. |
| func (d *Dir) list(filter func(string) bool) []DirEntry { |
| if d == nil { |
| return nil |
| } |
| |
| root := d |
| var list []DirEntry |
| root.walk(func(d *Dir, depth int) { |
| if depth == 0 || filter != nil && !filter(d.Path) { |
| return |
| } |
| // the path is relative to root.Path - remove the root.Path |
| // prefix (the prefix should always be present but avoid |
| // crashes and check) |
| path := strings.TrimPrefix(d.Path, root.Path) |
| // remove leading separator if any - path must be relative |
| path = strings.TrimPrefix(path, "/") |
| list = append(list, DirEntry{ |
| Depth: depth, |
| Path: path, |
| HasPkg: d.HasPkg, |
| Synopsis: d.Synopsis, |
| }) |
| }) |
| |
| if len(list) == 0 { |
| return nil |
| } |
| return list |
| } |
| |
| // newDir returns a Dir describing dirpath in fsys. |
| // When Go files need to be parsed, newDir uses fset. |
| // If there are no package files and no subdirectories containing packages, |
| // newDir returns nil. |
| func newDir(fsys fs.FS, fset *token.FileSet, dirpath string) *Dir { |
| var synopses [3]string // prioritized package documentation (0 == highest priority) |
| |
| hasPkgFiles := false |
| haveSummary := false |
| |
| list, err := fs.ReadDir(fsys, dirpath) |
| if err != nil { |
| // TODO: propagate more. See golang.org/issue/14252. |
| log.Printf("newDirTree reading %s: %v", dirpath, err) |
| } |
| |
| // determine number of subdirectories and if there are package files |
| var dirs []*Dir |
| |
| for _, de := range list { |
| filename := path.Join(dirpath, de.Name()) |
| switch { |
| case isPkgDir(de): |
| d := newDir(fsys, fset, filename) |
| if d != nil { |
| dirs = append(dirs, d) |
| } |
| |
| case !haveSummary && isPkgFile(de): |
| // looks like a package file, but may just be a file ending in ".go"; |
| // don't just count it yet (otherwise we may end up with hasPkgFiles even |
| // though the directory doesn't contain any real package files - was bug) |
| // no "optimal" package synopsis yet; continue to collect synopses |
| const flags = parser.ParseComments | parser.PackageClauseOnly |
| file, err := parseFile(fsys, fset, filename, flags) |
| if err != nil { |
| log.Printf("parsing %v: %v", filename, err) |
| break |
| } |
| |
| hasPkgFiles = true |
| if file.Doc != nil { |
| // prioritize documentation |
| i := -1 |
| switch file.Name.Name { |
| case path.Base(dirpath): |
| i = 0 // normal case: directory name matches package name |
| case "main": |
| i = 1 // directory contains a main package |
| default: |
| i = 2 // none of the above |
| } |
| if 0 <= i && i < len(synopses) && synopses[i] == "" { |
| synopses[i] = doc.Synopsis(file.Doc.Text()) |
| } |
| } |
| haveSummary = synopses[0] != "" |
| } |
| } |
| |
| // if there are no package files and no subdirectories |
| // containing package files, ignore the directory |
| if !hasPkgFiles && 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 &Dir{ |
| Path: dirpath, |
| HasPkg: hasPkgFiles, |
| Synopsis: synopsis, |
| Dirs: dirs, |
| } |
| } |
| |
| func isPkgFile(fi fs.DirEntry) bool { |
| name := fi.Name() |
| return !fi.IsDir() && |
| path.Ext(name) == ".go" && |
| !strings.HasSuffix(fi.Name(), "_test.go") // ignore test files |
| } |
| |
| func isPkgDir(fi fs.DirEntry) bool { |
| name := fi.Name() |
| return fi.IsDir() && |
| name != "testdata" && |
| len(name) > 0 && name[0] != '_' && name[0] != '.' // ignore _files and .files |
| } |
| |
| // walk calls f for each directory in the tree rooted at d, including d itself. |
| // The depth argument specifies the depth of the directory in the tree. |
| // The depth of d itself is 0. |
| func (d *Dir) walk(f func(d *Dir, depth int)) { |
| walkDirs(f, d, 0) |
| } |
| |
| func walkDirs(f func(d *Dir, depth int), d *Dir, depth int) { |
| f(d, depth) |
| for _, sub := range d.Dirs { |
| walkDirs(f, sub, depth+1) |
| } |
| } |
| |
| func parseFile(fsys fs.FS, fset *token.FileSet, filename string, mode parser.Mode) (*ast.File, error) { |
| src, err := fs.ReadFile(fsys, filename) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Temporary ad-hoc fix for issue 5247. |
| // TODO(gri,dmitshur) Remove this in favor of a better fix, eventually (see issue 32092). |
| replaceLinePrefixCommentsWithBlankLine(src) |
| |
| return parser.ParseFile(fset, filename, src, mode) |
| } |
| |
| func parseFiles(fsys fs.FS, fset *token.FileSet, dirname string, localnames []string) (map[string]*ast.File, error) { |
| files := make(map[string]*ast.File) |
| for _, f := range localnames { |
| filename := path.Join(dirname, f) |
| file, err := parseFile(fsys, fset, filename, parser.ParseComments) |
| if err != nil { |
| return nil, err |
| } |
| files[filename] = file |
| } |
| |
| return files, nil |
| } |
| |
| var linePrefix = []byte("//line ") |
| |
| // This function replaces source lines starting with "//line " with a blank line. |
| // It does this irrespective of whether the line is truly a line comment or not; |
| // e.g., the line may be inside a string, or a /*-style comment; however that is |
| // rather unlikely (proper testing would require a full Go scan which we want to |
| // avoid for performance). |
| func replaceLinePrefixCommentsWithBlankLine(src []byte) { |
| for { |
| i := bytes.Index(src, linePrefix) |
| if i < 0 { |
| break // we're done |
| } |
| // 0 <= i && i+len(linePrefix) <= len(src) |
| if i == 0 || src[i-1] == '\n' { |
| // at beginning of line: blank out line |
| for i < len(src) && src[i] != '\n' { |
| src[i] = ' ' |
| i++ |
| } |
| } else { |
| // not at beginning of line: skip over prefix |
| i += len(linePrefix) |
| } |
| // i <= len(src) |
| src = src[i:] |
| } |
| } |