├── .gitignore ├── go.mod ├── cmd └── copyloopvar │ └── main.go ├── go.sum ├── README.md ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── copyloopvar_test.go ├── testdata └── src │ ├── basic │ ├── main.go.golden │ └── main.go │ └── checkalias │ ├── main.go.golden │ └── main.go └── copyloopvar.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | copyloopvar 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/karamaru-alpha/copyloopvar 2 | 3 | go 1.24.0 4 | 5 | require golang.org/x/tools v0.37.0 6 | 7 | require ( 8 | golang.org/x/mod v0.28.0 // indirect 9 | golang.org/x/sync v0.17.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /cmd/copyloopvar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/tools/go/analysis/singlechecker" 5 | 6 | "github.com/karamaru-alpha/copyloopvar" 7 | ) 8 | 9 | func main() { singlechecker.Main(copyloopvar.NewAnalyzer()) } 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= 4 | golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= 5 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 6 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 7 | golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= 8 | golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # copyloopvar 2 | 3 | copyloopvar is a linter detects places where loop variables are copied. 4 | 5 | cf. [Fixing For Loops in Go 1.22](https://go.dev/blog/loopvar-preview) 6 | 7 | ## Example 8 | 9 | ```go 10 | for i, v := range []int{1, 2, 3} { 11 | i := i // The copy of the 'for' variable "i" can be deleted (Go 1.22+) 12 | v := v // The copy of the 'for' variable "v" can be deleted (Go 1.22+) 13 | _, _ = i, v 14 | } 15 | 16 | for i := 1; i <= 3; i++ { 17 | i := i // The copy of the 'for' variable "i" can be deleted (Go 1.22+) 18 | _ = i 19 | } 20 | ``` 21 | 22 | ## Install 23 | 24 | ```bash 25 | go install github.com/karamaru-alpha/copyloopvar/cmd/copyloopvar@latest 26 | go vet -vettool=`which copyloopvar` ./... 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths: 8 | - '**.go' 9 | - go.mod 10 | - go.sum 11 | - .github/workflows/ci.yml 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | ci: 19 | name: test and lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4.2.2 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version-file: 'go.mod' 28 | - name: Run test 29 | run: go test -v ./... 30 | - name: Run golangci-lint 31 | uses: golangci/golangci-lint-action@v8 32 | with: 33 | version: v2.5.0 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ryosei Karaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /copyloopvar_test.go: -------------------------------------------------------------------------------- 1 | package copyloopvar 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/tools/go/analysis/analysistest" 7 | ) 8 | 9 | func TestAnalyzer(t *testing.T) { 10 | testCases := []struct { 11 | desc string 12 | dir string 13 | options map[string]string 14 | }{ 15 | { 16 | desc: "basic", 17 | dir: "basic", 18 | }, 19 | { 20 | desc: "check-alias", 21 | dir: "checkalias", 22 | options: map[string]string{ 23 | "check-alias": "true", 24 | }, 25 | }, 26 | } 27 | 28 | for _, test := range testCases { 29 | t.Run(test.desc, func(t *testing.T) { 30 | analyzer := NewAnalyzer() 31 | 32 | for k, v := range test.options { 33 | if err := analyzer.Flags.Set(k, v); err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | 38 | results := analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), analyzer, test.dir) 39 | 40 | hasSuggestedFixes(t, results) 41 | }) 42 | } 43 | } 44 | 45 | func hasSuggestedFixes(t *testing.T, results []*analysistest.Result) { 46 | t.Helper() 47 | 48 | for _, result := range results { 49 | for _, diagnostic := range result.Diagnostics { 50 | if len(diagnostic.SuggestedFixes) > 0 { 51 | return 52 | } 53 | } 54 | } 55 | 56 | t.Errorf("no suggested fixes found") 57 | } 58 | -------------------------------------------------------------------------------- /testdata/src/basic/main.go.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | for i, v := range []int{1, 2, 3} { 5 | // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 6 | _i := i 7 | // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 8 | _v := v 9 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 10 | b, _i := 1, i 11 | c, v := 1, v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 12 | d, _v := 1, v 13 | e := false 14 | _, _, _, _, _, _, _, _, _ = i, _i, v, _v, a, b, c, d, e 15 | } 16 | 17 | for i, j := 1, 1; i+j <= 3; i++ { 18 | // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 19 | _i := i 20 | // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 21 | _j := j 22 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 23 | b, _i := 1, i 24 | c, j := 1, j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 25 | d, _j := 1, j 26 | e := false 27 | _, _, _, _, _, _, _, _, _ = i, _i, j, _j, a, b, c, d, e 28 | } 29 | 30 | for i := range []int{1, 2, 3} { 31 | // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 32 | _i := i 33 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 34 | b, _i := 1, i 35 | c := false 36 | _, _, _, _, _ = i, _i, a, b, c 37 | } 38 | 39 | var t struct { 40 | Bool bool 41 | } 42 | for _, t.Bool = range []bool{true, false} { 43 | t.Bool = t.Bool // NOTE: ignore 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /testdata/src/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | for i, v := range []int{1, 2, 3} { 5 | i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 6 | _i := i 7 | v := v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 8 | _v := v 9 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 10 | b, _i := 1, i 11 | c, v := 1, v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 12 | d, _v := 1, v 13 | e := false 14 | _, _, _, _, _, _, _, _, _ = i, _i, v, _v, a, b, c, d, e 15 | } 16 | 17 | for i, j := 1, 1; i+j <= 3; i++ { 18 | i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 19 | _i := i 20 | j := j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 21 | _j := j 22 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 23 | b, _i := 1, i 24 | c, j := 1, j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 25 | d, _j := 1, j 26 | e := false 27 | _, _, _, _, _, _, _, _, _ = i, _i, j, _j, a, b, c, d, e 28 | } 29 | 30 | for i := range []int{1, 2, 3} { 31 | i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 32 | _i := i 33 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 34 | b, _i := 1, i 35 | c := false 36 | _, _, _, _, _ = i, _i, a, b, c 37 | } 38 | 39 | var t struct { 40 | Bool bool 41 | } 42 | for _, t.Bool = range []bool{true, false} { 43 | t.Bool = t.Bool // NOTE: ignore 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /testdata/src/checkalias/main.go.golden: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | for i, v := range []int{1, 2, 3} { 5 | // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 6 | _i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 7 | // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 8 | _v := v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 9 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 10 | b, _i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 11 | c, v := 1, v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 12 | d, _v := 1, v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 13 | e := false 14 | _, _, _, _, _, _, _, _, _ = i, _i, v, _v, a, b, c, d, e 15 | } 16 | 17 | for i, j := 1, 1; i+j <= 3; i++ { 18 | // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 19 | _i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 20 | // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 21 | _j := j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 22 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 23 | b, _i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 24 | c, j := 1, j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 25 | d, _j := 1, j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 26 | e := false 27 | _, _, _, _, _, _, _, _, _ = i, _i, j, _j, a, b, c, d, e 28 | } 29 | 30 | for i := range []int{1, 2, 3} { 31 | // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 32 | _i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 33 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 34 | b, _i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 35 | c := false 36 | _, _, _, _, _ = i, _i, a, b, c 37 | } 38 | 39 | var t struct { 40 | Bool bool 41 | } 42 | for _, t.Bool = range []bool{true, false} { 43 | t.Bool = t.Bool // NOTE: ignore 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /testdata/src/checkalias/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | for i, v := range []int{1, 2, 3} { 5 | i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 6 | _i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 7 | v := v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 8 | _v := v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 9 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 10 | b, _i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 11 | c, v := 1, v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 12 | d, _v := 1, v // want `The copy of the 'for' variable "v" can be deleted \(Go 1\.22\+\)` 13 | e := false 14 | _, _, _, _, _, _, _, _, _ = i, _i, v, _v, a, b, c, d, e 15 | } 16 | 17 | for i, j := 1, 1; i+j <= 3; i++ { 18 | i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 19 | _i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 20 | j := j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 21 | _j := j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 22 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 23 | b, _i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 24 | c, j := 1, j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 25 | d, _j := 1, j // want `The copy of the 'for' variable "j" can be deleted \(Go 1\.22\+\)` 26 | e := false 27 | _, _, _, _, _, _, _, _, _ = i, _i, j, _j, a, b, c, d, e 28 | } 29 | 30 | for i := range []int{1, 2, 3} { 31 | i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 32 | _i := i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 33 | a, i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 34 | b, _i := 1, i // want `The copy of the 'for' variable "i" can be deleted \(Go 1\.22\+\)` 35 | c := false 36 | _, _, _, _, _ = i, _i, a, b, c 37 | } 38 | 39 | var t struct { 40 | Bool bool 41 | } 42 | for _, t.Bool = range []bool{true, false} { 43 | t.Bool = t.Bool // NOTE: ignore 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /copyloopvar.go: -------------------------------------------------------------------------------- 1 | package copyloopvar 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | 8 | "golang.org/x/tools/go/analysis" 9 | "golang.org/x/tools/go/analysis/passes/inspect" 10 | "golang.org/x/tools/go/ast/inspector" 11 | ) 12 | 13 | var checkAlias bool 14 | 15 | func NewAnalyzer() *analysis.Analyzer { 16 | analyzer := &analysis.Analyzer{ 17 | Name: "copyloopvar", 18 | Doc: "a linter detects places where loop variables are copied", 19 | Run: run, 20 | Requires: []*analysis.Analyzer{ 21 | inspect.Analyzer, 22 | }, 23 | } 24 | analyzer.Flags.BoolVar(&checkAlias, "check-alias", false, "check all assigning the loop variable to another variable") 25 | return analyzer 26 | } 27 | 28 | func run(pass *analysis.Pass) (any, error) { 29 | pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Preorder([]ast.Node{ 30 | (*ast.RangeStmt)(nil), 31 | (*ast.ForStmt)(nil), 32 | }, func(n ast.Node) { 33 | switch node := n.(type) { 34 | case *ast.RangeStmt: 35 | checkRangeStmt(pass, node) 36 | case *ast.ForStmt: 37 | checkForStmt(pass, node) 38 | } 39 | }) 40 | 41 | return nil, nil 42 | } 43 | 44 | func checkRangeStmt(pass *analysis.Pass, rangeStmt *ast.RangeStmt) { 45 | key, ok := rangeStmt.Key.(*ast.Ident) 46 | if !ok { 47 | return 48 | } 49 | var value *ast.Ident 50 | if rangeStmt.Value != nil { 51 | if value, ok = rangeStmt.Value.(*ast.Ident); !ok { 52 | return 53 | } 54 | } 55 | for _, stmt := range rangeStmt.Body.List { 56 | assignStmt, ok := stmt.(*ast.AssignStmt) 57 | if !ok { 58 | continue 59 | } 60 | if assignStmt.Tok != token.DEFINE { 61 | continue 62 | } 63 | for i, rh := range assignStmt.Rhs { 64 | right, ok := rh.(*ast.Ident) 65 | if !ok { 66 | continue 67 | } 68 | if right.Name != key.Name && (value == nil || right.Name != value.Name) { 69 | continue 70 | } 71 | if !checkAlias { 72 | left, ok := assignStmt.Lhs[i].(*ast.Ident) 73 | if !ok { 74 | continue 75 | } 76 | if left.Name != right.Name { 77 | continue 78 | } 79 | } 80 | 81 | report(pass, assignStmt, right, i) 82 | } 83 | } 84 | } 85 | 86 | func checkForStmt(pass *analysis.Pass, forStmt *ast.ForStmt) { 87 | if forStmt.Init == nil { 88 | return 89 | } 90 | initAssignStmt, ok := forStmt.Init.(*ast.AssignStmt) 91 | if !ok { 92 | return 93 | } 94 | initVarNameMap := make(map[string]interface{}, len(initAssignStmt.Lhs)) 95 | for _, lh := range initAssignStmt.Lhs { 96 | if initVar, ok := lh.(*ast.Ident); ok { 97 | initVarNameMap[initVar.Name] = struct{}{} 98 | } 99 | } 100 | for _, stmt := range forStmt.Body.List { 101 | assignStmt, ok := stmt.(*ast.AssignStmt) 102 | if !ok { 103 | continue 104 | } 105 | if assignStmt.Tok != token.DEFINE { 106 | continue 107 | } 108 | for i, rh := range assignStmt.Rhs { 109 | right, ok := rh.(*ast.Ident) 110 | if !ok { 111 | continue 112 | } 113 | if _, ok := initVarNameMap[right.Name]; !ok { 114 | continue 115 | } 116 | if !checkAlias { 117 | left, ok := assignStmt.Lhs[i].(*ast.Ident) 118 | if !ok { 119 | continue 120 | } 121 | if left.Name != right.Name { 122 | continue 123 | } 124 | } 125 | 126 | report(pass, assignStmt, right, i) 127 | } 128 | } 129 | } 130 | 131 | func report(pass *analysis.Pass, assignStmt *ast.AssignStmt, right *ast.Ident, i int) { 132 | diagnostic := analysis.Diagnostic{ 133 | Pos: assignStmt.Pos(), 134 | Message: fmt.Sprintf(`The copy of the 'for' variable "%s" can be deleted (Go 1.22+)`, right.Name), 135 | } 136 | 137 | if i == 0 && isSimpleAssignStmt(assignStmt, right) { 138 | diagnostic.SuggestedFixes = append(diagnostic.SuggestedFixes, analysis.SuggestedFix{ 139 | TextEdits: []analysis.TextEdit{{ 140 | Pos: assignStmt.Pos(), 141 | End: assignStmt.End(), 142 | NewText: nil, 143 | }}, 144 | }) 145 | } 146 | 147 | pass.Report(diagnostic) 148 | } 149 | 150 | func isSimpleAssignStmt(assignStmt *ast.AssignStmt, rhs *ast.Ident) bool { 151 | if len(assignStmt.Lhs) != 1 { 152 | return false 153 | } 154 | 155 | lhs, ok := assignStmt.Lhs[0].(*ast.Ident) 156 | if !ok { 157 | return false 158 | } 159 | 160 | return rhs.Name == lhs.Name 161 | } 162 | --------------------------------------------------------------------------------