github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/simple/s1011/s1011.go (about) 1 package s1011 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/token" 7 "go/types" 8 9 "github.com/amarpal/go-tools/analysis/code" 10 "github.com/amarpal/go-tools/analysis/edit" 11 "github.com/amarpal/go-tools/analysis/facts/generated" 12 "github.com/amarpal/go-tools/analysis/facts/purity" 13 "github.com/amarpal/go-tools/analysis/lint" 14 "github.com/amarpal/go-tools/analysis/report" 15 "github.com/amarpal/go-tools/pattern" 16 17 "golang.org/x/tools/go/analysis" 18 "golang.org/x/tools/go/analysis/passes/inspect" 19 ) 20 21 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 22 Analyzer: &analysis.Analyzer{ 23 Name: "S1011", 24 Run: run, 25 Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer, purity.Analyzer}, 26 }, 27 Doc: &lint.Documentation{ 28 Title: `Use a single \'append\' to concatenate two slices`, 29 Before: ` 30 for _, e := range y { 31 x = append(x, e) 32 } 33 34 for i := range y { 35 x = append(x, y[i]) 36 } 37 38 for i := range y { 39 v := y[i] 40 x = append(x, v) 41 }`, 42 43 After: ` 44 x = append(x, y...) 45 x = append(x, y...) 46 x = append(x, y...)`, 47 Since: "2017.1", 48 // MergeIfAll because y might not be a slice under all build tags. 49 MergeIf: lint.MergeIfAll, 50 }, 51 }) 52 53 var Analyzer = SCAnalyzer.Analyzer 54 55 var checkLoopAppendQ = pattern.MustParse(` 56 (Or 57 (RangeStmt 58 (Ident "_") 59 val@(Object _) 60 _ 61 x 62 [(AssignStmt [lhs] "=" [(CallExpr (Builtin "append") [lhs val])])]) 63 (RangeStmt 64 idx@(Object _) 65 nil 66 _ 67 x 68 [(AssignStmt [lhs] "=" [(CallExpr (Builtin "append") [lhs (IndexExpr x idx)])])]) 69 (RangeStmt 70 idx@(Object _) 71 nil 72 _ 73 x 74 [(AssignStmt val@(Object _) ":=" (IndexExpr x idx)) 75 (AssignStmt [lhs] "=" [(CallExpr (Builtin "append") [lhs val])])]))`) 76 77 func run(pass *analysis.Pass) (interface{}, error) { 78 pure := pass.ResultOf[purity.Analyzer].(purity.Result) 79 80 fn := func(node ast.Node) { 81 m, ok := code.Match(pass, checkLoopAppendQ, node) 82 if !ok { 83 return 84 } 85 86 if val, ok := m.State["val"].(types.Object); ok && code.RefersTo(pass, m.State["lhs"].(ast.Expr), val) { 87 return 88 } 89 90 if m.State["idx"] != nil && code.MayHaveSideEffects(pass, m.State["x"].(ast.Expr), pure) { 91 // When using an index-based loop, x gets evaluated repeatedly and thus should be pure. 92 // This doesn't matter for value-based loops, because x only gets evaluated once. 93 return 94 } 95 96 if idx, ok := m.State["idx"].(types.Object); ok && code.RefersTo(pass, m.State["lhs"].(ast.Expr), idx) { 97 // The lhs mustn't refer to the index loop variable. 98 return 99 } 100 101 if code.MayHaveSideEffects(pass, m.State["lhs"].(ast.Expr), pure) { 102 // The lhs may be dynamic and return different values on each iteration. For example: 103 // 104 // func bar() map[int][]int { /* return one of several maps */ } 105 // 106 // func foo(x []int, y [][]int) { 107 // for i := range x { 108 // bar()[0] = append(bar()[0], x[i]) 109 // } 110 // } 111 // 112 // The dynamic nature of the lhs might also affect the value of the index. 113 return 114 } 115 116 src := pass.TypesInfo.TypeOf(m.State["x"].(ast.Expr)) 117 dst := pass.TypesInfo.TypeOf(m.State["lhs"].(ast.Expr)) 118 if !types.Identical(src, dst) { 119 return 120 } 121 122 r := &ast.AssignStmt{ 123 Lhs: []ast.Expr{m.State["lhs"].(ast.Expr)}, 124 Tok: token.ASSIGN, 125 Rhs: []ast.Expr{ 126 &ast.CallExpr{ 127 Fun: &ast.Ident{Name: "append"}, 128 Args: []ast.Expr{ 129 m.State["lhs"].(ast.Expr), 130 m.State["x"].(ast.Expr), 131 }, 132 Ellipsis: 1, 133 }, 134 }, 135 } 136 137 report.Report(pass, node, fmt.Sprintf("should replace loop with %s", report.Render(pass, r)), 138 report.ShortRange(), 139 report.FilterGenerated(), 140 report.Fixes(edit.Fix("replace loop with call to append", edit.ReplaceWithNode(pass.Fset, node, r)))) 141 } 142 code.Preorder(pass, fn, (*ast.RangeStmt)(nil)) 143 return nil, nil 144 }