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