qf1001.go raw
1 package qf1001
2
3 import (
4 "go/ast"
5 "go/types"
6
7 "honnef.co/go/tools/analysis/code"
8 "honnef.co/go/tools/analysis/edit"
9 "honnef.co/go/tools/analysis/lint"
10 "honnef.co/go/tools/analysis/report"
11 "honnef.co/go/tools/go/ast/astutil"
12 "honnef.co/go/tools/pattern"
13
14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 )
17
18 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
19 Analyzer: &analysis.Analyzer{
20 Name: "QF1001",
21 Run: CheckDeMorgan,
22 Requires: []*analysis.Analyzer{inspect.Analyzer},
23 },
24 Doc: &lint.RawDocumentation{
25 Title: "Apply De Morgan's law",
26 Since: "2021.1",
27 Severity: lint.SeverityHint,
28 },
29 })
30
31 var Analyzer = SCAnalyzer.Analyzer
32
33 var demorganQ = pattern.MustParse(`(UnaryExpr "!" expr@(BinaryExpr _ _ _))`)
34
35 func CheckDeMorgan(pass *analysis.Pass) (interface{}, error) {
36 // TODO(dh): support going in the other direction, e.g. turning `!a && !b && !c` into `!(a || b || c)`
37
38 // hasFloats reports whether any subexpression is of type float.
39 hasFloats := func(expr ast.Expr) bool {
40 found := false
41 ast.Inspect(expr, func(node ast.Node) bool {
42 if expr, ok := node.(ast.Expr); ok {
43 if typ := pass.TypesInfo.TypeOf(expr); typ != nil {
44 if basic, ok := typ.Underlying().(*types.Basic); ok {
45 if (basic.Info() & types.IsFloat) != 0 {
46 found = true
47 return false
48 }
49 }
50 }
51 }
52 return true
53 })
54 return found
55 }
56
57 fn := func(node ast.Node, stack []ast.Node) {
58 matcher, ok := code.Match(pass, demorganQ, node)
59 if !ok {
60 return
61 }
62
63 expr := matcher.State["expr"].(ast.Expr)
64
65 // be extremely conservative when it comes to floats
66 if hasFloats(expr) {
67 return
68 }
69
70 n := astutil.NegateDeMorgan(expr, false)
71 nr := astutil.NegateDeMorgan(expr, true)
72 nc, ok := astutil.CopyExpr(n)
73 if !ok {
74 return
75 }
76 ns := astutil.SimplifyParentheses(nc)
77 nrc, ok := astutil.CopyExpr(nr)
78 if !ok {
79 return
80 }
81 nrs := astutil.SimplifyParentheses(nrc)
82
83 var bn, bnr, bns, bnrs string
84 switch parent := stack[len(stack)-2]; parent.(type) {
85 case *ast.BinaryExpr, *ast.IfStmt, *ast.ForStmt, *ast.SwitchStmt:
86 // Always add parentheses for if, for and switch. If
87 // they're unnecessary, go/printer will strip them when
88 // the whole file gets formatted.
89
90 bn = report.Render(pass, &ast.ParenExpr{X: n})
91 bnr = report.Render(pass, &ast.ParenExpr{X: nr})
92 bns = report.Render(pass, &ast.ParenExpr{X: ns})
93 bnrs = report.Render(pass, &ast.ParenExpr{X: nrs})
94
95 default:
96 // TODO are there other types where we don't want to strip parentheses?
97 bn = report.Render(pass, n)
98 bnr = report.Render(pass, nr)
99 bns = report.Render(pass, ns)
100 bnrs = report.Render(pass, nrs)
101 }
102
103 // Note: we cannot compare the ASTs directly, because
104 // simplifyParentheses might have rebalanced trees without
105 // affecting the rendered form.
106 var fixes []analysis.SuggestedFix
107 fixes = append(fixes, edit.Fix("Apply De Morgan's law", edit.ReplaceWithString(node, bn)))
108 if bn != bns {
109 fixes = append(fixes, edit.Fix("Apply De Morgan's law & simplify", edit.ReplaceWithString(node, bns)))
110 }
111 if bn != bnr {
112 fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively", edit.ReplaceWithString(node, bnr)))
113 if bnr != bnrs {
114 fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively & simplify", edit.ReplaceWithString(node, bnrs)))
115 }
116 }
117
118 report.Report(pass, node, "could apply De Morgan's law", report.Fixes(fixes...))
119 }
120
121 code.PreorderStack(pass, fn, (*ast.UnaryExpr)(nil))
122
123 return nil, nil
124 }
125