github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa4014/sa4014.go (about)

     1  package sa4014
     2  
     3  import (
     4  	"go/ast"
     5  
     6  	"github.com/amarpal/go-tools/analysis/code"
     7  	"github.com/amarpal/go-tools/analysis/lint"
     8  	"github.com/amarpal/go-tools/analysis/report"
     9  
    10  	"golang.org/x/tools/go/analysis"
    11  	"golang.org/x/tools/go/analysis/passes/inspect"
    12  )
    13  
    14  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    15  	Analyzer: &analysis.Analyzer{
    16  		Name:     "SA4014",
    17  		Run:      run,
    18  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    19  	},
    20  	Doc: &lint.Documentation{
    21  		Title:    `An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either`,
    22  		Since:    "2017.1",
    23  		Severity: lint.SeverityWarning,
    24  		MergeIf:  lint.MergeIfAll,
    25  	},
    26  })
    27  
    28  var Analyzer = SCAnalyzer.Analyzer
    29  
    30  func run(pass *analysis.Pass) (interface{}, error) {
    31  	seen := map[ast.Node]bool{}
    32  
    33  	var collectConds func(ifstmt *ast.IfStmt, conds []ast.Expr) ([]ast.Expr, bool)
    34  	collectConds = func(ifstmt *ast.IfStmt, conds []ast.Expr) ([]ast.Expr, bool) {
    35  		seen[ifstmt] = true
    36  		// Bail if any if-statement has an Init statement or side effects in its condition
    37  		if ifstmt.Init != nil {
    38  			return nil, false
    39  		}
    40  		if code.MayHaveSideEffects(pass, ifstmt.Cond, nil) {
    41  			return nil, false
    42  		}
    43  
    44  		conds = append(conds, ifstmt.Cond)
    45  		if elsestmt, ok := ifstmt.Else.(*ast.IfStmt); ok {
    46  			return collectConds(elsestmt, conds)
    47  		}
    48  		return conds, true
    49  	}
    50  	fn := func(node ast.Node) {
    51  		ifstmt := node.(*ast.IfStmt)
    52  		if seen[ifstmt] {
    53  			// this if-statement is part of an if/else-if chain that we've already processed
    54  			return
    55  		}
    56  		if ifstmt.Else == nil {
    57  			// there can be at most one condition
    58  			return
    59  		}
    60  		conds, ok := collectConds(ifstmt, nil)
    61  		if !ok {
    62  			return
    63  		}
    64  		if len(conds) < 2 {
    65  			return
    66  		}
    67  		counts := map[string]int{}
    68  		for _, cond := range conds {
    69  			s := report.Render(pass, cond)
    70  			counts[s]++
    71  			if counts[s] == 2 {
    72  				report.Report(pass, cond, "this condition occurs multiple times in this if/else if chain")
    73  			}
    74  		}
    75  	}
    76  	code.Preorder(pass, fn, (*ast.IfStmt)(nil))
    77  	return nil, nil
    78  }