qf1001.go (3523B)
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) (any, 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 for c := range code.Cursor(pass).Preorder((*ast.UnaryExpr)(nil)) { 58 node := c.Node() 59 matcher, ok := code.Match(pass, demorganQ, node) 60 if !ok { 61 continue 62 } 63 64 expr := matcher.State["expr"].(ast.Expr) 65 66 // be extremely conservative when it comes to floats 67 if hasFloats(expr) { 68 continue 69 } 70 71 n := astutil.NegateDeMorgan(expr, false) 72 nr := astutil.NegateDeMorgan(expr, true) 73 nc, ok := astutil.CopyExpr(n) 74 if !ok { 75 continue 76 } 77 ns := astutil.SimplifyParentheses(nc) 78 nrc, ok := astutil.CopyExpr(nr) 79 if !ok { 80 continue 81 } 82 nrs := astutil.SimplifyParentheses(nrc) 83 84 var bn, bnr, bns, bnrs string 85 switch c.Parent().Node().(type) { 86 case *ast.BinaryExpr, *ast.IfStmt, *ast.ForStmt, *ast.SwitchStmt: 87 // Always add parentheses for if, for and switch. If 88 // they're unnecessary, go/printer will strip them when 89 // the whole file gets formatted. 90 91 bn = report.Render(pass, &ast.ParenExpr{X: n}) 92 bnr = report.Render(pass, &ast.ParenExpr{X: nr}) 93 bns = report.Render(pass, &ast.ParenExpr{X: ns}) 94 bnrs = report.Render(pass, &ast.ParenExpr{X: nrs}) 95 96 default: 97 // TODO are there other types where we don't want to strip parentheses? 98 bn = report.Render(pass, n) 99 bnr = report.Render(pass, nr) 100 bns = report.Render(pass, ns) 101 bnrs = report.Render(pass, nrs) 102 } 103 104 // Note: we cannot compare the ASTs directly, because 105 // simplifyParentheses might have rebalanced trees without 106 // affecting the rendered form. 107 var fixes []analysis.SuggestedFix 108 fixes = append(fixes, edit.Fix("Apply De Morgan's law", edit.ReplaceWithString(node, bn))) 109 if bn != bns { 110 fixes = append(fixes, edit.Fix("Apply De Morgan's law & simplify", edit.ReplaceWithString(node, bns))) 111 } 112 if bn != bnr { 113 fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively", edit.ReplaceWithString(node, bnr))) 114 if bnr != bnrs { 115 fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively & simplify", edit.ReplaceWithString(node, bnrs))) 116 } 117 } 118 119 report.Report(pass, node, "could apply De Morgan's law", report.Fixes(fixes...)) 120 } 121 122 return nil, nil 123 }