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)
13
14func init() {
15	var info linter.CheckerInfo
16	info.Name = "sloppyReassign"
17	info.Tags = []string{"diagnostic", "experimental"}
18	info.Summary = "Detects suspicious/confusing re-assignments"
19	info.Before = `if err = f(); err != nil { return err }`
20	info.After = `if err := f(); err != nil { return err }`
21
22	collection.AddChecker(&info, func(ctx *linter.CheckerContext) linter.FileWalker {
23		return astwalk.WalkerForStmt(&sloppyReassignChecker{ctx: ctx})
24	})
25}
26
27type sloppyReassignChecker struct {
28	astwalk.WalkHandler
29	ctx *linter.CheckerContext
30}
31
32func (c *sloppyReassignChecker) VisitStmt(stmt ast.Stmt) {
33	// Right now only check assignments in if statements init.
34	ifStmt := astcast.ToIfStmt(stmt)
35	assign := astcast.ToAssignStmt(ifStmt.Init)
36	if assign.Tok != token.ASSIGN {
37		return
38	}
39
40	// TODO(quasilyte): is handling of multi-value assignments worthwhile?
41	if len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
42		return
43	}
44
45	// TODO(quasilyte): handle not only the simplest, return-only case.
46	body := ifStmt.Body.List
47	if len(body) != 1 {
48		return
49	}
50
51	// Variable that is being re-assigned.
52	reAssigned := astcast.ToIdent(assign.Lhs[0])
53	if reAssigned.Name == "" {
54		return
55	}
56
57	// TODO(quasilyte): handle not only nil comparisons.
58	eqToNil := &ast.BinaryExpr{
59		Op: token.NEQ,
60		X:  reAssigned,
61		Y:  &ast.Ident{Name: "nil"},
62	}
63	if !astequal.Expr(ifStmt.Cond, eqToNil) {
64		return
65	}
66
67	results := astcast.ToReturnStmt(body[0]).Results
68	for _, res := range results {
69		if astequal.Expr(reAssigned, res) {
70			c.warnAssignToDefine(assign, reAssigned.Name)
71			break
72		}
73	}
74}
75
76func (c *sloppyReassignChecker) warnAssignToDefine(assign *ast.AssignStmt, name string) {
77	suggest := astcopy.AssignStmt(assign)
78	suggest.Tok = token.DEFINE
79	c.ctx.Warn(assign, "re-assignment to `%s` can be replaced with `%s`", name, suggest)
80}
81