report.go raw
1 package report
2
3 import (
4 "bytes"
5 "fmt"
6 "go/ast"
7 "go/format"
8 "go/token"
9 "go/version"
10 "path/filepath"
11 "strconv"
12 "strings"
13
14 "honnef.co/go/tools/analysis/code"
15 "honnef.co/go/tools/analysis/facts/generated"
16 "honnef.co/go/tools/go/ast/astutil"
17
18 "golang.org/x/tools/go/analysis"
19 )
20
21 type Options struct {
22 ShortRange bool
23 FilterGenerated bool
24 Fixes []analysis.SuggestedFix
25 Related []analysis.RelatedInformation
26 MinimumLanguageVersion string
27 MaximumLanguageVersion string
28 MinimumStdlibVersion string
29 MaximumStdlibVersion string
30 }
31
32 type Option func(*Options)
33
34 func ShortRange() Option {
35 return func(opts *Options) {
36 opts.ShortRange = true
37 }
38 }
39
40 func FilterGenerated() Option {
41 return func(opts *Options) {
42 opts.FilterGenerated = true
43 }
44 }
45
46 func Fixes(fixes ...analysis.SuggestedFix) Option {
47 return func(opts *Options) {
48 opts.Fixes = append(opts.Fixes, fixes...)
49 }
50 }
51
52 func Related(node Positioner, message string) Option {
53 return func(opts *Options) {
54 pos, end, ok := getRange(node, opts.ShortRange)
55 if !ok {
56 return
57 }
58 r := analysis.RelatedInformation{
59 Pos: pos,
60 End: end,
61 Message: message,
62 }
63 opts.Related = append(opts.Related, r)
64 }
65 }
66
67 func MinimumLanguageVersion(vers string) Option {
68 return func(opts *Options) { opts.MinimumLanguageVersion = vers }
69 }
70 func MaximumLanguageVersion(vers string) Option {
71 return func(opts *Options) { opts.MinimumLanguageVersion = vers }
72 }
73 func MinimumStdlibVersion(vers string) Option {
74 return func(opts *Options) { opts.MinimumStdlibVersion = vers }
75 }
76 func MaximumStdlibVersion(vers string) Option {
77 return func(opts *Options) { opts.MaximumStdlibVersion = vers }
78 }
79
80 type Positioner interface {
81 Pos() token.Pos
82 }
83
84 type fullPositioner interface {
85 Pos() token.Pos
86 End() token.Pos
87 }
88
89 type sourcer interface {
90 Source() ast.Node
91 }
92
93 // shortRange returns the position and end of the main component of an
94 // AST node. For nodes that have no body, the short range is identical
95 // to the node's Pos and End. For nodes that do have a body, the short
96 // range excludes the body.
97 func shortRange(node ast.Node) (pos, end token.Pos) {
98 switch node := node.(type) {
99 case *ast.File:
100 return node.Pos(), node.Name.End()
101 case *ast.CaseClause:
102 return node.Pos(), node.Colon + 1
103 case *ast.CommClause:
104 return node.Pos(), node.Colon + 1
105 case *ast.DeferStmt:
106 return node.Pos(), node.Defer + token.Pos(len("defer"))
107 case *ast.ExprStmt:
108 return shortRange(node.X)
109 case *ast.ForStmt:
110 if node.Post != nil {
111 return node.For, node.Post.End()
112 } else if node.Cond != nil {
113 return node.For, node.Cond.End()
114 } else if node.Init != nil {
115 // +1 to catch the semicolon, for gofmt'ed code
116 return node.Pos(), node.Init.End() + 1
117 } else {
118 return node.Pos(), node.For + token.Pos(len("for"))
119 }
120 case *ast.FuncDecl:
121 return node.Pos(), node.Type.End()
122 case *ast.FuncLit:
123 return node.Pos(), node.Type.End()
124 case *ast.GoStmt:
125 if _, ok := astutil.Unparen(node.Call.Fun).(*ast.FuncLit); ok {
126 return node.Pos(), node.Go + token.Pos(len("go"))
127 } else {
128 return node.Pos(), node.End()
129 }
130 case *ast.IfStmt:
131 return node.Pos(), node.Cond.End()
132 case *ast.RangeStmt:
133 return node.Pos(), node.X.End()
134 case *ast.SelectStmt:
135 return node.Pos(), node.Pos() + token.Pos(len("select"))
136 case *ast.SwitchStmt:
137 if node.Tag != nil {
138 return node.Pos(), node.Tag.End()
139 } else if node.Init != nil {
140 // +1 to catch the semicolon, for gofmt'ed code
141 return node.Pos(), node.Init.End() + 1
142 } else {
143 return node.Pos(), node.Pos() + token.Pos(len("switch"))
144 }
145 case *ast.TypeSwitchStmt:
146 return node.Pos(), node.Assign.End()
147 default:
148 return node.Pos(), node.End()
149 }
150 }
151
152 func HasRange(node Positioner) bool {
153 // we don't know if getRange will be called with shortRange set to
154 // true, so make sure that both work.
155 _, _, ok := getRange(node, false)
156 if !ok {
157 return false
158 }
159 _, _, ok = getRange(node, true)
160 return ok
161 }
162
163 func getRange(node Positioner, short bool) (pos, end token.Pos, ok bool) {
164 switch n := node.(type) {
165 case sourcer:
166 s := n.Source()
167 if s == nil {
168 return 0, 0, false
169 }
170 if short {
171 p, e := shortRange(s)
172 return p, e, true
173 }
174 return s.Pos(), s.End(), true
175 case fullPositioner:
176 if short {
177 p, e := shortRange(n)
178 return p, e, true
179 }
180 return n.Pos(), n.End(), true
181 default:
182 return n.Pos(), token.NoPos, true
183 }
184 }
185
186 func Report(pass *analysis.Pass, node Positioner, message string, opts ...Option) {
187 cfg := &Options{}
188 for _, opt := range opts {
189 opt(cfg)
190 }
191
192 langVersion := code.LanguageVersion(pass, node)
193 stdlibVersion := code.StdlibVersion(pass, node)
194 if n := cfg.MaximumLanguageVersion; n != "" && version.Compare(n, langVersion) == -1 {
195 return
196 }
197 if n := cfg.MaximumStdlibVersion; n != "" && version.Compare(n, stdlibVersion) == -1 {
198 return
199 }
200 if n := cfg.MinimumLanguageVersion; n != "" && version.Compare(n, langVersion) == 1 {
201 return
202 }
203 if n := cfg.MinimumStdlibVersion; n != "" && version.Compare(n, stdlibVersion) == 1 {
204 return
205 }
206
207 file := DisplayPosition(pass.Fset, node.Pos()).Filename
208 if cfg.FilterGenerated {
209 m := pass.ResultOf[generated.Analyzer].(map[string]generated.Generator)
210 if _, ok := m[file]; ok {
211 return
212 }
213 }
214
215 pos, end, ok := getRange(node, cfg.ShortRange)
216 if !ok {
217 panic(fmt.Sprintf("no valid position for reporting node %v", node))
218 }
219 d := analysis.Diagnostic{
220 Pos: pos,
221 End: end,
222 Message: message,
223 SuggestedFixes: cfg.Fixes,
224 Related: cfg.Related,
225 }
226 pass.Report(d)
227 }
228
229 func Render(pass *analysis.Pass, x interface{}) string {
230 var buf bytes.Buffer
231 if err := format.Node(&buf, pass.Fset, x); err != nil {
232 panic(err)
233 }
234 return buf.String()
235 }
236
237 func RenderArgs(pass *analysis.Pass, args []ast.Expr) string {
238 var ss []string
239 for _, arg := range args {
240 ss = append(ss, Render(pass, arg))
241 }
242 return strings.Join(ss, ", ")
243 }
244
245 func DisplayPosition(fset *token.FileSet, p token.Pos) token.Position {
246 if p == token.NoPos {
247 return token.Position{}
248 }
249
250 // Only use the adjusted position if it points to another Go file.
251 // This means we'll point to the original file for cgo files, but
252 // we won't point to a YACC grammar file.
253 pos := fset.PositionFor(p, false)
254 adjPos := fset.PositionFor(p, true)
255
256 if filepath.Ext(adjPos.Filename) == ".go" {
257 return adjPos
258 }
259
260 return pos
261 }
262
263 func Ordinal(n int) string {
264 suffix := "th"
265 if n < 10 || n > 20 {
266 switch n % 10 {
267 case 0:
268 suffix = "th"
269 case 1:
270 suffix = "st"
271 case 2:
272 suffix = "nd"
273 case 3:
274 suffix = "rd"
275 default:
276 suffix = "th"
277 }
278 }
279
280 return strconv.Itoa(n) + suffix
281 }
282