blob: 0e175ca06f8a04e3d437bc142f036f0b19778ca4 [file] [log] [blame]
// Copyright 2023 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 analysisutil
import (
"fmt"
"go/parser"
"go/token"
"strings"
)
// MustExtractDoc is like [ExtractDoc] but it panics on error.
//
// To use, define a doc.go file such as:
//
// // Package halting defines an analyzer of program termination.
// //
// // # Analyzer halting
// //
// // halting: reports whether execution will halt.
// //
// // The halting analyzer reports a diagnostic for functions
// // that run forever. To suppress the diagnostics, try inserting
// // a 'break' statement into each loop.
// package halting
//
// import _ "embed"
//
// //go:embed doc.go
// var doc string
//
// And declare your analyzer as:
//
// var Analyzer = &analysis.Analyzer{
// Name: "halting",
// Doc: analysisutil.MustExtractDoc(doc, "halting"),
// ...
// }
func MustExtractDoc(content, name string) string {
doc, err := ExtractDoc(content, name)
if err != nil {
panic(err)
}
return doc
}
// ExtractDoc extracts a section of a package doc comment from the
// provided contents of an analyzer package's doc.go file.
//
// A section is a portion of the comment between one heading and
// the next, using this form:
//
// # Analyzer NAME
//
// NAME: SUMMARY
//
// Full description...
//
// where NAME matches the name argument, and SUMMARY is a brief
// verb-phrase that describes the analyzer. The following lines, up
// until the next heading or the end of the comment, contain the full
// description. ExtractDoc returns the portion following the colon,
// which is the form expected by Analyzer.Doc.
//
// Example:
//
// # Analyzer printf
//
// printf: checks consistency of calls to printf
//
// The printf analyzer checks consistency of calls to printf.
// Here is the complete description...
//
// This notation allows a single doc comment to provide documentation
// for multiple analyzers, each in its own section.
// The HTML anchors generated for each heading are predictable.
//
// It returns an error if the content was not a valid Go source file
// containing a package doc comment with a heading of the required
// form.
//
// This machinery enables the package documentation (typically
// accessible via the web at https://pkg.go.dev/) and the command
// documentation (typically printed to a terminal) to be derived from
// the same source and formatted appropriately.
func ExtractDoc(content, name string) (string, error) {
if content == "" {
return "", fmt.Errorf("empty Go source file")
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", content, parser.ParseComments|parser.PackageClauseOnly)
if err != nil {
return "", fmt.Errorf("not a Go source file")
}
if f.Doc == nil {
return "", fmt.Errorf("Go source file has no package doc comment")
}
for _, section := range strings.Split(f.Doc.Text(), "\n# ") {
if body := strings.TrimPrefix(section, "Analyzer "+name); body != section &&
body != "" &&
body[0] == '\r' || body[0] == '\n' {
body = strings.TrimSpace(body)
rest := strings.TrimPrefix(body, name+":")
if rest == body {
return "", fmt.Errorf("'Analyzer %s' heading not followed by '%s: summary...' line", name, name)
}
return strings.TrimSpace(rest), nil
}
}
return "", fmt.Errorf("package doc comment contains no 'Analyzer %s' heading", name)
}