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