fix #243: support Scopelint linter

This commit is contained in:
Denis Isaev 2018-11-05 22:29:45 +03:00 committed by Isaev Denis
parent ccac35a87e
commit 84c9c65f39
13 changed files with 176 additions and 1 deletions

2
Gopkg.lock generated
View File

@ -555,7 +555,7 @@
[[projects]]
branch = "master"
digest = "1:aac721c5ec779073b35cf7a51b7764d13f6db2243d650965d2c71469e8994e5b"
digest = "1:c09b8b20f35b802fa418b0715e3147d6366b706c6003bbdef9df78272ffe80a5"
name = "golang.org/x/tools"
packages = [
"go/ast/astutil",

View File

@ -199,6 +199,7 @@ lll: Reports long lines [fast: true]
unparam: Reports unused function parameters [fast: false]
nakedret: Finds naked returns in functions greater than a specified function length [fast: true]
prealloc: Finds slice declarations that could potentially be preallocated [fast: true]
scopelint: Scopelint checks for unpinned variables in go programs [fast: true]
```
Pass `-E/--enable` to enable linter and `-D/--disable` to disable:
@ -398,6 +399,7 @@ golangci-lint help linters
- [unparam](https://github.com/mvdan/unparam) - Reports unused function parameters
- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns in functions greater than a specified function length
- [prealloc](https://github.com/alexkohler/prealloc) - Finds slice declarations that could potentially be preallocated
- [scopelint](https://github.com/kyoh86/scopelint) - Scopelint checks for unpinned variables in go programs
## Configuration
@ -807,6 +809,7 @@ Thanks to developers and authors of used linters:
- [client9](https://github.com/client9)
- [walle](https://github.com/walle)
- [alexkohler](https://github.com/alexkohler)
- [kyoh86](https://github.com/kyoh86)
## Changelog

139
pkg/golinters/scopelint.go Normal file
View File

@ -0,0 +1,139 @@
package golinters
import (
"context"
"fmt"
"go/ast"
"go/token"
"github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/result"
)
type Scopelint struct{}
func (Scopelint) Name() string {
return "scopelint"
}
func (Scopelint) Desc() string {
return "Scopelint checks for unpinned variables in go programs"
}
func (lint Scopelint) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) {
var res []result.Issue
for _, f := range lintCtx.ASTCache.GetAllValidFiles() {
n := Node{
fset: f.Fset,
dangerObjects: map[*ast.Object]struct{}{},
unsafeObjects: map[*ast.Object]struct{}{},
skipFuncs: map[*ast.FuncLit]struct{}{},
issues: &res,
}
ast.Walk(&n, f.F)
}
return res, nil
}
// The code below is copy-pasted from https://github.com/kyoh86/scopelint
// Node represents a Node being linted.
type Node struct {
fset *token.FileSet
dangerObjects map[*ast.Object]struct{}
unsafeObjects map[*ast.Object]struct{}
skipFuncs map[*ast.FuncLit]struct{}
issues *[]result.Issue
}
// Visit method is invoked for each node encountered by Walk.
// If the result visitor w is not nil, Walk visits each of the children
// of node with the visitor w, followed by a call of w.Visit(nil).
//nolint:gocyclo
func (f *Node) Visit(node ast.Node) ast.Visitor {
switch typedNode := node.(type) {
case *ast.ForStmt:
switch init := typedNode.Init.(type) {
case *ast.AssignStmt:
for _, lh := range init.Lhs {
switch tlh := lh.(type) {
case *ast.Ident:
f.unsafeObjects[tlh.Obj] = struct{}{}
}
}
}
case *ast.RangeStmt:
// Memory variables declarated in range statement
switch k := typedNode.Key.(type) {
case *ast.Ident:
f.unsafeObjects[k.Obj] = struct{}{}
}
switch v := typedNode.Value.(type) {
case *ast.Ident:
f.unsafeObjects[v.Obj] = struct{}{}
}
case *ast.UnaryExpr:
if typedNode.Op == token.AND {
switch ident := typedNode.X.(type) {
case *ast.Ident:
if _, unsafe := f.unsafeObjects[ident.Obj]; unsafe {
f.errorf(ident, "Using a reference for the variable on range scope %s", formatCode(ident.Name, nil))
}
}
}
case *ast.Ident:
if _, obj := f.dangerObjects[typedNode.Obj]; obj {
// It is the naked variable in scope of range statement.
f.errorf(node, "Using the variable on range scope %s in function literal", formatCode(typedNode.Name, nil))
break
}
case *ast.CallExpr:
// Ignore func literals that'll be called immediately.
switch funcLit := typedNode.Fun.(type) {
case *ast.FuncLit:
f.skipFuncs[funcLit] = struct{}{}
}
case *ast.FuncLit:
if _, skip := f.skipFuncs[typedNode]; !skip {
dangers := map[*ast.Object]struct{}{}
for d := range f.dangerObjects {
dangers[d] = struct{}{}
}
for u := range f.unsafeObjects {
dangers[u] = struct{}{}
}
return &Node{
fset: f.fset,
dangerObjects: dangers,
unsafeObjects: f.unsafeObjects,
skipFuncs: f.skipFuncs,
issues: f.issues,
}
}
}
return f
}
// The variadic arguments may start with link and category types,
// and must end with a format string and any arguments.
// It returns the new Problem.
//nolint:interfacer
func (f *Node) errorf(n ast.Node, format string, args ...interface{}) {
pos := f.fset.Position(n.Pos())
f.errorfAt(pos, format, args...)
}
func (f *Node) errorfAt(pos token.Position, format string, args ...interface{}) {
*f.issues = append(*f.issues, result.Issue{
Pos: pos,
Text: fmt.Sprintf(format, args...),
FromLinter: Scopelint{}.Name(),
})
}

View File

@ -83,6 +83,7 @@ func TestGetEnabledLintersSet(t *testing.T) {
m := NewManager()
es := NewEnabledSet(m, NewValidator(m), nil, nil)
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
defaultLinters := []linter.Config{}
for _, ln := range c.def {

View File

@ -49,6 +49,7 @@ func (m Manager) GetLinterConfig(name string) *linter.Config {
func enableLinterConfigs(lcs []linter.Config, isEnabled func(lc *linter.Config) bool) []linter.Config {
var ret []linter.Config
for _, lc := range lcs {
lc := lc
lc.EnabledByDefault = isEnabled(&lc)
ret = append(ret, lc)
}
@ -186,6 +187,10 @@ func (Manager) GetAllSupportedLinterConfigs() []linter.Config {
WithPresets(linter.PresetPerformance).
WithSpeed(8).
WithURL("https://github.com/alexkohler/prealloc"),
linter.NewConfig(golinters.Scopelint{}).
WithPresets(linter.PresetBugs).
WithSpeed(8).
WithURL("https://github.com/kyoh86/scopelint"),
}
isLocalRun := os.Getenv("GOLANGCI_COM_RUN") == ""

View File

@ -222,6 +222,7 @@ func (r Runner) processLintResults(inCh <-chan lintRes) <-chan lintRes {
// finalize processors: logging, clearing, no heavy work here
for _, p := range r.Processors {
p := p
sw.TrackStage(p.Name(), func() {
p.Finish()
})
@ -273,6 +274,7 @@ func (r *Runner) processIssues(issues []result.Issue, sw *timeutils.Stopwatch) [
for _, p := range r.Processors {
var newIssues []result.Issue
var err error
p := p
sw.TrackStage(p.Name(), func() {
newIssues, err = p.Process(issues)
})

View File

@ -33,6 +33,7 @@ func (p *Tab) Print(ctx context.Context, issues <-chan result.Issue) error {
w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0)
for i := range issues {
i := i
p.printIssue(&i, w)
}

View File

@ -38,6 +38,7 @@ func (p Text) SprintfColored(ca color.Attribute, format string, args ...interfac
func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) error {
for i := range issues {
i := i
p.printIssue(&i)
if !p.printIssuedLine {

View File

@ -148,6 +148,7 @@ func (e *rangeExpander) Visit(node ast.Node) ast.Visitor {
var foundRange *ignoredRange
for _, r := range e.inlineRanges {
if r.To == nodeStartLine-1 && nodeStartPos.Column == r.col {
r := r
foundRange = &r
break
}

View File

@ -153,6 +153,7 @@ func TestNolintAliases(t *testing.T) {
p := newTestNolintProcessor(getOkLogger(ctrl))
for _, line := range []int{47, 49, 51} {
line := line
t.Run(fmt.Sprintf("line-%d", line), func(t *testing.T) {
processAssertEmpty(t, p, newNolintFileIssue(line, "gosec"))
})

View File

@ -9,6 +9,7 @@ import (
func filterIssues(issues []result.Issue, filter func(i *result.Issue) bool) []result.Issue {
retIssues := make([]result.Issue, 0, len(issues))
for _, i := range issues {
i := i
if filter(&i) {
retIssues = append(retIssues, i)
}
@ -20,6 +21,7 @@ func filterIssues(issues []result.Issue, filter func(i *result.Issue) bool) []re
func filterIssuesErr(issues []result.Issue, filter func(i *result.Issue) (bool, error)) ([]result.Issue, error) {
retIssues := make([]result.Issue, 0, len(issues))
for _, i := range issues {
i := i
ok, err := filter(&i)
if err != nil {
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,
func transformIssues(issues []result.Issue, transform func(i *result.Issue) *result.Issue) []result.Issue {
retIssues := make([]result.Issue, 0, len(issues))
for _, i := range issues {
i := i
newI := transform(&i)
if newI != nil {
retIssues = append(retIssues, *newI)

View File

@ -368,6 +368,7 @@ func TestEnabledLinters(t *testing.T) {
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
runArgs := []string{"-v"}
if !c.noImplicitFast {

17
test/testdata/scopelint.go vendored Normal file
View File

@ -0,0 +1,17 @@
// args: -Escopelint
package testdata
import "fmt"
func ScopelintTest() {
values := []string{"a", "b", "c"}
var funcs []func()
for _, val := range values {
funcs = append(funcs, func() {
fmt.Println(val) // ERROR "Using the variable on range scope `val` in function literal"
})
}
for _, f := range funcs {
f()
}
}