// 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)) }