1// Copyright 2014 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package bools defines an Analyzer that detects common mistakes
6// involving boolean operators.
7package bools
8
9import (
10	"go/ast"
11	"go/token"
12	"go/types"
13
14	"golang.org/x/tools/go/analysis"
15	"golang.org/x/tools/go/analysis/passes/inspect"
16	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
17	"golang.org/x/tools/go/ast/astutil"
18	"golang.org/x/tools/go/ast/inspector"
19)
20
21const Doc = "check for common mistakes involving boolean operators"
22
23var Analyzer = &analysis.Analyzer{
24	Name:     "bools",
25	Doc:      Doc,
26	URL:      "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/bools",
27	Requires: []*analysis.Analyzer{inspect.Analyzer},
28	Run:      run,
29}
30
31func run(pass *analysis.Pass) (interface{}, error) {
32	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
33
34	nodeFilter := []ast.Node{
35		(*ast.BinaryExpr)(nil),
36	}
37	seen := make(map[*ast.BinaryExpr]bool)
38	inspect.Preorder(nodeFilter, func(n ast.Node) {
39		e := n.(*ast.BinaryExpr)
40		if seen[e] {
41			// Already processed as a subexpression of an earlier node.
42			return
43		}
44
45		var op boolOp
46		switch e.Op {
47		case token.LOR:
48			op = or
49		case token.LAND:
50			op = and
51		default:
52			return
53		}
54
55		comm := op.commutativeSets(pass.TypesInfo, e, seen)
56		for _, exprs := range comm {
57			op.checkRedundant(pass, exprs)
58			op.checkSuspect(pass, exprs)
59		}
60	})
61	return nil, nil
62}
63
64type boolOp struct {
65	name  string
66	tok   token.Token // token corresponding to this operator
67	badEq token.Token // token corresponding to the equality test that should not be used with this operator
68}
69
70var (
71	or  = boolOp{"or", token.LOR, token.NEQ}
72	and = boolOp{"and", token.LAND, token.EQL}
73)
74
75// commutativeSets returns all side effect free sets of
76// expressions in e that are connected by op.
77// For example, given 'a || b || f() || c || d' with the or op,
78// commutativeSets returns {{b, a}, {d, c}}.
79// commutativeSets adds any expanded BinaryExprs to seen.
80func (op boolOp) commutativeSets(info *types.Info, e *ast.BinaryExpr, seen map[*ast.BinaryExpr]bool) [][]ast.Expr {
81	exprs := op.split(e, seen)
82
83	// Partition the slice of expressions into commutative sets.
84	i := 0
85	var sets [][]ast.Expr
86	for j := 0; j <= len(exprs); j++ {
87		if j == len(exprs) || analysisutil.HasSideEffects(info, exprs[j]) {
88			if i < j {
89				sets = append(sets, exprs[i:j])
90			}
91			i = j + 1
92		}
93	}
94
95	return sets
96}
97
98// checkRedundant checks for expressions of the form
99//
100//	e && e
101//	e || e
102//
103// Exprs must contain only side effect free expressions.
104func (op boolOp) checkRedundant(pass *analysis.Pass, exprs []ast.Expr) {
105	seen := make(map[string]bool)
106	for _, e := range exprs {
107		efmt := analysisutil.Format(pass.Fset, e)
108		if seen[efmt] {
109			pass.ReportRangef(e, "redundant %s: %s %s %s", op.name, efmt, op.tok, efmt)
110		} else {
111			seen[efmt] = true
112		}
113	}
114}
115
116// checkSuspect checks for expressions of the form
117//
118//	x != c1 || x != c2
119//	x == c1 && x == c2
120//
121// where c1 and c2 are constant expressions.
122// If c1 and c2 are the same then it's redundant;
123// if c1 and c2 are different then it's always true or always false.
124// Exprs must contain only side effect free expressions.
125func (op boolOp) checkSuspect(pass *analysis.Pass, exprs []ast.Expr) {
126	// seen maps from expressions 'x' to equality expressions 'x != c'.
127	seen := make(map[string]string)
128
129	for _, e := range exprs {
130		bin, ok := e.(*ast.BinaryExpr)
131		if !ok || bin.Op != op.badEq {
132			continue
133		}
134
135		// In order to avoid false positives, restrict to cases
136		// in which one of the operands is constant. We're then
137		// interested in the other operand.
138		// In the rare case in which both operands are constant
139		// (e.g. runtime.GOOS and "windows"), we'll only catch
140		// mistakes if the LHS is repeated, which is how most
141		// code is written.
142		var x ast.Expr
143		switch {
144		case pass.TypesInfo.Types[bin.Y].Value != nil:
145			x = bin.X
146		case pass.TypesInfo.Types[bin.X].Value != nil:
147			x = bin.Y
148		default:
149			continue
150		}
151
152		// e is of the form 'x != c' or 'x == c'.
153		xfmt := analysisutil.Format(pass.Fset, x)
154		efmt := analysisutil.Format(pass.Fset, e)
155		if prev, found := seen[xfmt]; found {
156			// checkRedundant handles the case in which efmt == prev.
157			if efmt != prev {
158				pass.ReportRangef(e, "suspect %s: %s %s %s", op.name, efmt, op.tok, prev)
159			}
160		} else {
161			seen[xfmt] = efmt
162		}
163	}
164}
165
166// split returns a slice of all subexpressions in e that are connected by op.
167// For example, given 'a || (b || c) || d' with the or op,
168// split returns []{d, c, b, a}.
169// seen[e] is already true; any newly processed exprs are added to seen.
170func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.Expr) {
171	for {
172		e = astutil.Unparen(e)
173		if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok {
174			seen[b] = true
175			exprs = append(exprs, op.split(b.Y, seen)...)
176			e = b.X
177		} else {
178			exprs = append(exprs, e)
179			break
180		}
181	}
182	return
183}
184