lint.go raw
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.Split(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.Split(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 interface{}) {
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 }
222