package qf1001 import ( "go/ast" "go/types" "honnef.co/go/tools/analysis/code" "honnef.co/go/tools/analysis/edit" "honnef.co/go/tools/analysis/lint" "honnef.co/go/tools/analysis/report" "honnef.co/go/tools/go/ast/astutil" "honnef.co/go/tools/pattern" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" ) var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ Analyzer: &analysis.Analyzer{ Name: "QF1001", Run: CheckDeMorgan, Requires: []*analysis.Analyzer{inspect.Analyzer}, }, Doc: &lint.RawDocumentation{ Title: "Apply De Morgan's law", Since: "2021.1", Severity: lint.SeverityHint, }, }) var Analyzer = SCAnalyzer.Analyzer var demorganQ = pattern.MustParse(`(UnaryExpr "!" expr@(BinaryExpr _ _ _))`) func CheckDeMorgan(pass *analysis.Pass) (interface{}, error) { // TODO(dh): support going in the other direction, e.g. turning `!a && !b && !c` into `!(a || b || c)` // hasFloats reports whether any subexpression is of type float. hasFloats := func(expr ast.Expr) bool { found := false ast.Inspect(expr, func(node ast.Node) bool { if expr, ok := node.(ast.Expr); ok { if typ := pass.TypesInfo.TypeOf(expr); typ != nil { if basic, ok := typ.Underlying().(*types.Basic); ok { if (basic.Info() & types.IsFloat) != 0 { found = true return false } } } } return true }) return found } fn := func(node ast.Node, stack []ast.Node) { matcher, ok := code.Match(pass, demorganQ, node) if !ok { return } expr := matcher.State["expr"].(ast.Expr) // be extremely conservative when it comes to floats if hasFloats(expr) { return } n := astutil.NegateDeMorgan(expr, false) nr := astutil.NegateDeMorgan(expr, true) nc, ok := astutil.CopyExpr(n) if !ok { return } ns := astutil.SimplifyParentheses(nc) nrc, ok := astutil.CopyExpr(nr) if !ok { return } nrs := astutil.SimplifyParentheses(nrc) var bn, bnr, bns, bnrs string switch parent := stack[len(stack)-2]; parent.(type) { case *ast.BinaryExpr, *ast.IfStmt, *ast.ForStmt, *ast.SwitchStmt: // Always add parentheses for if, for and switch. If // they're unnecessary, go/printer will strip them when // the whole file gets formatted. bn = report.Render(pass, &ast.ParenExpr{X: n}) bnr = report.Render(pass, &ast.ParenExpr{X: nr}) bns = report.Render(pass, &ast.ParenExpr{X: ns}) bnrs = report.Render(pass, &ast.ParenExpr{X: nrs}) default: // TODO are there other types where we don't want to strip parentheses? bn = report.Render(pass, n) bnr = report.Render(pass, nr) bns = report.Render(pass, ns) bnrs = report.Render(pass, nrs) } // Note: we cannot compare the ASTs directly, because // simplifyParentheses might have rebalanced trees without // affecting the rendered form. var fixes []analysis.SuggestedFix fixes = append(fixes, edit.Fix("Apply De Morgan's law", edit.ReplaceWithString(node, bn))) if bn != bns { fixes = append(fixes, edit.Fix("Apply De Morgan's law & simplify", edit.ReplaceWithString(node, bns))) } if bn != bnr { fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively", edit.ReplaceWithString(node, bnr))) if bnr != bnrs { fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively & simplify", edit.ReplaceWithString(node, bnrs))) } } report.Report(pass, node, "could apply De Morgan's law", report.Fixes(fixes...)) } code.PreorderStack(pass, fn, (*ast.UnaryExpr)(nil)) return nil, nil }