├── .github └── workflows │ ├── assign.yml │ ├── release.yml │ ├── review.yml │ └── test.yml ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── exportloopref │ └── main.go ├── exportloopref.go ├── exportloopref_test.go ├── go.mod ├── go.sum └── testdata └── src ├── complex └── complex.go ├── deep └── deep.go ├── deeppointer └── deeppointer.go ├── false └── false.go ├── fixed └── fixed.go ├── issue2 └── issue2.go ├── pslice └── pslice.go ├── reref ├── another_file.go └── issue13.go ├── simple └── simple.go └── struct └── struct.go /.github/workflows/assign.yml: -------------------------------------------------------------------------------- 1 | name: Issue assignment 2 | on: 3 | issues: 4 | types: [opened] 5 | jobs: 6 | auto-assign: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | steps: 11 | - name: 'Auto-assign issue' 12 | uses: pozil/auto-assign-issue@v1 13 | with: 14 | assignees: kyoh86 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to the GitHub Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | method: 6 | description: | 7 | Which number to increment in the semantic versioning. 8 | Set 'major', 'minor' or 'patch'. 9 | required: true 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check Actor 16 | if: github.actor != 'kyoh86' 17 | run: exit 1 18 | - name: Check Branch 19 | if: github.ref != 'refs/heads/main' 20 | run: exit 1 21 | - name: Wait Tests 22 | id: test_result 23 | uses: Sibz/await-status-action@v1.0.2 24 | with: 25 | contexts: test-status 26 | authToken: ${{ secrets.GITHUB_TOKEN }} 27 | timeout: 30 28 | - name: Check Test Result 29 | if: steps.test_result.outputs.result != 'success' 30 | run: | 31 | echo "feiled ${{ steps.test_result.outputs.failedCheckNames }}" 32 | echo "status ${{ steps.test_result.outputs.failedCheckStates }}" 33 | exit 1 34 | - name: Checkout Sources 35 | uses: actions/checkout@v3 36 | - name: Bump-up Semantic Version 37 | uses: kyoh86/git-vertag-action@v1 38 | with: 39 | # method: "major", "minor" or "patch" to update tag with semver 40 | method: "${{ github.event.inputs.method }}" 41 | - name: Setup Go 42 | uses: actions/setup-go@v3 43 | with: 44 | go-version: 1.19 45 | - name: Run GoReleaser 46 | uses: goreleaser/goreleaser-action@v3 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} 49 | with: 50 | args: release --rm-dist 51 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Review 2 | on: [pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: reviewdog/action-golangci-lint@v1 9 | with: 10 | level: info 11 | github_token: ${{ secrets.GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | jobs: 7 | test: 8 | name: Test local sources 9 | strategy: 10 | fail-fast: false 11 | max-parallel: 3 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Checkout Sources 17 | uses: actions/checkout@v3 18 | - name: Setup Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | - name: Test Go 23 | run: go test -v --race ./... 24 | test-release: 25 | name: Test releases 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout Sources 29 | uses: actions/checkout@v3 30 | - name: Setup Go 31 | uses: actions/setup-go@v3 32 | with: 33 | go-version: 1.19 34 | - name: Try Bump-up Semantic Version 35 | uses: kyoh86/git-vertag-action@v1 36 | with: 37 | method: "patch" 38 | - name: Run GoReleaser (dry-run) 39 | uses: goreleaser/goreleaser-action@v3 40 | with: 41 | args: release --rm-dist --skip-publish --snapshot 42 | test-others: 43 | name: Test others 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout Sources 47 | uses: actions/checkout@v3 48 | - name: Setup Go 49 | uses: actions/setup-go@v3 50 | with: 51 | go-version: 1.19 52 | - name: Search diagnostics 53 | uses: golangci/golangci-lint-action@v3 54 | with: 55 | version: v1.50 56 | - name: Take coverage 57 | run: go test -coverprofile=coverage.txt -covermode=atomic ./... 58 | - name: Send coverage 59 | uses: codecov/codecov-action@v3 60 | with: 61 | fail_ci_if_error: true 62 | files: coverage.txt 63 | test-status: 64 | name: Test status 65 | runs-on: ubuntu-latest 66 | needs: 67 | - test 68 | - test-others 69 | - test-release 70 | steps: 71 | - name: Set Check Status Success 72 | uses: Sibz/github-status-action@v1.1.6 73 | with: 74 | context: test-status 75 | authToken: ${{ secrets.GITHUB_TOKEN }} 76 | state: success 77 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - unparam 4 | - exportloopref 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | 3 | project_name: exportloopref 4 | builds: 5 | - id: default 6 | goos: 7 | - linux 8 | - darwin 9 | - windows 10 | goarch: 11 | - amd64 12 | - arm64 13 | - "386" 14 | main: ./cmd/exportloopref 15 | binary: exportloopref 16 | brews: 17 | - install: | 18 | bin.install "exportloopref" 19 | tap: 20 | owner: kyoh86 21 | name: homebrew-tap 22 | folder: Formula 23 | homepage: https://github.com/kyoh86/exportloopref 24 | description: An analyzer that finds exporting pointers for loop variables. 25 | license: MIT 26 | nfpms: 27 | - builds: 28 | - default 29 | maintainer: kyoh86 30 | homepage: https://github.com/kyoh86/exportloopref 31 | description: An analyzer that finds exporting pointers for loop variables. 32 | license: MIT 33 | formats: 34 | - apk 35 | - deb 36 | - rpm 37 | archives: 38 | - id: gzip 39 | format: tar.gz 40 | format_overrides: 41 | - goos: windows 42 | format: zip 43 | files: 44 | - licence* 45 | - LICENCE* 46 | - license* 47 | - LICENSE* 48 | - readme* 49 | - README* 50 | - changelog* 51 | - CHANGELOG* 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 kyoh86 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, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: gen lint test install man 2 | 3 | VERSION := `git vertag get` 4 | COMMIT := `git rev-parse HEAD` 5 | 6 | gen: 7 | go generate ./... 8 | 9 | lint: gen 10 | golangci-lint run 11 | 12 | test: lint 13 | go test -v --race ./... 14 | 15 | install: test 16 | go install -a -ldflags "-X=main.version=$(VERSION) -X=main.commit=$(COMMIT)" ./... 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exportloopref 2 | 3 | An analyzer that finds exporting pointers for loop variables. 4 | ![](https://repository-images.githubusercontent.com/256768552/a1c5bb80-dd73-11eb-9453-e520f517e730) 5 | Pin them all! 6 | 7 | **As of Go 1.22, this problem no longer occurs and fixed by Go team, see [here](https://go.dev/blog/loopvar-preview)** 8 | 9 | [![PkgGoDev](https://pkg.go.dev/badge/kyoh86/exportloopref)](https://pkg.go.dev/kyoh86/exportloopref) 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/kyoh86/exportloopref)](https://goreportcard.com/report/github.com/kyoh86/exportloopref) 11 | [![Coverage Status](https://img.shields.io/codecov/c/github/kyoh86/exportloopref.svg)](https://codecov.io/gh/kyoh86/exportloopref) 12 | [![Release](https://github.com/kyoh86/exportloopref/workflows/Release/badge.svg)](https://github.com/kyoh86/exportloopref/releases) 13 | 14 | ## What's this? 15 | 16 | Sample problem code from: https://github.com/kyoh86/exportloopref/blob/main/testdata/src/simple/simple.go 17 | 18 | ```go 19 | package main 20 | 21 | func main() { 22 | var intArray [4]*int 23 | var intSlice []*int 24 | var intRef *int 25 | var intStr struct{ x *int } 26 | 27 | println("loop expecting 10, 11, 12, 13") 28 | for i, p := range []int{10, 11, 12, 13} { 29 | printp(&p) // not a diagnostic 30 | intSlice = append(intSlice, &p) // want "exporting a pointer for the loop variable p" 31 | intArray[i] = &p // want "exporting a pointer for the loop variable p" 32 | if i%2 == 0 { 33 | intRef = &p // want "exporting a pointer for the loop variable p" 34 | intStr.x = &p // want "exporting a pointer for the loop variable p" 35 | } 36 | var vStr struct{ x *int } 37 | var vArray [4]*int 38 | var v *int 39 | if i%2 == 0 { 40 | v = &p // not a diagnostic (x is local variable) 41 | vArray[1] = &p // not a diagnostic (x is local variable) 42 | vStr.x = &p 43 | } 44 | _ = v 45 | } 46 | 47 | println(`slice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 48 | for _, p := range intSlice { 49 | printp(p) 50 | } 51 | println(`array expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 52 | for _, p := range intArray { 53 | printp(p) 54 | } 55 | println(`captured value expecting "12" but "13"`) 56 | printp(intRef) 57 | } 58 | 59 | func printp(p *int) { 60 | println(*p) 61 | } 62 | ``` 63 | 64 | In Go, the `p` variable in the above loops is actually a single variable. 65 | So in many case (like the above), using it makes for us annoying bugs. 66 | 67 | You can find them with `exportloopref`, and fix it. 68 | 69 | ```go 70 | package main 71 | 72 | func main() { 73 | var intArray [4]*int 74 | var intSlice []*int 75 | var intRef *int 76 | var intStr struct{ x *int } 77 | 78 | println("loop expecting 10, 11, 12, 13") 79 | for i, p := range []int{10, 11, 12, 13} { 80 | p := p // FIX variable into the local variable 81 | printp(&p) 82 | intSlice = append(intSlice, &p) 83 | intArray[i] = &p 84 | if i%2 == 0 { 85 | intRef = &p 86 | intStr.x = &p 87 | } 88 | var vStr struct{ x *int } 89 | var vArray [4]*int 90 | var v *int 91 | if i%2 == 0 { 92 | v = &p 93 | vArray[1] = &p 94 | vStr.x = &p 95 | } 96 | _ = v 97 | } 98 | 99 | println(`slice expecting "10, 11, 12, 13"`) 100 | for _, p := range intSlice { 101 | printp(p) 102 | } 103 | println(`array expecting "10, 11, 12, 13"`) 104 | for _, p := range intArray { 105 | printp(p) 106 | } 107 | println(`captured value expecting "12"`) 108 | printp(intRef) 109 | } 110 | 111 | func printp(p *int) { 112 | println(*p) 113 | } 114 | ``` 115 | 116 | ref: https://github.com/kyoh86/exportloopref/blob/main/testdata/src/fixed/fixed.go 117 | 118 | ## Sensing policy 119 | 120 | I want to make exportloopref as accurately as possible. 121 | So some cases of lints will be false-negative. 122 | 123 | e.g. 124 | 125 | ```go 126 | var s Foo 127 | for _, p := range []int{10, 11, 12, 13} { 128 | s.Bar(&p) // If s stores the pointer, it will be bug. 129 | } 130 | ``` 131 | 132 | If you want to report all of lints (with some false-positives), 133 | you should use [looppointer](https://github.com/kyoh86/looppointer). 134 | 135 | ### Known false negatives 136 | 137 | Case 1: pass the pointer to function to export. 138 | 139 | Case 2: pass the pointer to local variable, and export it. 140 | 141 | ```go 142 | package main 143 | 144 | type List []*int 145 | 146 | func (l *List) AppendP(p *int) { 147 | *l = append(*l, p) 148 | } 149 | 150 | func main() { 151 | var slice []*int 152 | list := List{} 153 | 154 | println("loop expect exporting 10, 11, 12, 13") 155 | for _, v := range []int{10, 11, 12, 13} { 156 | list.AppendP(&v) // Case 1: wanted "exporting a pointer for the loop variable v", but cannot be found 157 | 158 | p := &v // p is the local variable 159 | slice = append(slice, p) // Case 2: wanted "exporting a pointer for the loop variable v", but cannot be found 160 | } 161 | 162 | println(`slice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 163 | for _, p := range slice { 164 | printp(p) 165 | } 166 | println(`array expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 167 | for _, p := range ([]*int)(list) { 168 | printp(p) 169 | } 170 | } 171 | 172 | func printp(p *int) { 173 | println(*p) 174 | } 175 | ``` 176 | 177 | ## Install 178 | 179 | go: 180 | 181 | ```console 182 | $ go install github.com/kyoh86/exportloopref/cmd/exportloopref@latest 183 | ``` 184 | 185 | [homebrew](https://brew.sh/): 186 | 187 | ```console 188 | $ brew install kyoh86/tap/exportloopref 189 | ``` 190 | 191 | [gordon](https://github.com/kyoh86/gordon): 192 | 193 | ```console 194 | $ gordon install kyoh86/exportloopref 195 | ``` 196 | 197 | ## Usage 198 | 199 | ``` 200 | exportloopref [-flag] [package] 201 | ``` 202 | 203 | ### Flags 204 | 205 | | Flag | Description | 206 | | --- | --- | 207 | | -V | print version and exit | 208 | | -all | no effect (deprecated) | 209 | | -c int | display offending line with this many lines of context (default -1) | 210 | | -cpuprofile string | write CPU profile to this file | 211 | | -debug string | debug flags, any subset of "fpstv" | 212 | | -fix | apply all suggested fixes | 213 | | -flags | print analyzer flags in JSON | 214 | | -json | emit JSON output | 215 | | -memprofile string | write memory profile to this file | 216 | | -source | no effect (deprecated) | 217 | | -tags string | no effect (deprecated) | 218 | | -trace string | write trace log to this file | 219 | | -v | no effect (deprecated) | 220 | 221 | # LICENSE 222 | 223 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg)](http://www.opensource.org/licenses/MIT) 224 | 225 | This is distributed under the [MIT License](http://www.opensource.org/licenses/MIT). 226 | -------------------------------------------------------------------------------- /cmd/exportloopref/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/kyoh86/exportloopref" 5 | "golang.org/x/tools/go/analysis/singlechecker" 6 | ) 7 | 8 | func main() { 9 | singlechecker.Main(exportloopref.Analyzer) 10 | } 11 | -------------------------------------------------------------------------------- /exportloopref.go: -------------------------------------------------------------------------------- 1 | package exportloopref 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/token" 7 | "go/types" 8 | 9 | "golang.org/x/tools/go/analysis" 10 | "golang.org/x/tools/go/analysis/passes/inspect" 11 | "golang.org/x/tools/go/ast/inspector" 12 | ) 13 | 14 | var Analyzer = &analysis.Analyzer{ 15 | Name: "exportloopref", 16 | Doc: "checks for pointers to enclosing loop variables", 17 | Run: run, 18 | RunDespiteErrors: true, 19 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 20 | } 21 | 22 | func run(pass *analysis.Pass) (interface{}, error) { 23 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 24 | 25 | search := &Searcher{ 26 | LoopVars: map[token.Pos]struct{}{}, 27 | LocalVars: map[token.Pos]map[token.Pos]struct{}{}, 28 | Pass: pass, 29 | } 30 | 31 | nodeFilter := []ast.Node{ 32 | (*ast.RangeStmt)(nil), 33 | (*ast.ForStmt)(nil), 34 | (*ast.DeclStmt)(nil), 35 | (*ast.AssignStmt)(nil), 36 | (*ast.UnaryExpr)(nil), 37 | } 38 | 39 | inspect.WithStack(nodeFilter, search.CheckAndReport) 40 | 41 | return nil, nil 42 | } 43 | 44 | type Searcher struct { 45 | // LoopVars is positions that loop-variables are declared like below. 46 | // - for , := range ... 47 | // - for := ; ; 48 | LoopVars map[token.Pos]struct{} 49 | // LocalVars is positions of loops and the variables declared in them. 50 | // Use this to determine if a point assignment is an export outside the loop. 51 | LocalVars map[token.Pos]map[token.Pos]struct{} 52 | 53 | Pass *analysis.Pass 54 | } 55 | 56 | // CheckAndReport inspects each node with stack. 57 | // It is implemented as the I/F of the "golang.org/x/tools/go/analysis/passes/inspect".Analysis.WithStack. 58 | func (s *Searcher) CheckAndReport(n ast.Node, push bool, stack []ast.Node) bool { 59 | id, insert, digg := s.Check(n, stack) 60 | if id == nil { 61 | // no prob. 62 | return digg 63 | } 64 | 65 | // suggests fix 66 | var suggest []analysis.SuggestedFix 67 | if insert != token.NoPos { 68 | suggest = []analysis.SuggestedFix{{ 69 | Message: fmt.Sprintf("loop variable %s should be pinned", id.Name), 70 | TextEdits: []analysis.TextEdit{{ 71 | Pos: insert, 72 | End: insert, 73 | NewText: []byte(fmt.Sprintf("%[1]s := %[1]s\n", id.Name)), 74 | }}, 75 | }} 76 | } 77 | 78 | // report a diagnostic 79 | d := analysis.Diagnostic{Pos: id.Pos(), 80 | End: id.End(), 81 | Message: fmt.Sprintf("exporting a pointer for the loop variable %s", id.Name), 82 | Category: "exportloopref", 83 | SuggestedFixes: suggest, 84 | } 85 | s.Pass.Report(d) 86 | return digg 87 | } 88 | 89 | // Check each node and stack, whether it exports loop variables or not. 90 | // Finding export, report the *ast.Ident of exported loop variable, 91 | // and token.Pos to insert assignment to fix the diagnostic. 92 | func (s *Searcher) Check(n ast.Node, stack []ast.Node) (loopVar *ast.Ident, insertPos token.Pos, digg bool) { 93 | switch typed := n.(type) { 94 | case *ast.RangeStmt: 95 | s.parseRangeStmt(typed) 96 | case *ast.ForStmt: 97 | s.parseForStmt(typed) 98 | case *ast.DeclStmt: 99 | s.parseDeclStmt(typed, stack) 100 | case *ast.AssignStmt: 101 | s.parseAssignStmt(typed, stack) 102 | 103 | case *ast.UnaryExpr: 104 | return s.checkUnaryExpr(typed, stack) 105 | } 106 | return nil, token.NoPos, true 107 | } 108 | 109 | // parseRangeStmt will check range statement (i.e. `for , := range ...`), 110 | // and collect positions of and . 111 | func (s *Searcher) parseRangeStmt(n *ast.RangeStmt) { 112 | s.storeLoopVars(n.Key) 113 | s.storeLoopVars(n.Value) 114 | } 115 | 116 | // parseForStmt will check for statement (i.e. `for := ; ; `), 117 | // and collect positions of . 118 | func (s *Searcher) parseForStmt(n *ast.ForStmt) { 119 | switch post := n.Post.(type) { 120 | case *ast.AssignStmt: 121 | // e.g. for p = head; p != nil; p = p.next 122 | for _, lhs := range post.Lhs { 123 | s.storeLoopVars(lhs) 124 | } 125 | case *ast.IncDecStmt: 126 | // e.g. for i := 0; i < n; i++ 127 | s.storeLoopVars(post.X) 128 | } 129 | } 130 | 131 | func (s *Searcher) storeLoopVars(expr ast.Expr) { 132 | if id, ok := expr.(*ast.Ident); ok { 133 | s.LoopVars[id.Pos()] = struct{}{} 134 | } 135 | } 136 | 137 | // parseDeclStmt will parse declaring statement (i.e. `var`, `type`, `const`), 138 | // and store the position if it is "var" declaration and is in any loop. 139 | func (s *Searcher) parseDeclStmt(n *ast.DeclStmt, stack []ast.Node) { 140 | genDecl, ok := n.Decl.(*ast.GenDecl) 141 | if !ok { 142 | // (dead branch) 143 | // if the Decl is not GenDecl (i.e. `var`, `type` or `const` statement), it is ignored 144 | return 145 | } 146 | if genDecl.Tok != token.VAR { 147 | // if the Decl is not `var` (may be `type` or `const`), it is ignored 148 | return 149 | } 150 | 151 | loop, _ := s.innermostLoop(stack) 152 | if loop == nil { 153 | return 154 | } 155 | 156 | // Register declared variables 157 | for _, spec := range genDecl.Specs { 158 | for _, name := range spec.(*ast.ValueSpec).Names { 159 | s.storeLocalVar(loop, name) 160 | } 161 | } 162 | } 163 | 164 | // parseDeclStmt will parse assignment statement (i.e. ` = `), 165 | // and store the position if it is . 166 | func (s *Searcher) parseAssignStmt(n *ast.AssignStmt, stack []ast.Node) { 167 | if n.Tok != token.DEFINE { 168 | // if the statement is simple assignment (without definement), it is ignored 169 | return 170 | } 171 | 172 | loop, _ := s.innermostLoop(stack) 173 | if loop == nil { 174 | return 175 | } 176 | 177 | // Find statements declaring local variable 178 | for _, h := range n.Lhs { 179 | s.storeLocalVar(loop, h) 180 | } 181 | } 182 | 183 | func (s *Searcher) storeLocalVar(loop ast.Node, expr ast.Expr) { 184 | loopPos := loop.Pos() 185 | id, ok := expr.(*ast.Ident) 186 | if !ok { 187 | return 188 | } 189 | vars, ok := s.LocalVars[loopPos] 190 | if !ok { 191 | vars = map[token.Pos]struct{}{} 192 | } 193 | vars[id.Obj.Pos()] = struct{}{} 194 | s.LocalVars[loopPos] = vars 195 | } 196 | 197 | func insertionPosition(block *ast.BlockStmt) token.Pos { 198 | if len(block.List) > 0 { 199 | return block.List[0].Pos() 200 | } 201 | return token.NoPos 202 | } 203 | 204 | func (s *Searcher) innermostLoop(stack []ast.Node) (ast.Node, token.Pos) { 205 | for i := len(stack) - 1; i >= 0; i-- { 206 | switch typed := stack[i].(type) { 207 | case *ast.RangeStmt: 208 | return typed, insertionPosition(typed.Body) 209 | case *ast.ForStmt: 210 | return typed, insertionPosition(typed.Body) 211 | } 212 | } 213 | return nil, token.NoPos 214 | } 215 | 216 | // checkUnaryExpr check unary expression (i.e. like `-x`, `*p` or `&v`) and stack. 217 | // THIS IS THE ESSENTIAL PART OF THIS PARSER. 218 | func (s *Searcher) checkUnaryExpr(n *ast.UnaryExpr, stack []ast.Node) (*ast.Ident, token.Pos, bool) { 219 | if n.Op != token.AND { 220 | return nil, token.NoPos, true 221 | } 222 | 223 | loop, insert := s.innermostLoop(stack) 224 | if loop == nil { 225 | return nil, token.NoPos, true 226 | } 227 | 228 | // Get identity of the referred item 229 | id := s.getIdentity(n.X) 230 | if id == nil { 231 | return nil, token.NoPos, true 232 | } 233 | 234 | // If the identity is not the loop statement variable, 235 | // it will not be reported. 236 | if _, isDecl := s.LoopVars[id.Obj.Pos()]; !isDecl { 237 | return nil, token.NoPos, true 238 | } 239 | 240 | // check stack append(), []X{}, map[Type]X{}, Struct{}, &Struct{}, X.(Type), (X) 241 | // in the = 242 | var mayRHPos token.Pos 243 | for i := len(stack) - 2; i >= 0; i-- { 244 | switch typed := stack[i].(type) { 245 | case (*ast.UnaryExpr): 246 | // noop 247 | case (*ast.CompositeLit): 248 | // noop 249 | case (*ast.KeyValueExpr): 250 | // noop 251 | case (*ast.CallExpr): 252 | fun, ok := typed.Fun.(*ast.Ident) 253 | if !ok { 254 | return nil, token.NoPos, false // it's calling a function other of `append`. It cannot be checked 255 | } 256 | 257 | if fun.Name != "append" { 258 | return nil, token.NoPos, false // it's calling a function other of `append`. It cannot be checked 259 | } 260 | 261 | case (*ast.AssignStmt): 262 | if len(typed.Rhs) != len(typed.Lhs) { 263 | return nil, token.NoPos, false // dead logic 264 | } 265 | 266 | // search x where Rhs[x].Pos() == mayRHPos 267 | var index int 268 | for ri, rh := range typed.Rhs { 269 | if rh.Pos() == mayRHPos { 270 | index = ri 271 | break 272 | } 273 | } 274 | 275 | // check Lhs[x] is not local variable 276 | lh := typed.Lhs[index] 277 | isVar := s.isVar(loop, lh) 278 | if !isVar { 279 | return id, insert, false 280 | } 281 | 282 | return nil, token.NoPos, true 283 | default: 284 | // Other statement is not able to be checked. 285 | return nil, token.NoPos, false 286 | } 287 | 288 | // memory an expr that may be right-hand in the AssignStmt 289 | mayRHPos = stack[i].Pos() 290 | } 291 | return nil, token.NoPos, true 292 | } 293 | 294 | func (s *Searcher) isVar(loop ast.Node, expr ast.Expr) bool { 295 | vars := s.LocalVars[loop.Pos()] // map[token.Pos]struct{} 296 | if vars == nil { 297 | return false 298 | } 299 | switch typed := expr.(type) { 300 | case (*ast.Ident): 301 | if typed.Obj == nil { 302 | return false // global var in another file (ref: #13) 303 | } 304 | _, isVar := vars[typed.Obj.Pos()] 305 | return isVar 306 | case (*ast.IndexExpr): // like X[Y], check X 307 | return s.isVar(loop, typed.X) 308 | case (*ast.SelectorExpr): // like X.Y, check X 309 | return s.isVar(loop, typed.X) 310 | } 311 | return false 312 | } 313 | 314 | // Get variable identity 315 | func (s *Searcher) getIdentity(expr ast.Expr) *ast.Ident { 316 | switch typed := expr.(type) { 317 | case *ast.SelectorExpr: 318 | // Ignore if the parent is pointer ref (fix for #2) 319 | if _, ok := s.Pass.TypesInfo.Types[typed.X].Type.(*types.Pointer); ok { 320 | return nil 321 | } 322 | 323 | // Get parent identity; i.e. `a.b` of the `a.b.c`. 324 | return s.getIdentity(typed.X) 325 | 326 | case *ast.Ident: 327 | // Get simple identity; i.e. `a` of the `a`. 328 | if typed.Obj == nil { 329 | return nil 330 | } 331 | return typed 332 | } 333 | return nil 334 | } 335 | -------------------------------------------------------------------------------- /exportloopref_test.go: -------------------------------------------------------------------------------- 1 | package exportloopref_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kyoh86/exportloopref" 7 | "golang.org/x/tools/go/analysis/analysistest" 8 | ) 9 | 10 | func TestSimple(t *testing.T) { 11 | testdata := analysistest.TestData() 12 | analysistest.Run(t, testdata, exportloopref.Analyzer, "simple") 13 | } 14 | 15 | func TestStruct(t *testing.T) { 16 | testdata := analysistest.TestData() 17 | analysistest.Run(t, testdata, exportloopref.Analyzer, "struct") 18 | } 19 | 20 | func TestComplex(t *testing.T) { 21 | testdata := analysistest.TestData() 22 | analysistest.Run(t, testdata, exportloopref.Analyzer, "complex") 23 | } 24 | 25 | func TestFixed(t *testing.T) { 26 | testdata := analysistest.TestData() 27 | analysistest.Run(t, testdata, exportloopref.Analyzer, "fixed") 28 | } 29 | 30 | func TestPslice(t *testing.T) { 31 | testdata := analysistest.TestData() 32 | analysistest.Run(t, testdata, exportloopref.Analyzer, "pslice") 33 | } 34 | 35 | func TestIssue2(t *testing.T) { 36 | testdata := analysistest.TestData() 37 | analysistest.Run(t, testdata, exportloopref.Analyzer, "issue2") 38 | } 39 | 40 | func TestDeep(t *testing.T) { 41 | testdata := analysistest.TestData() 42 | analysistest.Run(t, testdata, exportloopref.Analyzer, "deep") 43 | } 44 | 45 | func TestDepPointer(t *testing.T) { 46 | testdata := analysistest.TestData() 47 | analysistest.Run(t, testdata, exportloopref.Analyzer, "deeppointer") 48 | } 49 | 50 | func TestReRef(t *testing.T) { 51 | testdata := analysistest.TestData() 52 | analysistest.Run(t, testdata, exportloopref.Analyzer, "reref") 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kyoh86/exportloopref 2 | 3 | go 1.18 4 | 5 | require golang.org/x/tools v0.2.0 6 | 7 | require ( 8 | golang.org/x/mod v0.6.0 // indirect 9 | golang.org/x/sys v0.1.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= 2 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 3 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 4 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= 6 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 7 | -------------------------------------------------------------------------------- /testdata/src/complex/complex.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | var ref struct { 5 | x struct { 6 | y []map[string]*int 7 | z []map[string]*int 8 | } 9 | } 10 | for i, p := range []int{10, 11, 12, 13} { 11 | if i%2 == 0 { 12 | ref = struct { 13 | x struct { 14 | y []map[string]*int 15 | z []map[string]*int 16 | } 17 | }{ 18 | x: struct { 19 | y []map[string]*int 20 | z []map[string]*int 21 | }{ 22 | y: []map[string]*int{{ 23 | "baz": &p, // want "exporting a pointer for the loop variable p" 24 | }}, 25 | }, 26 | } 27 | ref.x.y = append(ref.x.y, map[string]*int{"baz": &p}) // want "exporting a pointer for the loop variable p" 28 | ref.x.z = append( 29 | ref.x.z, 30 | append( 31 | []map[string]*int{{"baz": &p}}, // want "exporting a pointer for the loop variable p" 32 | map[string]*int{"baz": &p}, // want "exporting a pointer for the loop variable p" 33 | map[string]*int{}, 34 | )..., 35 | ) 36 | } 37 | } 38 | 39 | println(`captured value expecting "12" but "13"`) 40 | printp(ref.x.y[0]["baz"]) 41 | printp(ref.x.y[1]["baz"]) 42 | printp(ref.x.z[0]["baz"]) 43 | printp(ref.x.z[1]["baz"]) 44 | 45 | } 46 | 47 | func printp(p *int) { 48 | println(*p) 49 | } 50 | -------------------------------------------------------------------------------- /testdata/src/deep/deep.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Container struct { 4 | Inner 5 | } 6 | 7 | type Inner struct { 8 | Number int 9 | } 10 | 11 | func main() { 12 | var array [4]*int 13 | var slice []*int 14 | var ref *int 15 | var str struct{ x *int } 16 | 17 | var target = []Container{{Inner{10}}, {Inner{11}}, {Inner{12}}, {Inner{13}}} 18 | 19 | // access to unsafe member 20 | println("loop expecting 10, 11, 12, 13") 21 | for i, p := range target { 22 | printp(&p.Inner.Number) 23 | slice = append(slice, &p.Inner.Number) // want "exporting a pointer for the loop variable p" 24 | array[i] = &p.Inner.Number // want "exporting a pointer for the loop variable p" 25 | if i%2 == 0 { 26 | ref = &p.Inner.Number // want "exporting a pointer for the loop variable p" 27 | str.x = &p.Inner.Number // want "exporting a pointer for the loop variable p" 28 | } 29 | } 30 | 31 | println(`slice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 32 | for _, p := range slice { 33 | printp(p) 34 | } 35 | println(`array expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 36 | for _, p := range array { 37 | printp(p) 38 | } 39 | println(`captured value expecting "12" but "13"`) 40 | printp(ref) 41 | } 42 | 43 | func printp(p *int) { 44 | println(*p) 45 | } 46 | -------------------------------------------------------------------------------- /testdata/src/deeppointer/deeppointer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Container struct { 4 | Pointer *Inner 5 | } 6 | 7 | type Inner struct { 8 | Number int 9 | } 10 | 11 | func main() { 12 | var array [4]*int 13 | var slice []*int 14 | var ref *int 15 | var str struct{ x *int } 16 | 17 | target := []Container{{&Inner{10}}, {&Inner{11}}, {&Inner{12}}, {&Inner{13}}} 18 | 19 | // access to unsafe member 20 | println("loop expecting 10, 11, 12, 13") 21 | for i, p := range target { 22 | printp(&p.Pointer.Number) 23 | slice = append(slice, &p.Pointer.Number) 24 | array[i] = &p.Pointer.Number 25 | if i%2 == 0 { 26 | ref = &p.Pointer.Number 27 | str.x = &p.Pointer.Number 28 | } 29 | } 30 | 31 | println(`slice expecting "10, 11, 12, 13"`) 32 | for _, p := range slice { 33 | printp(p) 34 | } 35 | println(`array expecting "10, 11, 12, 13"`) 36 | for _, p := range array { 37 | printp(p) 38 | } 39 | println(`captured value expecting "12"`) 40 | printp(ref) 41 | } 42 | 43 | func printp(p *int) { 44 | println(*p) 45 | } 46 | -------------------------------------------------------------------------------- /testdata/src/false/false.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type List []*int 4 | 5 | func (l *List) AppendP(p *int) { 6 | *l = append(*l, p) 7 | } 8 | 9 | func main() { 10 | var slice []*int 11 | list := List{} 12 | 13 | println("loop expect exporting 10, 11, 12, 13") 14 | for _, v := range []int{10, 11, 12, 13} { 15 | list.AppendP(&v) // wanted "exporting a pointer for the loop variable v", but cannot be found 16 | 17 | p := &v // p is the local variable 18 | slice = append(slice, p) // wanted "exporting a pointer for the loop variable v", but cannot be found 19 | } 20 | 21 | println(`slice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 22 | for _, p := range slice { 23 | printp(p) 24 | } 25 | println(`array expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 26 | for _, p := range ([]*int)(list) { 27 | printp(p) 28 | } 29 | } 30 | 31 | func printp(p *int) { 32 | println(*p) 33 | } 34 | -------------------------------------------------------------------------------- /testdata/src/fixed/fixed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | var array [4]*int 5 | var slice []*int 6 | var ref *int 7 | var str struct{ x *int } 8 | 9 | println("loop expecting 10, 11, 12, 13") 10 | for i, p := range []int{10, 11, 12, 13} { 11 | p := p // FIX variable into the local variable 12 | printp(&p) 13 | slice = append(slice, &p) 14 | array[i] = &p 15 | if i%2 == 0 { 16 | ref = &p 17 | str.x = &p 18 | } 19 | var vStr struct{ x *int } 20 | var vArray [4]*int 21 | var v *int 22 | if i%2 == 0 { 23 | v = &p 24 | vArray[1] = &p 25 | vStr.x = &p 26 | } 27 | _ = v 28 | } 29 | 30 | println(`slice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 31 | for _, p := range slice { 32 | printp(p) 33 | } 34 | println(`array expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 35 | for _, p := range array { 36 | printp(p) 37 | } 38 | println(`captured value expecting "12" but "13"`) 39 | printp(ref) 40 | } 41 | 42 | func printp(p *int) { 43 | println(*p) 44 | } 45 | -------------------------------------------------------------------------------- /testdata/src/issue2/issue2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Issue #2 (https://github.com/kyoh86/exportloopref/issues/2 4 | // This is valid code because n takes new pointer values, so &n.name points correctly to different names. 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | type Node struct { 11 | name string 12 | } 13 | 14 | type Nodes []*Node 15 | 16 | type Graph struct { 17 | nodes Nodes 18 | } 19 | 20 | func main() { 21 | var graph Graph 22 | var s *string 23 | 24 | graph.nodes = Nodes{&Node{"hello"}, &Node{"world"}, &Node{"foo"}, &Node{"bar"}, &Node{"baz"}} 25 | 26 | for i, n := range graph.nodes { 27 | if i == 2 { 28 | s = &n.name // here 29 | } 30 | } 31 | 32 | fmt.Println(*s) 33 | } 34 | -------------------------------------------------------------------------------- /testdata/src/pslice/pslice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | var pslice []*int 5 | var ppslice []**int 6 | 7 | println("loop expecting 10, 11, 12, 13") 8 | for _, p := range []int{10, 11, 12, 13} { 9 | p := p 10 | pslice = append(pslice, &p) // not a diagnostic (fixed p) 11 | } 12 | 13 | for _, p := range pslice { 14 | ppslice = append(ppslice, &p) // want "exporting a pointer for the loop variable p" 15 | } 16 | 17 | println(`ppslice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 18 | for _, p := range ppslice { 19 | printp(*p) 20 | } 21 | } 22 | 23 | func printp(p *int) { 24 | println(*p) 25 | } 26 | -------------------------------------------------------------------------------- /testdata/src/reref/another_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Moving this map to main.go fixes nil pointer deference 4 | var globalMapInDifferentFile = map[int]*MyStruct{} 5 | -------------------------------------------------------------------------------- /testdata/src/reref/issue13.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type MyStruct struct { 4 | MyStructPtrField *int 5 | } 6 | 7 | func main() { 8 | localVal := 0 9 | arr := []MyStruct{{&localVal}} 10 | for _, p := range arr { 11 | t := *p.MyStructPtrField 12 | globalMapInDifferentFile[t] = &p // want "exporting a pointer for the loop variable p" 13 | } 14 | -------------------------------------------------------------------------------- /testdata/src/simple/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | var array [4]*int 5 | var slice []*int 6 | var ref *int 7 | var str struct{ x *int } 8 | 9 | println("loop expecting 10, 11, 12, 13") 10 | for i, p := range []int{10, 11, 12, 13} { 11 | printp(&p) // not a diagnostic 12 | slice = append(slice, &p) // want "exporting a pointer for the loop variable p" 13 | array[i] = &p // want "exporting a pointer for the loop variable p" 14 | if i%2 == 0 { 15 | ref = &p // want "exporting a pointer for the loop variable p" 16 | str.x = &p // want "exporting a pointer for the loop variable p" 17 | } 18 | var vStr struct{ x *int } 19 | var vArray [4]*int 20 | var v *int 21 | if i%2 == 0 { 22 | v = &p // not a diagnostic (x is local variable) 23 | vArray[1] = &p // not a diagnostic (x is local variable) 24 | vStr.x = &p 25 | } 26 | _ = v 27 | } 28 | 29 | println(`slice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 30 | for _, p := range slice { 31 | printp(p) 32 | } 33 | println(`array expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 34 | for _, p := range array { 35 | printp(p) 36 | } 37 | println(`captured value expecting "12" but "13"`) 38 | printp(ref) 39 | } 40 | 41 | func printp(p *int) { 42 | println(*p) 43 | } 44 | -------------------------------------------------------------------------------- /testdata/src/struct/struct.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | type singleLayer struct { 5 | num int 6 | } 7 | 8 | var array [4]*int 9 | var slice []*int 10 | var ref *int 11 | 12 | println("loop expecting 10, 11, 12, 13") 13 | for i, p := range []singleLayer{{10}, {11}, {12}, {13}} { 14 | printp(&p.num) // not a diagnostic 15 | slice = append(slice, &p.num) // want "exporting a pointer for the loop variable p" 16 | array[i] = &p.num // want "exporting a pointer for the loop variable p" 17 | if i%2 == 0 { 18 | ref = &p.num // want "exporting a pointer for the loop variable p" 19 | } 20 | } 21 | 22 | println(`slice expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 23 | for _, p := range slice { 24 | printp(p) 25 | } 26 | println(`array expecting "10, 11, 12, 13" but "13, 13, 13, 13"`) 27 | for _, p := range array { 28 | printp(p) 29 | } 30 | println(`captured value expecting "12" but "13"`) 31 | printp(ref) 32 | } 33 | 34 | func printp(p *int) { 35 | println(*p) 36 | } 37 | --------------------------------------------------------------------------------