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