github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/quickfix/qf1001/qf1001.go (about) 1 package qf1001 2 3 import ( 4 "go/ast" 5 "go/types" 6 7 "github.com/amarpal/go-tools/analysis/code" 8 "github.com/amarpal/go-tools/analysis/edit" 9 "github.com/amarpal/go-tools/analysis/lint" 10 "github.com/amarpal/go-tools/analysis/report" 11 "github.com/amarpal/go-tools/go/ast/astutil" 12 "github.com/amarpal/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.Documentation{ 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 basic, ok := pass.TypesInfo.TypeOf(expr).Underlying().(*types.Basic); ok { 44 if (basic.Info() & types.IsFloat) != 0 { 45 found = true 46 return false 47 } 48 } 49 } 50 return true 51 }) 52 return found 53 } 54 55 fn := func(node ast.Node, stack []ast.Node) { 56 matcher, ok := code.Match(pass, demorganQ, node) 57 if !ok { 58 return 59 } 60 61 expr := matcher.State["expr"].(ast.Expr) 62 63 // be extremely conservative when it comes to floats 64 if hasFloats(expr) { 65 return 66 } 67 68 n := astutil.NegateDeMorgan(expr, false) 69 nr := astutil.NegateDeMorgan(expr, true) 70 nc, ok := astutil.CopyExpr(n) 71 if !ok { 72 return 73 } 74 ns := astutil.SimplifyParentheses(nc) 75 nrc, ok := astutil.CopyExpr(nr) 76 if !ok { 77 return 78 } 79 nrs := astutil.SimplifyParentheses(nrc) 80 81 var bn, bnr, bns, bnrs string 82 switch parent := stack[len(stack)-2]; parent.(type) { 83 case *ast.BinaryExpr, *ast.IfStmt, *ast.ForStmt, *ast.SwitchStmt: 84 // Always add parentheses for if, for and switch. If 85 // they're unnecessary, go/printer will strip them when 86 // the whole file gets formatted. 87 88 bn = report.Render(pass, &ast.ParenExpr{X: n}) 89 bnr = report.Render(pass, &ast.ParenExpr{X: nr}) 90 bns = report.Render(pass, &ast.ParenExpr{X: ns}) 91 bnrs = report.Render(pass, &ast.ParenExpr{X: nrs}) 92 93 default: 94 // TODO are there other types where we don't want to strip parentheses? 95 bn = report.Render(pass, n) 96 bnr = report.Render(pass, nr) 97 bns = report.Render(pass, ns) 98 bnrs = report.Render(pass, nrs) 99 } 100 101 // Note: we cannot compare the ASTs directly, because 102 // simplifyParentheses might have rebalanced trees without 103 // affecting the rendered form. 104 var fixes []analysis.SuggestedFix 105 fixes = append(fixes, edit.Fix("Apply De Morgan's law", edit.ReplaceWithString(node, bn))) 106 if bn != bns { 107 fixes = append(fixes, edit.Fix("Apply De Morgan's law & simplify", edit.ReplaceWithString(node, bns))) 108 } 109 if bn != bnr { 110 fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively", edit.ReplaceWithString(node, bnr))) 111 if bnr != bnrs { 112 fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively & simplify", edit.ReplaceWithString(node, bnrs))) 113 } 114 } 115 116 report.Report(pass, node, "could apply De Morgan's law", report.Fixes(fixes...)) 117 } 118 119 code.PreorderStack(pass, fn, (*ast.UnaryExpr)(nil)) 120 121 return nil, nil 122 }