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  }