Skip to content

Commit 84c9c65

Browse files
committed
fix #243: support Scopelint linter
1 parent ccac35a commit 84c9c65

File tree

13 files changed

+176
-1
lines changed

13 files changed

+176
-1
lines changed

Gopkg.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ lll: Reports long lines [fast: true]
199199
unparam: Reports unused function parameters [fast: false]
200200
nakedret: Finds naked returns in functions greater than a specified function length [fast: true]
201201
prealloc: Finds slice declarations that could potentially be preallocated [fast: true]
202+
scopelint: Scopelint checks for unpinned variables in go programs [fast: true]
202203
```
203204
204205
Pass `-E/--enable` to enable linter and `-D/--disable` to disable:
@@ -398,6 +399,7 @@ golangci-lint help linters
398399
- [unparam](https://github.com/mvdan/unparam) - Reports unused function parameters
399400
- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns in functions greater than a specified function length
400401
- [prealloc](https://github.com/alexkohler/prealloc) - Finds slice declarations that could potentially be preallocated
402+
- [scopelint](https://github.com/kyoh86/scopelint) - Scopelint checks for unpinned variables in go programs
401403
402404
## Configuration
403405
@@ -807,6 +809,7 @@ Thanks to developers and authors of used linters:
807809
- [client9](https://github.com/client9)
808810
- [walle](https://github.com/walle)
809811
- [alexkohler](https://github.com/alexkohler)
812+
- [kyoh86](https://github.com/kyoh86)
810813
811814
## Changelog
812815

pkg/golinters/scopelint.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package golinters
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"go/ast"
7+
"go/token"
8+
9+
"github.com/golangci/golangci-lint/pkg/lint/linter"
10+
"github.com/golangci/golangci-lint/pkg/result"
11+
)
12+
13+
type Scopelint struct{}
14+
15+
func (Scopelint) Name() string {
16+
return "scopelint"
17+
}
18+
19+
func (Scopelint) Desc() string {
20+
return "Scopelint checks for unpinned variables in go programs"
21+
}
22+
23+
func (lint Scopelint) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) {
24+
var res []result.Issue
25+
26+
for _, f := range lintCtx.ASTCache.GetAllValidFiles() {
27+
n := Node{
28+
fset: f.Fset,
29+
dangerObjects: map[*ast.Object]struct{}{},
30+
unsafeObjects: map[*ast.Object]struct{}{},
31+
skipFuncs: map[*ast.FuncLit]struct{}{},
32+
issues: &res,
33+
}
34+
ast.Walk(&n, f.F)
35+
}
36+
37+
return res, nil
38+
}
39+
40+
// The code below is copy-pasted from https://github.com/kyoh86/scopelint
41+
42+
// Node represents a Node being linted.
43+
type Node struct {
44+
fset *token.FileSet
45+
dangerObjects map[*ast.Object]struct{}
46+
unsafeObjects map[*ast.Object]struct{}
47+
skipFuncs map[*ast.FuncLit]struct{}
48+
issues *[]result.Issue
49+
}
50+
51+
// Visit method is invoked for each node encountered by Walk.
52+
// If the result visitor w is not nil, Walk visits each of the children
53+
// of node with the visitor w, followed by a call of w.Visit(nil).
54+
//nolint:gocyclo
55+
func (f *Node) Visit(node ast.Node) ast.Visitor {
56+
switch typedNode := node.(type) {
57+
case *ast.ForStmt:
58+
switch init := typedNode.Init.(type) {
59+
case *ast.AssignStmt:
60+
for _, lh := range init.Lhs {
61+
switch tlh := lh.(type) {
62+
case *ast.Ident:
63+
f.unsafeObjects[tlh.Obj] = struct{}{}
64+
}
65+
}
66+
}
67+
68+
case *ast.RangeStmt:
69+
// Memory variables declarated in range statement
70+
switch k := typedNode.Key.(type) {
71+
case *ast.Ident:
72+
f.unsafeObjects[k.Obj] = struct{}{}
73+
}
74+
switch v := typedNode.Value.(type) {
75+
case *ast.Ident:
76+
f.unsafeObjects[v.Obj] = struct{}{}
77+
}
78+
79+
case *ast.UnaryExpr:
80+
if typedNode.Op == token.AND {
81+
switch ident := typedNode.X.(type) {
82+
case *ast.Ident:
83+
if _, unsafe := f.unsafeObjects[ident.Obj]; unsafe {
84+
f.errorf(ident, "Using a reference for the variable on range scope %s", formatCode(ident.Name, nil))
85+
}
86+
}
87+
}
88+
89+
case *ast.Ident:
90+
if _, obj := f.dangerObjects[typedNode.Obj]; obj {
91+
// It is the naked variable in scope of range statement.
92+
f.errorf(node, "Using the variable on range scope %s in function literal", formatCode(typedNode.Name, nil))
93+
break
94+
}
95+
96+
case *ast.CallExpr:
97+
// Ignore func literals that'll be called immediately.
98+
switch funcLit := typedNode.Fun.(type) {
99+
case *ast.FuncLit:
100+
f.skipFuncs[funcLit] = struct{}{}
101+
}
102+
103+
case *ast.FuncLit:
104+
if _, skip := f.skipFuncs[typedNode]; !skip {
105+
dangers := map[*ast.Object]struct{}{}
106+
for d := range f.dangerObjects {
107+
dangers[d] = struct{}{}
108+
}
109+
for u := range f.unsafeObjects {
110+
dangers[u] = struct{}{}
111+
}
112+
return &Node{
113+
fset: f.fset,
114+
dangerObjects: dangers,
115+
unsafeObjects: f.unsafeObjects,
116+
skipFuncs: f.skipFuncs,
117+
issues: f.issues,
118+
}
119+
}
120+
}
121+
return f
122+
}
123+
124+
// The variadic arguments may start with link and category types,
125+
// and must end with a format string and any arguments.
126+
// It returns the new Problem.
127+
//nolint:interfacer
128+
func (f *Node) errorf(n ast.Node, format string, args ...interface{}) {
129+
pos := f.fset.Position(n.Pos())
130+
f.errorfAt(pos, format, args...)
131+
}
132+
133+
func (f *Node) errorfAt(pos token.Position, format string, args ...interface{}) {
134+
*f.issues = append(*f.issues, result.Issue{
135+
Pos: pos,
136+
Text: fmt.Sprintf(format, args...),
137+
FromLinter: Scopelint{}.Name(),
138+
})
139+
}

pkg/lint/lintersdb/enabled_set_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func TestGetEnabledLintersSet(t *testing.T) {
8383
m := NewManager()
8484
es := NewEnabledSet(m, NewValidator(m), nil, nil)
8585
for _, c := range cases {
86+
c := c
8687
t.Run(c.name, func(t *testing.T) {
8788
defaultLinters := []linter.Config{}
8889
for _, ln := range c.def {

pkg/lint/lintersdb/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func (m Manager) GetLinterConfig(name string) *linter.Config {
4949
func enableLinterConfigs(lcs []linter.Config, isEnabled func(lc *linter.Config) bool) []linter.Config {
5050
var ret []linter.Config
5151
for _, lc := range lcs {
52+
lc := lc
5253
lc.EnabledByDefault = isEnabled(&lc)
5354
ret = append(ret, lc)
5455
}
@@ -186,6 +187,10 @@ func (Manager) GetAllSupportedLinterConfigs() []linter.Config {
186187
WithPresets(linter.PresetPerformance).
187188
WithSpeed(8).
188189
WithURL("https://github.com/alexkohler/prealloc"),
190+
linter.NewConfig(golinters.Scopelint{}).
191+
WithPresets(linter.PresetBugs).
192+
WithSpeed(8).
193+
WithURL("https://github.com/kyoh86/scopelint"),
189194
}
190195

191196
isLocalRun := os.Getenv("GOLANGCI_COM_RUN") == ""

pkg/lint/runner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ func (r Runner) processLintResults(inCh <-chan lintRes) <-chan lintRes {
222222
// finalize processors: logging, clearing, no heavy work here
223223

224224
for _, p := range r.Processors {
225+
p := p
225226
sw.TrackStage(p.Name(), func() {
226227
p.Finish()
227228
})
@@ -273,6 +274,7 @@ func (r *Runner) processIssues(issues []result.Issue, sw *timeutils.Stopwatch) [
273274
for _, p := range r.Processors {
274275
var newIssues []result.Issue
275276
var err error
277+
p := p
276278
sw.TrackStage(p.Name(), func() {
277279
newIssues, err = p.Process(issues)
278280
})

pkg/printers/tab.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func (p *Tab) Print(ctx context.Context, issues <-chan result.Issue) error {
3333
w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0)
3434

3535
for i := range issues {
36+
i := i
3637
p.printIssue(&i, w)
3738
}
3839

pkg/printers/text.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func (p Text) SprintfColored(ca color.Attribute, format string, args ...interfac
3838

3939
func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) error {
4040
for i := range issues {
41+
i := i
4142
p.printIssue(&i)
4243

4344
if !p.printIssuedLine {

pkg/result/processors/nolint.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ func (e *rangeExpander) Visit(node ast.Node) ast.Visitor {
148148
var foundRange *ignoredRange
149149
for _, r := range e.inlineRanges {
150150
if r.To == nodeStartLine-1 && nodeStartPos.Column == r.col {
151+
r := r
151152
foundRange = &r
152153
break
153154
}

pkg/result/processors/nolint_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ func TestNolintAliases(t *testing.T) {
153153

154154
p := newTestNolintProcessor(getOkLogger(ctrl))
155155
for _, line := range []int{47, 49, 51} {
156+
line := line
156157
t.Run(fmt.Sprintf("line-%d", line), func(t *testing.T) {
157158
processAssertEmpty(t, p, newNolintFileIssue(line, "gosec"))
158159
})

pkg/result/processors/utils.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
func filterIssues(issues []result.Issue, filter func(i *result.Issue) bool) []result.Issue {
1010
retIssues := make([]result.Issue, 0, len(issues))
1111
for _, i := range issues {
12+
i := i
1213
if filter(&i) {
1314
retIssues = append(retIssues, i)
1415
}
@@ -20,6 +21,7 @@ func filterIssues(issues []result.Issue, filter func(i *result.Issue) bool) []re
2021
func filterIssuesErr(issues []result.Issue, filter func(i *result.Issue) (bool, error)) ([]result.Issue, error) {
2122
retIssues := make([]result.Issue, 0, len(issues))
2223
for _, i := range issues {
24+
i := i
2325
ok, err := filter(&i)
2426
if err != nil {
2527
return nil, fmt.Errorf("can't filter issue %#v: %s", i, err)
@@ -36,6 +38,7 @@ func filterIssuesErr(issues []result.Issue, filter func(i *result.Issue) (bool,
3638
func transformIssues(issues []result.Issue, transform func(i *result.Issue) *result.Issue) []result.Issue {
3739
retIssues := make([]result.Issue, 0, len(issues))
3840
for _, i := range issues {
41+
i := i
3942
newI := transform(&i)
4043
if newI != nil {
4144
retIssues = append(retIssues, *newI)

test/run_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ func TestEnabledLinters(t *testing.T) {
368368
}
369369

370370
for _, c := range cases {
371+
c := c
371372
t.Run(c.name, func(t *testing.T) {
372373
runArgs := []string{"-v"}
373374
if !c.noImplicitFast {

test/testdata/scopelint.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// args: -Escopelint
2+
package testdata
3+
4+
import "fmt"
5+
6+
func ScopelintTest() {
7+
values := []string{"a", "b", "c"}
8+
var funcs []func()
9+
for _, val := range values {
10+
funcs = append(funcs, func() {
11+
fmt.Println(val) // ERROR "Using the variable on range scope `val` in function literal"
12+
})
13+
}
14+
for _, f := range funcs {
15+
f()
16+
}
17+
}

0 commit comments

Comments
 (0)