mirror of https://github.com/golang/go.git
355 lines
11 KiB
Go
355 lines
11 KiB
Go
// Copyright 2009 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.
|
|
|
|
// Package doc extracts source code documentation from a Go AST.
|
|
package doc
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/doc/comment"
|
|
"go/token"
|
|
"strings"
|
|
)
|
|
|
|
// Package is the documentation for an entire package.
|
|
type Package struct {
|
|
Doc string
|
|
Name string
|
|
ImportPath string
|
|
Imports []string
|
|
Filenames []string
|
|
Notes map[string][]*Note
|
|
|
|
// Deprecated: For backward compatibility Bugs is still populated,
|
|
// but all new code should use Notes instead.
|
|
Bugs []string
|
|
|
|
// declarations
|
|
Consts []*Value
|
|
Types []*Type
|
|
Vars []*Value
|
|
Funcs []*Func
|
|
|
|
// Examples is a sorted list of examples associated with
|
|
// the package. Examples are extracted from _test.go files
|
|
// provided to NewFromFiles.
|
|
Examples []*Example
|
|
|
|
importByName map[string]string
|
|
syms map[string]bool
|
|
}
|
|
|
|
// Value is the documentation for a (possibly grouped) var or const declaration.
|
|
type Value struct {
|
|
Doc string
|
|
Names []string // var or const names in declaration order
|
|
Decl *ast.GenDecl
|
|
|
|
order int
|
|
}
|
|
|
|
// Type is the documentation for a type declaration.
|
|
type Type struct {
|
|
Doc string
|
|
Name string
|
|
Decl *ast.GenDecl
|
|
|
|
// associated declarations
|
|
Consts []*Value // sorted list of constants of (mostly) this type
|
|
Vars []*Value // sorted list of variables of (mostly) this type
|
|
Funcs []*Func // sorted list of functions returning this type
|
|
Methods []*Func // sorted list of methods (including embedded ones) of this type
|
|
|
|
// Examples is a sorted list of examples associated with
|
|
// this type. Examples are extracted from _test.go files
|
|
// provided to NewFromFiles.
|
|
Examples []*Example
|
|
}
|
|
|
|
// Func is the documentation for a func declaration.
|
|
type Func struct {
|
|
Doc string
|
|
Name string
|
|
Decl *ast.FuncDecl
|
|
|
|
// methods
|
|
// (for functions, these fields have the respective zero value)
|
|
Recv string // actual receiver "T" or "*T" possibly followed by type parameters [P1, ..., Pn]
|
|
Orig string // original receiver "T" or "*T"
|
|
Level int // embedding level; 0 means not embedded
|
|
|
|
// Examples is a sorted list of examples associated with this
|
|
// function or method. Examples are extracted from _test.go files
|
|
// provided to NewFromFiles.
|
|
Examples []*Example
|
|
}
|
|
|
|
// A Note represents a marked comment starting with "MARKER(uid): note body".
|
|
// Any note with a marker of 2 or more upper case [A-Z] letters and a uid of
|
|
// at least one character is recognized. The ":" following the uid is optional.
|
|
// Notes are collected in the Package.Notes map indexed by the notes marker.
|
|
type Note struct {
|
|
Pos, End token.Pos // position range of the comment containing the marker
|
|
UID string // uid found with the marker
|
|
Body string // note body text
|
|
}
|
|
|
|
// Mode values control the operation of [New] and [NewFromFiles].
|
|
type Mode int
|
|
|
|
const (
|
|
// AllDecls says to extract documentation for all package-level
|
|
// declarations, not just exported ones.
|
|
AllDecls Mode = 1 << iota
|
|
|
|
// AllMethods says to show all embedded methods, not just the ones of
|
|
// invisible (unexported) anonymous fields.
|
|
AllMethods
|
|
|
|
// PreserveAST says to leave the AST unmodified. Originally, pieces of
|
|
// the AST such as function bodies were nil-ed out to save memory in
|
|
// godoc, but not all programs want that behavior.
|
|
PreserveAST
|
|
)
|
|
|
|
// New computes the package documentation for the given package AST.
|
|
// New takes ownership of the AST pkg and may edit or overwrite it.
|
|
// To have the [Examples] fields populated, use [NewFromFiles] and include
|
|
// the package's _test.go files.
|
|
func New(pkg *ast.Package, importPath string, mode Mode) *Package {
|
|
var r reader
|
|
r.readPackage(pkg, mode)
|
|
r.computeMethodSets()
|
|
r.cleanupTypes()
|
|
p := &Package{
|
|
Doc: r.doc,
|
|
Name: pkg.Name,
|
|
ImportPath: importPath,
|
|
Imports: sortedKeys(r.imports),
|
|
Filenames: r.filenames,
|
|
Notes: r.notes,
|
|
Bugs: noteBodies(r.notes["BUG"]),
|
|
Consts: sortedValues(r.values, token.CONST),
|
|
Types: sortedTypes(r.types, mode&AllMethods != 0),
|
|
Vars: sortedValues(r.values, token.VAR),
|
|
Funcs: sortedFuncs(r.funcs, true),
|
|
|
|
importByName: r.importByName,
|
|
syms: make(map[string]bool),
|
|
}
|
|
|
|
p.collectValues(p.Consts)
|
|
p.collectValues(p.Vars)
|
|
p.collectTypes(p.Types)
|
|
p.collectFuncs(p.Funcs)
|
|
|
|
return p
|
|
}
|
|
|
|
func (p *Package) collectValues(values []*Value) {
|
|
for _, v := range values {
|
|
for _, name := range v.Names {
|
|
p.syms[name] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Package) collectTypes(types []*Type) {
|
|
for _, t := range types {
|
|
if p.syms[t.Name] {
|
|
// Shouldn't be any cycles but stop just in case.
|
|
continue
|
|
}
|
|
p.syms[t.Name] = true
|
|
p.collectValues(t.Consts)
|
|
p.collectValues(t.Vars)
|
|
p.collectFuncs(t.Funcs)
|
|
p.collectFuncs(t.Methods)
|
|
}
|
|
}
|
|
|
|
func (p *Package) collectFuncs(funcs []*Func) {
|
|
for _, f := range funcs {
|
|
if f.Recv != "" {
|
|
r := strings.TrimPrefix(f.Recv, "*")
|
|
if i := strings.IndexByte(r, '['); i >= 0 {
|
|
r = r[:i] // remove type parameters
|
|
}
|
|
p.syms[r+"."+f.Name] = true
|
|
} else {
|
|
p.syms[f.Name] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// NewFromFiles computes documentation for a package.
|
|
//
|
|
// The package is specified by a list of *ast.Files and corresponding
|
|
// file set, which must not be nil.
|
|
// NewFromFiles uses all provided files when computing documentation,
|
|
// so it is the caller's responsibility to provide only the files that
|
|
// match the desired build context. "go/build".Context.MatchFile can
|
|
// be used for determining whether a file matches a build context with
|
|
// the desired GOOS and GOARCH values, and other build constraints.
|
|
// The import path of the package is specified by importPath.
|
|
//
|
|
// Examples found in _test.go files are associated with the corresponding
|
|
// type, function, method, or the package, based on their name.
|
|
// If the example has a suffix in its name, it is set in the
|
|
// [Example.Suffix] field. [Examples] with malformed names are skipped.
|
|
//
|
|
// Optionally, a single extra argument of type [Mode] can be provided to
|
|
// control low-level aspects of the documentation extraction behavior.
|
|
//
|
|
// NewFromFiles takes ownership of the AST files and may edit them,
|
|
// unless the PreserveAST Mode bit is on.
|
|
func NewFromFiles(fset *token.FileSet, files []*ast.File, importPath string, opts ...any) (*Package, error) {
|
|
// Check for invalid API usage.
|
|
if fset == nil {
|
|
panic(fmt.Errorf("doc.NewFromFiles: no token.FileSet provided (fset == nil)"))
|
|
}
|
|
var mode Mode
|
|
switch len(opts) { // There can only be 0 or 1 options, so a simple switch works for now.
|
|
case 0:
|
|
// Nothing to do.
|
|
case 1:
|
|
m, ok := opts[0].(Mode)
|
|
if !ok {
|
|
panic(fmt.Errorf("doc.NewFromFiles: option argument type must be doc.Mode"))
|
|
}
|
|
mode = m
|
|
default:
|
|
panic(fmt.Errorf("doc.NewFromFiles: there must not be more than 1 option argument"))
|
|
}
|
|
|
|
// Collect .go and _test.go files.
|
|
var (
|
|
goFiles = make(map[string]*ast.File)
|
|
testGoFiles []*ast.File
|
|
)
|
|
for i := range files {
|
|
f := fset.File(files[i].Pos())
|
|
if f == nil {
|
|
return nil, fmt.Errorf("file files[%d] is not found in the provided file set", i)
|
|
}
|
|
switch name := f.Name(); {
|
|
case strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go"):
|
|
goFiles[name] = files[i]
|
|
case strings.HasSuffix(name, "_test.go"):
|
|
testGoFiles = append(testGoFiles, files[i])
|
|
default:
|
|
return nil, fmt.Errorf("file files[%d] filename %q does not have a .go extension", i, name)
|
|
}
|
|
}
|
|
|
|
// TODO(dmitshur,gri): A relatively high level call to ast.NewPackage with a simpleImporter
|
|
// ast.Importer implementation is made below. It might be possible to short-circuit and simplify.
|
|
|
|
// Compute package documentation.
|
|
pkg, _ := ast.NewPackage(fset, goFiles, simpleImporter, nil) // Ignore errors that can happen due to unresolved identifiers.
|
|
p := New(pkg, importPath, mode)
|
|
classifyExamples(p, Examples(testGoFiles...))
|
|
return p, nil
|
|
}
|
|
|
|
// simpleImporter returns a (dummy) package object named by the last path
|
|
// component of the provided package path (as is the convention for packages).
|
|
// This is sufficient to resolve package identifiers without doing an actual
|
|
// import. It never returns an error.
|
|
func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
|
|
pkg := imports[path]
|
|
if pkg == nil {
|
|
// note that strings.LastIndex returns -1 if there is no "/"
|
|
pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
|
|
pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
|
|
imports[path] = pkg
|
|
}
|
|
return pkg, nil
|
|
}
|
|
|
|
// lookupSym reports whether the package has a given symbol or method.
|
|
//
|
|
// If recv == "", HasSym reports whether the package has a top-level
|
|
// const, func, type, or var named name.
|
|
//
|
|
// If recv != "", HasSym reports whether the package has a type
|
|
// named recv with a method named name.
|
|
func (p *Package) lookupSym(recv, name string) bool {
|
|
if recv != "" {
|
|
return p.syms[recv+"."+name]
|
|
}
|
|
return p.syms[name]
|
|
}
|
|
|
|
// lookupPackage returns the import path identified by name
|
|
// in the given package. If name uniquely identifies a single import,
|
|
// then lookupPackage returns that import.
|
|
// If multiple packages are imported as name, importPath returns "", false.
|
|
// Otherwise, if name is the name of p itself, importPath returns "", true,
|
|
// to signal a reference to p.
|
|
// Otherwise, importPath returns "", false.
|
|
func (p *Package) lookupPackage(name string) (importPath string, ok bool) {
|
|
if path, ok := p.importByName[name]; ok {
|
|
if path == "" {
|
|
return "", false // multiple imports used the name
|
|
}
|
|
return path, true // found import
|
|
}
|
|
if p.Name == name {
|
|
return "", true // allow reference to this package
|
|
}
|
|
return "", false // unknown name
|
|
}
|
|
|
|
// Parser returns a doc comment parser configured
|
|
// for parsing doc comments from package p.
|
|
// Each call returns a new parser, so that the caller may
|
|
// customize it before use.
|
|
func (p *Package) Parser() *comment.Parser {
|
|
return &comment.Parser{
|
|
LookupPackage: p.lookupPackage,
|
|
LookupSym: p.lookupSym,
|
|
}
|
|
}
|
|
|
|
// Printer returns a doc comment printer configured
|
|
// for printing doc comments from package p.
|
|
// Each call returns a new printer, so that the caller may
|
|
// customize it before use.
|
|
func (p *Package) Printer() *comment.Printer {
|
|
// No customization today, but having p.Printer()
|
|
// gives us flexibility in the future, and it is convenient for callers.
|
|
return &comment.Printer{}
|
|
}
|
|
|
|
// HTML returns formatted HTML for the doc comment text.
|
|
//
|
|
// To customize details of the HTML, use [Package.Printer]
|
|
// to obtain a [comment.Printer], and configure it
|
|
// before calling its HTML method.
|
|
func (p *Package) HTML(text string) []byte {
|
|
return p.Printer().HTML(p.Parser().Parse(text))
|
|
}
|
|
|
|
// Markdown returns formatted Markdown for the doc comment text.
|
|
//
|
|
// To customize details of the Markdown, use [Package.Printer]
|
|
// to obtain a [comment.Printer], and configure it
|
|
// before calling its Markdown method.
|
|
func (p *Package) Markdown(text string) []byte {
|
|
return p.Printer().Markdown(p.Parser().Parse(text))
|
|
}
|
|
|
|
// Text returns formatted text for the doc comment text,
|
|
// wrapped to 80 Unicode code points and using tabs for
|
|
// code block indentation.
|
|
//
|
|
// To customize details of the formatting, use [Package.Printer]
|
|
// to obtain a [comment.Printer], and configure it
|
|
// before calling its Text method.
|
|
func (p *Package) Text(text string) []byte {
|
|
return p.Printer().Text(p.Parser().Parse(text))
|
|
}
|