lint.go (5308B)
1 // Package lint provides abstractions on top of go/analysis. 2 // These abstractions add extra information to analyzes, such as structured documentation and severities. 3 package lint 4 5 import ( 6 "fmt" 7 "go/ast" 8 "go/token" 9 "strings" 10 11 "golang.org/x/tools/go/analysis" 12 "honnef.co/go/tools/analysis/facts/tokenfile" 13 ) 14 15 // Analyzer wraps a go/analysis.Analyzer and provides structured documentation. 16 type Analyzer struct { 17 // The analyzer's documentation. Unlike go/analysis.Analyzer.Doc, 18 // this field is structured, providing access to severity, options 19 // etc. 20 Doc *RawDocumentation 21 Analyzer *analysis.Analyzer 22 } 23 24 func InitializeAnalyzer(a *Analyzer) *Analyzer { 25 a.Analyzer.Doc = a.Doc.Compile().String() 26 a.Analyzer.URL = "https://staticcheck.dev/docs/checks/#" + a.Analyzer.Name 27 a.Analyzer.Requires = append(a.Analyzer.Requires, tokenfile.Analyzer) 28 return a 29 } 30 31 // Severity describes the severity of diagnostics reported by an analyzer. 32 type Severity int 33 34 const ( 35 SeverityNone Severity = iota 36 SeverityError 37 SeverityDeprecated 38 SeverityWarning 39 SeverityInfo 40 SeverityHint 41 ) 42 43 // MergeStrategy sets how merge mode should behave for diagnostics of an analyzer. 44 type MergeStrategy int 45 46 const ( 47 MergeIfAny MergeStrategy = iota 48 MergeIfAll 49 ) 50 51 type RawDocumentation struct { 52 Title string 53 Text string 54 Before string 55 After string 56 Since string 57 NonDefault bool 58 Options []string 59 Severity Severity 60 MergeIf MergeStrategy 61 } 62 63 type Documentation struct { 64 Title string 65 Text string 66 67 TitleMarkdown string 68 TextMarkdown string 69 70 Before string 71 After string 72 Since string 73 NonDefault bool 74 Options []string 75 Severity Severity 76 MergeIf MergeStrategy 77 } 78 79 func (doc RawDocumentation) Compile() *Documentation { 80 return &Documentation{ 81 Title: strings.TrimSpace(stripMarkdown(doc.Title)), 82 Text: strings.TrimSpace(stripMarkdown(doc.Text)), 83 84 TitleMarkdown: strings.TrimSpace(toMarkdown(doc.Title)), 85 TextMarkdown: strings.TrimSpace(toMarkdown(doc.Text)), 86 87 Before: strings.TrimSpace(doc.Before), 88 After: strings.TrimSpace(doc.After), 89 Since: doc.Since, 90 NonDefault: doc.NonDefault, 91 Options: doc.Options, 92 Severity: doc.Severity, 93 MergeIf: doc.MergeIf, 94 } 95 } 96 97 func toMarkdown(s string) string { 98 return strings.NewReplacer(`\'`, "`", `\"`, "`").Replace(s) 99 } 100 101 func stripMarkdown(s string) string { 102 return strings.NewReplacer(`\'`, "", `\"`, "'").Replace(s) 103 } 104 105 func (doc *Documentation) Format(metadata bool) string { 106 return doc.format(false, metadata) 107 } 108 109 func (doc *Documentation) FormatMarkdown(metadata bool) string { 110 return doc.format(true, metadata) 111 } 112 113 func (doc *Documentation) format(markdown bool, metadata bool) string { 114 b := &strings.Builder{} 115 if markdown { 116 fmt.Fprintf(b, "%s\n\n", doc.TitleMarkdown) 117 if doc.Text != "" { 118 fmt.Fprintf(b, "%s\n\n", doc.TextMarkdown) 119 } 120 } else { 121 fmt.Fprintf(b, "%s\n\n", doc.Title) 122 if doc.Text != "" { 123 fmt.Fprintf(b, "%s\n\n", doc.Text) 124 } 125 } 126 127 if doc.Before != "" { 128 fmt.Fprintln(b, "Before:") 129 fmt.Fprintln(b, "") 130 for line := range strings.SplitSeq(doc.Before, "\n") { 131 fmt.Fprint(b, " ", line, "\n") 132 } 133 fmt.Fprintln(b, "") 134 fmt.Fprintln(b, "After:") 135 fmt.Fprintln(b, "") 136 for line := range strings.SplitSeq(doc.After, "\n") { 137 fmt.Fprint(b, " ", line, "\n") 138 } 139 fmt.Fprintln(b, "") 140 } 141 142 if metadata { 143 fmt.Fprint(b, "Available since\n ") 144 if doc.Since == "" { 145 fmt.Fprint(b, "unreleased") 146 } else { 147 fmt.Fprintf(b, "%s", doc.Since) 148 } 149 if doc.NonDefault { 150 fmt.Fprint(b, ", non-default") 151 } 152 fmt.Fprint(b, "\n") 153 if len(doc.Options) > 0 { 154 fmt.Fprintf(b, "\nOptions\n") 155 for _, opt := range doc.Options { 156 fmt.Fprintf(b, " %s", opt) 157 } 158 fmt.Fprint(b, "\n") 159 } 160 } 161 162 return b.String() 163 } 164 165 func (doc *Documentation) String() string { 166 return doc.Format(true) 167 } 168 169 // ExhaustiveTypeSwitch panics when called. It can be used to ensure 170 // that type switches are exhaustive. 171 func ExhaustiveTypeSwitch(v any) { 172 panic(fmt.Sprintf("internal error: unhandled case %T", v)) 173 } 174 175 // A directive is a comment of the form '//lint:<command> 176 // [arguments...]'. It represents instructions to the static analysis 177 // tool. 178 type Directive struct { 179 Command string 180 Arguments []string 181 Directive *ast.Comment 182 Node ast.Node 183 } 184 185 func parseDirective(s string) (cmd string, args []string) { 186 if !strings.HasPrefix(s, "//lint:") { 187 return "", nil 188 } 189 s = strings.TrimPrefix(s, "//lint:") 190 fields := strings.Split(s, " ") 191 return fields[0], fields[1:] 192 } 193 194 // ParseDirectives extracts all directives from a list of Go files. 195 func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive { 196 var dirs []Directive 197 for _, f := range files { 198 // OPT(dh): in our old code, we skip all the comment map work if we 199 // couldn't find any directives, benchmark if that's actually 200 // worth doing 201 cm := ast.NewCommentMap(fset, f, f.Comments) 202 for node, cgs := range cm { 203 for _, cg := range cgs { 204 for _, c := range cg.List { 205 if !strings.HasPrefix(c.Text, "//lint:") { 206 continue 207 } 208 cmd, args := parseDirective(c.Text) 209 d := Directive{ 210 Command: cmd, 211 Arguments: args, 212 Directive: c, 213 Node: node, 214 } 215 dirs = append(dirs, d) 216 } 217 } 218 } 219 } 220 return dirs 221 }