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 }