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

     1  package sa4004
     2  
     3  import (
     4  	"go/ast"
     5  	"go/token"
     6  	"go/types"
     7  
     8  	"github.com/amarpal/go-tools/analysis/code"
     9  	"github.com/amarpal/go-tools/analysis/lint"
    10  	"github.com/amarpal/go-tools/analysis/report"
    11  	"github.com/amarpal/go-tools/go/types/typeutil"
    12  
    13  	"golang.org/x/tools/go/analysis"
    14  	"golang.org/x/tools/go/analysis/passes/inspect"
    15  )
    16  
    17  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    18  	Analyzer: &analysis.Analyzer{
    19  		Name:     "SA4004",
    20  		Run:      run,
    21  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    22  	},
    23  	Doc: &lint.Documentation{
    24  		Title:    `The loop exits unconditionally after one iteration`,
    25  		Since:    "2017.1",
    26  		Severity: lint.SeverityWarning,
    27  		MergeIf:  lint.MergeIfAll,
    28  	},
    29  })
    30  
    31  var Analyzer = SCAnalyzer.Analyzer
    32  
    33  func run(pass *analysis.Pass) (interface{}, error) {
    34  	// This check detects some, but not all unconditional loop exits.
    35  	// We give up in the following cases:
    36  	//
    37  	// - a goto anywhere in the loop. The goto might skip over our
    38  	// return, and we don't check that it doesn't.
    39  	//
    40  	// - any nested, unlabelled continue, even if it is in another
    41  	// loop or closure.
    42  	fn := func(node ast.Node) {
    43  		var body *ast.BlockStmt
    44  		switch fn := node.(type) {
    45  		case *ast.FuncDecl:
    46  			body = fn.Body
    47  		case *ast.FuncLit:
    48  			body = fn.Body
    49  		default:
    50  			lint.ExhaustiveTypeSwitch(node)
    51  		}
    52  		if body == nil {
    53  			return
    54  		}
    55  		labels := map[types.Object]ast.Stmt{}
    56  		ast.Inspect(body, func(node ast.Node) bool {
    57  			label, ok := node.(*ast.LabeledStmt)
    58  			if !ok {
    59  				return true
    60  			}
    61  			labels[pass.TypesInfo.ObjectOf(label.Label)] = label.Stmt
    62  			return true
    63  		})
    64  
    65  		ast.Inspect(body, func(node ast.Node) bool {
    66  			var loop ast.Node
    67  			var body *ast.BlockStmt
    68  			switch node := node.(type) {
    69  			case *ast.ForStmt:
    70  				body = node.Body
    71  				loop = node
    72  			case *ast.RangeStmt:
    73  				ok := typeutil.All(pass.TypesInfo.TypeOf(node.X), func(term *types.Term) bool {
    74  					switch term.Type().Underlying().(type) {
    75  					case *types.Slice, *types.Chan, *types.Basic, *types.Pointer, *types.Array:
    76  						return true
    77  					case *types.Map:
    78  						// looping once over a map is a valid pattern for
    79  						// getting an arbitrary element.
    80  						return false
    81  					default:
    82  						lint.ExhaustiveTypeSwitch(term.Type().Underlying())
    83  						return false
    84  					}
    85  				})
    86  				if !ok {
    87  					return true
    88  				}
    89  				body = node.Body
    90  				loop = node
    91  			default:
    92  				return true
    93  			}
    94  			if len(body.List) < 2 {
    95  				// TODO(dh): is this check needed? when body.List < 2,
    96  				// then we can't find both an unconditional exit and a
    97  				// branching statement (if, ...). and we don't flag
    98  				// unconditional exits if there has been no branching
    99  				// in the loop body.
   100  
   101  				// avoid flagging the somewhat common pattern of using
   102  				// a range loop to get the first element in a slice,
   103  				// or the first rune in a string.
   104  				return true
   105  			}
   106  			var unconditionalExit ast.Node
   107  			hasBranching := false
   108  			for _, stmt := range body.List {
   109  				switch stmt := stmt.(type) {
   110  				case *ast.BranchStmt:
   111  					switch stmt.Tok {
   112  					case token.BREAK:
   113  						if stmt.Label == nil || labels[pass.TypesInfo.ObjectOf(stmt.Label)] == loop {
   114  							unconditionalExit = stmt
   115  						}
   116  					case token.CONTINUE:
   117  						if stmt.Label == nil || labels[pass.TypesInfo.ObjectOf(stmt.Label)] == loop {
   118  							unconditionalExit = nil
   119  							return false
   120  						}
   121  					}
   122  				case *ast.ReturnStmt:
   123  					unconditionalExit = stmt
   124  				case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt, *ast.SelectStmt:
   125  					hasBranching = true
   126  				}
   127  			}
   128  			if unconditionalExit == nil || !hasBranching {
   129  				return false
   130  			}
   131  			ast.Inspect(body, func(node ast.Node) bool {
   132  				if branch, ok := node.(*ast.BranchStmt); ok {
   133  
   134  					switch branch.Tok {
   135  					case token.GOTO:
   136  						unconditionalExit = nil
   137  						return false
   138  					case token.CONTINUE:
   139  						if branch.Label != nil && labels[pass.TypesInfo.ObjectOf(branch.Label)] != loop {
   140  							return true
   141  						}
   142  						unconditionalExit = nil
   143  						return false
   144  					}
   145  				}
   146  				return true
   147  			})
   148  			if unconditionalExit != nil {
   149  				report.Report(pass, unconditionalExit, "the surrounding loop is unconditionally terminated")
   150  			}
   151  			return true
   152  		})
   153  	}
   154  	code.Preorder(pass, fn, (*ast.FuncDecl)(nil), (*ast.FuncLit)(nil))
   155  	return nil, nil
   156  }