1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package analysisinternal 6 7 import ( 8 "fmt" 9 "go/parser" 10 "go/token" 11 "strings" 12 ) 13 14 // MustExtractDoc is like [ExtractDoc] but it panics on error. 15 // 16 // To use, define a doc.go file such as: 17 // 18 // // Package halting defines an analyzer of program termination. 19 // // 20 // // # Analyzer halting 21 // // 22 // // halting: reports whether execution will halt. 23 // // 24 // // The halting analyzer reports a diagnostic for functions 25 // // that run forever. To suppress the diagnostics, try inserting 26 // // a 'break' statement into each loop. 27 // package halting 28 // 29 // import _ "embed" 30 // 31 // //go:embed doc.go 32 // var doc string 33 // 34 // And declare your analyzer as: 35 // 36 // var Analyzer = &analysis.Analyzer{ 37 // Name: "halting", 38 // Doc: analysisutil.MustExtractDoc(doc, "halting"), 39 // ... 40 // } 41 func MustExtractDoc(content, name string) string { 42 doc, err := ExtractDoc(content, name) 43 if err != nil { 44 panic(err) 45 } 46 return doc 47 } 48 49 // ExtractDoc extracts a section of a package doc comment from the 50 // provided contents of an analyzer package's doc.go file. 51 // 52 // A section is a portion of the comment between one heading and 53 // the next, using this form: 54 // 55 // # Analyzer NAME 56 // 57 // NAME: SUMMARY 58 // 59 // Full description... 60 // 61 // where NAME matches the name argument, and SUMMARY is a brief 62 // verb-phrase that describes the analyzer. The following lines, up 63 // until the next heading or the end of the comment, contain the full 64 // description. ExtractDoc returns the portion following the colon, 65 // which is the form expected by Analyzer.Doc. 66 // 67 // Example: 68 // 69 // # Analyzer printf 70 // 71 // printf: checks consistency of calls to printf 72 // 73 // The printf analyzer checks consistency of calls to printf. 74 // Here is the complete description... 75 // 76 // This notation allows a single doc comment to provide documentation 77 // for multiple analyzers, each in its own section. 78 // The HTML anchors generated for each heading are predictable. 79 // 80 // It returns an error if the content was not a valid Go source file 81 // containing a package doc comment with a heading of the required 82 // form. 83 // 84 // This machinery enables the package documentation (typically 85 // accessible via the web at https://pkg.go.dev/) and the command 86 // documentation (typically printed to a terminal) to be derived from 87 // the same source and formatted appropriately. 88 func ExtractDoc(content, name string) (string, error) { 89 if content == "" { 90 return "", fmt.Errorf("empty Go source file") 91 } 92 fset := token.NewFileSet() 93 f, err := parser.ParseFile(fset, "", content, parser.ParseComments|parser.PackageClauseOnly) 94 if err != nil { 95 return "", fmt.Errorf("not a Go source file") 96 } 97 if f.Doc == nil { 98 return "", fmt.Errorf("Go source file has no package doc comment") 99 } 100 for _, section := range strings.Split(f.Doc.Text(), "\n# ") { 101 if body := strings.TrimPrefix(section, "Analyzer "+name); body != section && 102 body != "" && 103 body[0] == '\r' || body[0] == '\n' { 104 body = strings.TrimSpace(body) 105 rest := strings.TrimPrefix(body, name+":") 106 if rest == body { 107 return "", fmt.Errorf("'Analyzer %s' heading not followed by '%s: summary...' line", name, name) 108 } 109 return strings.TrimSpace(rest), nil 110 } 111 } 112 return "", fmt.Errorf("package doc comment contains no 'Analyzer %s' heading", name) 113 } 114