1package checkers 2 3import ( 4 "go/ast" 5 "go/token" 6 7 "github.com/go-critic/go-critic/checkers/internal/astwalk" 8 "github.com/go-critic/go-critic/framework/linter" 9 "github.com/go-toolsmith/astcast" 10 "github.com/go-toolsmith/astcopy" 11 "github.com/go-toolsmith/astequal" 12 "github.com/go-toolsmith/typep" 13) 14 15func init() { 16 var info linter.CheckerInfo 17 info.Name = "offBy1" 18 info.Tags = []string{"diagnostic"} 19 info.Summary = "Detects various off-by-one kind of errors" 20 info.Before = `xs[len(xs)]` 21 info.After = `xs[len(xs)-1]` 22 23 collection.AddChecker(&info, func(ctx *linter.CheckerContext) (linter.FileWalker, error) { 24 return astwalk.WalkerForExpr(&offBy1Checker{ctx: ctx}), nil 25 }) 26} 27 28type offBy1Checker struct { 29 astwalk.WalkHandler 30 ctx *linter.CheckerContext 31} 32 33func (c *offBy1Checker) VisitExpr(e ast.Expr) { 34 // TODO(quasilyte): handle more off-by-1 patterns. 35 // TODO(quasilyte): check whether go/analysis can help here. 36 37 // Detect s[len(s)] expressions that always panic. 38 // The correct form is s[len(s)-1]. 39 40 indexExpr := astcast.ToIndexExpr(e) 41 indexed := indexExpr.X 42 if !typep.IsSlice(c.ctx.TypeOf(indexed)) { 43 return 44 } 45 if !typep.SideEffectFree(c.ctx.TypesInfo, indexed) { 46 return 47 } 48 call := astcast.ToCallExpr(indexExpr.Index) 49 if astcast.ToIdent(call.Fun).Name != "len" { 50 return 51 } 52 if len(call.Args) != 1 || !astequal.Expr(call.Args[0], indexed) { 53 return 54 } 55 c.warnLenIndex(indexExpr) 56} 57 58func (c *offBy1Checker) warnLenIndex(cause *ast.IndexExpr) { 59 suggest := astcopy.IndexExpr(cause) 60 suggest.Index = &ast.BinaryExpr{ 61 Op: token.SUB, 62 X: cause.Index, 63 Y: &ast.BasicLit{Value: "1"}, 64 } 65 c.ctx.Warn(cause, "index expr always panics; maybe you wanted %s?", suggest) 66} 67