move code from golangci-worker to golangci-lint
This commit is contained in:
parent
cd8b11773c
commit
0e4998bb4f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/vendor/
|
150
Gopkg.lock
generated
Normal file
150
Gopkg.lock
generated
Normal file
@ -0,0 +1,150 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/StackExchange/wmi"
|
||||
packages = ["."]
|
||||
revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/bradleyfalzon/revgrep"
|
||||
packages = ["."]
|
||||
revision = "c04006dc3307c8768bda7a33c7c15d1c6f664e14"
|
||||
version = "v0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dukex/mixpanel"
|
||||
packages = ["."]
|
||||
revision = "88b7bfd34643dce0c28a6b797652d6b5026091af"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-ole/go-ole"
|
||||
packages = [
|
||||
".",
|
||||
"oleutil"
|
||||
]
|
||||
revision = "a41e3c4b706f6ae8dfbff342b06e40fa4d2d0506"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/mock"
|
||||
packages = ["gomock"]
|
||||
revision = "c34cdb4725f4c3844d095133c6e40e448b86589b"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/golangci-shared"
|
||||
packages = [
|
||||
"pkg/analytics",
|
||||
"pkg/executors",
|
||||
"pkg/runmode",
|
||||
"pkg/timeutils"
|
||||
]
|
||||
revision = "044f3332f2e8c38cfbb56bab29f65b4245bbe76b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/savaki/amplitude-go"
|
||||
packages = ["."]
|
||||
revision = "f62e3b57c0e4d24da1fad27aa5181c59b0f7868b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/shirou/gopsutil"
|
||||
packages = [
|
||||
"cpu",
|
||||
"host",
|
||||
"internal/common",
|
||||
"mem",
|
||||
"net",
|
||||
"process"
|
||||
]
|
||||
revision = "c95755e4bcd7a62bb8bd33f3a597a7c7f35e2cf3"
|
||||
version = "v2.18.04"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/shirou/w32"
|
||||
packages = ["."]
|
||||
revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stvp/rollbar"
|
||||
packages = ["."]
|
||||
revision = "b20261800d8cda3be14dcef0d1a8320779bba61a"
|
||||
version = "v0.5.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["ssh/terminal"]
|
||||
revision = "4ec37c66abab2c7e02ae775328b2ff001c3f025a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
revision = "640f4622ab692b87c2f3a94265e6f579fe38263d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "6f686a352de66814cdd080d970febae7767857a3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "sourcegraph.com/sourcegraph/go-diff"
|
||||
packages = ["diff"]
|
||||
revision = "3f415a150aec0685cb81b73cc201e762e075006d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "sourcegraph.com/sqs/pbtypes"
|
||||
packages = ["."]
|
||||
revision = "4d1b9dc7ffc3f7b555de9b02055fa616f0ebcd18"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "fc3abded9121a32e3fc9de887b9f1dd7a4d64ee58e169053e9120881d6c54658"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
50
Gopkg.toml
Normal file
50
Gopkg.toml
Normal file
@ -0,0 +1,50 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/bradleyfalzon/revgrep"
|
||||
version = "0.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/golang/mock"
|
||||
version = "1.1.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/golangci-shared"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "sourcegraph.com/sourcegraph/go-diff"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
43
cmd/golangci-lint/main.go
Normal file
43
cmd/golangci-lint/main.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/config"
|
||||
"github.com/golangci/golangci-lint/pkg/golinters"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
var cfg config.Config
|
||||
config.ReadFromCommandLine(&cfg)
|
||||
|
||||
linters := golinters.GetSupportedLinters()
|
||||
ctx := context.Background()
|
||||
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exPath := filepath.Dir(ex)
|
||||
exec := executors.NewShell(exPath)
|
||||
|
||||
for _, linter := range linters {
|
||||
res, err := linter.Run(ctx, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Print(res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
11
pkg/config/command_line.go
Normal file
11
pkg/config/command_line.go
Normal file
@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
import "flag"
|
||||
|
||||
func ReadFromCommandLine(cfg *Config) {
|
||||
flag.Parse()
|
||||
paths := flag.Args()
|
||||
if len(paths) != 0 {
|
||||
cfg.Paths = paths
|
||||
}
|
||||
}
|
5
pkg/config/config.go
Normal file
5
pkg/config/config.go
Normal file
@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Paths []string
|
||||
}
|
10
pkg/fsutils/fsutils.go
Normal file
10
pkg/fsutils/fsutils.go
Normal file
@ -0,0 +1,10 @@
|
||||
package fsutils
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"path"
|
||||
)
|
||||
|
||||
func GetProjectRoot() string {
|
||||
return path.Join(build.Default.GOPATH, "src", "github.com", "golangci", "golangci-worker")
|
||||
}
|
182
pkg/fsutils/path_resolver.go
Normal file
182
pkg/fsutils/path_resolver.go
Normal file
@ -0,0 +1,182 @@
|
||||
package fsutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathResolver struct {
|
||||
excludeDirs map[string]bool
|
||||
allowedFileExtensions map[string]bool
|
||||
}
|
||||
|
||||
type pathResolveState struct {
|
||||
files map[string]bool
|
||||
dirs map[string]bool
|
||||
}
|
||||
|
||||
func (s *pathResolveState) addFile(path string) {
|
||||
s.files[filepath.Clean(path)] = true
|
||||
}
|
||||
|
||||
func (s *pathResolveState) addDir(path string) {
|
||||
s.dirs[filepath.Clean(path)] = true
|
||||
}
|
||||
|
||||
type PathResolveResult struct {
|
||||
files []string
|
||||
dirs []string
|
||||
}
|
||||
|
||||
func (prr PathResolveResult) Files() []string {
|
||||
return prr.files
|
||||
}
|
||||
|
||||
func (prr PathResolveResult) Dirs() []string {
|
||||
return prr.dirs
|
||||
}
|
||||
|
||||
func (s pathResolveState) toResult() *PathResolveResult {
|
||||
res := &PathResolveResult{
|
||||
files: []string{},
|
||||
dirs: []string{},
|
||||
}
|
||||
for f := range s.files {
|
||||
res.files = append(res.files, f)
|
||||
}
|
||||
for d := range s.dirs {
|
||||
res.dirs = append(res.dirs, d)
|
||||
}
|
||||
|
||||
sort.Strings(res.files)
|
||||
sort.Strings(res.dirs)
|
||||
return res
|
||||
}
|
||||
|
||||
func NewPathResolver(excludeDirs, allowedFileExtensions []string) *PathResolver {
|
||||
excludeDirsMap := map[string]bool{}
|
||||
for _, dir := range excludeDirs {
|
||||
excludeDirsMap[dir] = true
|
||||
}
|
||||
|
||||
allowedFileExtensionsMap := map[string]bool{}
|
||||
for _, fe := range allowedFileExtensions {
|
||||
allowedFileExtensionsMap[fe] = true
|
||||
}
|
||||
|
||||
return &PathResolver{
|
||||
excludeDirs: excludeDirsMap,
|
||||
allowedFileExtensions: allowedFileExtensionsMap,
|
||||
}
|
||||
}
|
||||
|
||||
func (pr PathResolver) isIgnoredDir(dir string) bool {
|
||||
dirName := filepath.Base(filepath.Clean(dir)) // ignore dirs on any depth level
|
||||
|
||||
// https://github.com/golang/dep/issues/298
|
||||
// https://github.com/tools/godep/issues/140
|
||||
if strings.HasPrefix(dirName, ".") && dirName != "." {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(dirName, "_") {
|
||||
return true
|
||||
}
|
||||
|
||||
return pr.excludeDirs[dirName]
|
||||
}
|
||||
|
||||
func (pr PathResolver) isAllowedFile(path string) bool {
|
||||
return pr.allowedFileExtensions[filepath.Ext(path)]
|
||||
}
|
||||
|
||||
func (pr PathResolver) resolveRecursively(root string, state *pathResolveState) error {
|
||||
walkErr := filepath.Walk(root, func(p string, i os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i.IsDir() {
|
||||
if pr.isIgnoredDir(p) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
state.addDir(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
if pr.isAllowedFile(p) {
|
||||
state.addFile(p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if walkErr != nil {
|
||||
return fmt.Errorf("can't walk dir %s: %s", root, walkErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pr PathResolver) resolveDir(root string, state *pathResolveState) error {
|
||||
walkErr := filepath.Walk(root, func(p string, i os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i.IsDir() {
|
||||
if filepath.Clean(p) != filepath.Clean(root) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
state.addDir(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
if pr.isAllowedFile(p) {
|
||||
state.addFile(p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if walkErr != nil {
|
||||
return fmt.Errorf("can't walk dir %s: %s", root, walkErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pr PathResolver) Resolve(paths ...string) (*PathResolveResult, error) {
|
||||
if len(paths) == 0 {
|
||||
return nil, fmt.Errorf("no paths are set")
|
||||
}
|
||||
|
||||
state := &pathResolveState{
|
||||
files: map[string]bool{},
|
||||
dirs: map[string]bool{},
|
||||
}
|
||||
for _, path := range paths {
|
||||
if strings.HasSuffix(path, "/...") {
|
||||
if err := pr.resolveRecursively(filepath.Dir(path), state); err != nil {
|
||||
return nil, fmt.Errorf("can't recursively resolve %s: %s", path, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't find path %s: %s", path, err)
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
if err := pr.resolveDir(path, state); err != nil {
|
||||
return nil, fmt.Errorf("can't resolve dir %s: %s", path, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
state.addFile(path)
|
||||
}
|
||||
|
||||
return state.toResult(), nil
|
||||
}
|
181
pkg/fsutils/path_resolver_test.go
Normal file
181
pkg/fsutils/path_resolver_test.go
Normal file
@ -0,0 +1,181 @@
|
||||
package fsutils
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type fsPreparer struct {
|
||||
t *testing.T
|
||||
root string
|
||||
prevWD string
|
||||
}
|
||||
|
||||
func (fp fsPreparer) clean() {
|
||||
err := os.Chdir(fp.prevWD)
|
||||
assert.NoError(fp.t, err)
|
||||
|
||||
err = os.RemoveAll(fp.root)
|
||||
assert.NoError(fp.t, err)
|
||||
}
|
||||
|
||||
func prepareFS(t *testing.T, paths ...string) *fsPreparer {
|
||||
root, err := ioutil.TempDir("/tmp", "golangci.test.path_resolver")
|
||||
assert.NoError(t, err)
|
||||
|
||||
prevWD, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.Chdir(root)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, p := range paths {
|
||||
err = os.MkdirAll(filepath.Dir(p), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if strings.HasSuffix(p, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(p, []byte("test"), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
return &fsPreparer{
|
||||
root: root,
|
||||
t: t,
|
||||
prevWD: prevWD,
|
||||
}
|
||||
}
|
||||
|
||||
func newPR() *PathResolver {
|
||||
return NewPathResolver([]string{}, []string{})
|
||||
}
|
||||
|
||||
func TestPathResolverNoPaths(t *testing.T) {
|
||||
_, err := newPR().Resolve()
|
||||
assert.EqualError(t, err, "no paths are set")
|
||||
}
|
||||
|
||||
func TestPathResolverNotExistingPath(t *testing.T) {
|
||||
fp := prepareFS(t)
|
||||
defer fp.clean()
|
||||
|
||||
_, err := newPR().Resolve("a")
|
||||
assert.EqualError(t, err, "can't find path a: stat a: no such file or directory")
|
||||
}
|
||||
|
||||
func TestPathResolverCommonCases(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
prepare []string
|
||||
resolve []string
|
||||
expFiles []string
|
||||
expDirs []string
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "empty root recursively",
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{"."},
|
||||
},
|
||||
{
|
||||
name: "empty root",
|
||||
resolve: []string{"./"},
|
||||
expDirs: []string{"."},
|
||||
},
|
||||
{
|
||||
name: "vendor is excluded recursively",
|
||||
prepare: []string{"vendor/a/"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{"."},
|
||||
},
|
||||
{
|
||||
name: "vendor is excluded",
|
||||
prepare: []string{"vendor/"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{"."},
|
||||
},
|
||||
{
|
||||
name: "vendor implicitely resolved",
|
||||
prepare: []string{"vendor/"},
|
||||
resolve: []string{"./vendor"},
|
||||
expDirs: []string{"vendor"},
|
||||
},
|
||||
{
|
||||
name: "extensions filter recursively",
|
||||
prepare: []string{"a/b.go", "a/c.txt", "d.go", "e.csv"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{".", "a"},
|
||||
expFiles: []string{"a/b.go", "d.go"},
|
||||
},
|
||||
{
|
||||
name: "extensions filter",
|
||||
prepare: []string{"a/b.go", "a/c.txt", "d.go"},
|
||||
resolve: []string{"a"},
|
||||
expDirs: []string{"a"},
|
||||
expFiles: []string{"a/b.go"},
|
||||
},
|
||||
{
|
||||
name: "one level dirs exclusion",
|
||||
prepare: []string{"a/b/", "a/c.go"},
|
||||
resolve: []string{"./a"},
|
||||
expDirs: []string{"a"},
|
||||
expFiles: []string{"a/c.go"},
|
||||
},
|
||||
{
|
||||
name: "implicitely resolved files",
|
||||
prepare: []string{"a/b/c.go", "a/d.txt"},
|
||||
resolve: []string{"./a/...", "a/d.txt"},
|
||||
expDirs: []string{"a", "a/b"},
|
||||
expFiles: []string{"a/b/c.go", "a/d.txt"},
|
||||
},
|
||||
{
|
||||
name: ".* is always ignored",
|
||||
prepare: []string{".git/a.go", ".circleci/b.go"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{"."},
|
||||
},
|
||||
{
|
||||
name: "exclude dirs on any depth level",
|
||||
prepare: []string{"ok/.git/a.go"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{".", "ok"},
|
||||
},
|
||||
{
|
||||
name: "ignore _*",
|
||||
prepare: []string{"_any/a.go"},
|
||||
resolve: []string{"./..."},
|
||||
expDirs: []string{"."},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fp := prepareFS(t, tc.prepare...)
|
||||
defer fp.clean()
|
||||
|
||||
pr := NewPathResolver([]string{"vendor"}, []string{".go"})
|
||||
res, err := pr.Resolve(tc.resolve...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if tc.expFiles == nil {
|
||||
assert.Empty(t, res.files)
|
||||
} else {
|
||||
assert.Equal(t, tc.expFiles, res.files)
|
||||
}
|
||||
|
||||
if tc.expDirs == nil {
|
||||
assert.Empty(t, res.dirs)
|
||||
} else {
|
||||
assert.Equal(t, tc.expDirs, res.dirs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
60
pkg/golinters/errcheck_test.go
Normal file
60
pkg/golinters/errcheck_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestErrcheckSimple(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func retErr() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func missedErrorCheck() {
|
||||
retErr()
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, errCheck, source, []result.Issue{NewIssue("errcheck", "Error return value is not checked", 8)})
|
||||
}
|
||||
|
||||
func TestErrcheckIgnoreClose(t *testing.T) {
|
||||
sources := []string{`package p
|
||||
|
||||
import "os"
|
||||
|
||||
func ok() error {
|
||||
f, err := os.Open("t.go")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Close()
|
||||
return nil
|
||||
}
|
||||
`,
|
||||
`package p
|
||||
|
||||
import "net/http"
|
||||
|
||||
func f() {
|
||||
resp, err := http.Get("http://example.com/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
panic(resp)
|
||||
}
|
||||
`}
|
||||
|
||||
for _, source := range sources {
|
||||
ExpectIssues(t, errCheck, source, []result.Issue{})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add cases of non-compiling code
|
||||
// TODO: don't report issues if got more than 20 issues
|
115
pkg/golinters/gofmt.go
Normal file
115
pkg/golinters/gofmt.go
Normal file
@ -0,0 +1,115 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
"sourcegraph.com/sourcegraph/go-diff/diff"
|
||||
)
|
||||
|
||||
type gofmt struct {
|
||||
useGoimports bool
|
||||
}
|
||||
|
||||
func (g gofmt) Name() string {
|
||||
if g.useGoimports {
|
||||
return "goimports"
|
||||
}
|
||||
|
||||
return "gofmt"
|
||||
}
|
||||
|
||||
func getFirstDeletedLineNumberInHunk(h *diff.Hunk) (int, error) {
|
||||
lines := bytes.Split(h.Body, []byte{'\n'})
|
||||
lineNumber := int(h.OrigStartLine - 1)
|
||||
for _, line := range lines {
|
||||
lineNumber++
|
||||
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
if line[0] == '-' {
|
||||
return lineNumber, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("didn't find deletion line in hunk %s", string(h.Body))
|
||||
}
|
||||
|
||||
func (g gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
|
||||
diffs, err := diff.ParseMultiFileDiff([]byte(patch))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse patch: %s", err)
|
||||
}
|
||||
|
||||
if len(diffs) == 0 {
|
||||
return nil, fmt.Errorf("got no diffs from patch parser: %v", diffs)
|
||||
}
|
||||
|
||||
issues := []result.Issue{}
|
||||
for _, d := range diffs {
|
||||
if len(d.Hunks) == 0 {
|
||||
analytics.Log(context.TODO()).Warnf("Got no hunks in diff %+v", d)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, hunk := range d.Hunks {
|
||||
lineNumber, err := getFirstDeletedLineNumberInHunk(hunk)
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Infof("Can't get first deleted line number for hunk: %s", err)
|
||||
lineNumber = int(hunk.OrigStartLine) // use first line if no deletions:
|
||||
}
|
||||
|
||||
text := "File is not gofmt-ed with -s"
|
||||
if g.useGoimports {
|
||||
text = "File is not goimports-ed"
|
||||
}
|
||||
i := result.Issue{
|
||||
FromLinter: g.Name(),
|
||||
File: d.NewName,
|
||||
LineNumber: lineNumber,
|
||||
Text: text,
|
||||
}
|
||||
issues = append(issues, i)
|
||||
}
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
func (g gofmt) Run(ctx context.Context, exec executors.Executor) (*result.Result, error) {
|
||||
paths, err := getPathsForGoProject(exec.WorkDir())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get files to analyze: %s", err)
|
||||
}
|
||||
|
||||
args := []string{"-d"}
|
||||
if !g.useGoimports {
|
||||
args = append(args, "-s")
|
||||
}
|
||||
args = append(args, paths.files...)
|
||||
out, err := exec.Run(ctx, g.Name(), args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't run gofmt: %s, %s", err, out)
|
||||
}
|
||||
|
||||
if len(out) == 0 { // no diff => no issues
|
||||
return &result.Result{
|
||||
Issues: []result.Issue{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
issues, err := g.extractIssuesFromPatch(out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't extract issues from gofmt diff output %q: %s", out, err)
|
||||
}
|
||||
|
||||
return &result.Result{
|
||||
Issues: issues,
|
||||
MaxIssuesPerFile: 1, // don't disturb user: show just first changed not gofmt-ed line
|
||||
}, nil
|
||||
}
|
50
pkg/golinters/gofmt_test.go
Normal file
50
pkg/golinters/gofmt_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestGofmtIssueFound(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func noFmt() error {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, gofmt{}, source, []result.Issue{NewIssue("gofmt", "File is not gofmt-ed with -s", 4)})
|
||||
}
|
||||
|
||||
func TestGofmtNoIssue(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func fmted() error {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, gofmt{}, source, []result.Issue{})
|
||||
}
|
||||
|
||||
func TestGoimportsIssueFound(t *testing.T) {
|
||||
const source = `package p
|
||||
func noFmt() error {return nil}
|
||||
`
|
||||
|
||||
lint := gofmt{useGoimports: true}
|
||||
ExpectIssues(t, lint, source, []result.Issue{NewIssue("goimports", "File is not goimports-ed", 2)})
|
||||
}
|
||||
|
||||
func TestGoimportsNoIssue(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
func fmted() error {
|
||||
return nil
|
||||
}
|
||||
`
|
||||
|
||||
lint := gofmt{useGoimports: true}
|
||||
ExpectIssues(t, lint, source, []result.Issue{})
|
||||
}
|
15
pkg/golinters/golint_test.go
Normal file
15
pkg/golinters/golint_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestGolintSimple(t *testing.T) {
|
||||
const source = `package p
|
||||
var v_1 string`
|
||||
|
||||
ExpectIssues(t, golint, source,
|
||||
[]result.Issue{NewIssue("golint", "don't use underscores in Go names; var v_1 should be v1", 2)})
|
||||
}
|
22
pkg/golinters/govet_test.go
Normal file
22
pkg/golinters/govet_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
func TestGovetSimple(t *testing.T) {
|
||||
const source = `package p
|
||||
|
||||
import "os"
|
||||
|
||||
func f() error {
|
||||
return &os.PathError{"first", "path", os.ErrNotExist}
|
||||
}
|
||||
`
|
||||
|
||||
ExpectIssues(t, govet, source, []result.Issue{
|
||||
NewIssue("govet", "os.PathError composite literal uses unkeyed fields", 6),
|
||||
})
|
||||
}
|
199
pkg/golinters/linter.go
Normal file
199
pkg/golinters/linter.go
Normal file
@ -0,0 +1,199 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
type linterConfig struct {
|
||||
messageTemplate *template.Template
|
||||
pattern *regexp.Regexp
|
||||
excludeByMessagePattern *regexp.Regexp
|
||||
args []string
|
||||
issuesFoundExitCode int
|
||||
}
|
||||
|
||||
func newLinterConfig(messageTemplate, pattern, excludeByMessagePattern string, args ...string) *linterConfig {
|
||||
if messageTemplate == "" {
|
||||
messageTemplate = "{{.message}}"
|
||||
}
|
||||
|
||||
var excludeByMessagePatternRe *regexp.Regexp
|
||||
if excludeByMessagePattern != "" {
|
||||
excludeByMessagePatternRe = regexp.MustCompile(excludeByMessagePattern)
|
||||
}
|
||||
|
||||
return &linterConfig{
|
||||
messageTemplate: template.Must(template.New("message").Parse(messageTemplate)),
|
||||
pattern: regexp.MustCompile(pattern),
|
||||
excludeByMessagePattern: excludeByMessagePatternRe,
|
||||
args: args,
|
||||
issuesFoundExitCode: 1,
|
||||
}
|
||||
}
|
||||
|
||||
type linter struct {
|
||||
name string
|
||||
|
||||
linterConfig
|
||||
}
|
||||
|
||||
func newLinter(name string, cfg *linterConfig) *linter {
|
||||
return &linter{
|
||||
name: name,
|
||||
linterConfig: *cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (lint linter) Name() string {
|
||||
return lint.name
|
||||
}
|
||||
|
||||
func (lint linter) doesExitCodeMeansIssuesWereFound(err error) bool {
|
||||
ee, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
status, ok := ee.Sys().(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
exitCode := status.ExitStatus()
|
||||
return exitCode == lint.issuesFoundExitCode
|
||||
}
|
||||
|
||||
func getOutTail(out string, linesCount int) string {
|
||||
lines := strings.Split(out, "\n")
|
||||
if len(lines) <= linesCount {
|
||||
return out
|
||||
}
|
||||
|
||||
return strings.Join(lines[len(lines)-linesCount:], "\n")
|
||||
}
|
||||
|
||||
func (lint linter) Run(ctx context.Context, exec executors.Executor) (*result.Result, error) {
|
||||
paths, err := getPathsForGoProject(exec.WorkDir())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get files to analyze: %s", err)
|
||||
}
|
||||
|
||||
retIssues := []result.Issue{}
|
||||
|
||||
const maxDirsPerRun = 100 // run one linter multiple times with groups of dirs: limit memory usage in the cost of higher CPU usage
|
||||
|
||||
for len(paths.dirs) != 0 {
|
||||
args := append([]string{}, lint.args...)
|
||||
|
||||
dirsCount := len(paths.dirs)
|
||||
if dirsCount > maxDirsPerRun {
|
||||
dirsCount = maxDirsPerRun
|
||||
}
|
||||
dirs := paths.dirs[:dirsCount]
|
||||
args = append(args, dirs...)
|
||||
|
||||
out, err := exec.Run(ctx, lint.name, args...)
|
||||
if err != nil && !lint.doesExitCodeMeansIssuesWereFound(err) {
|
||||
out = getOutTail(out, 10)
|
||||
return nil, fmt.Errorf("can't run linter %s with args %v: %s, %s", lint.name, lint.args, err, out)
|
||||
}
|
||||
|
||||
issues := lint.parseLinterOut(out)
|
||||
retIssues = append(retIssues, issues...)
|
||||
|
||||
paths.dirs = paths.dirs[dirsCount:]
|
||||
}
|
||||
|
||||
return &result.Result{
|
||||
Issues: retIssues,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type regexpVars map[string]string
|
||||
|
||||
func buildMatchedRegexpVars(match []string, pattern *regexp.Regexp) regexpVars {
|
||||
result := regexpVars{}
|
||||
for i, name := range pattern.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
result[name] = match[i]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (lint linter) parseLinterOutLine(line string) (regexpVars, error) {
|
||||
match := lint.pattern.FindStringSubmatch(line)
|
||||
if match == nil {
|
||||
return nil, fmt.Errorf("can't match line %q against regexp", line)
|
||||
}
|
||||
|
||||
return buildMatchedRegexpVars(match, lint.pattern), nil
|
||||
}
|
||||
|
||||
func (lint linter) makeIssue(vars regexpVars) (*result.Issue, error) {
|
||||
var messageBuffer bytes.Buffer
|
||||
err := lint.messageTemplate.Execute(&messageBuffer, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't execute message template: %s", err)
|
||||
}
|
||||
|
||||
if vars["path"] == "" {
|
||||
return nil, fmt.Errorf("no path in vars %+v", vars)
|
||||
}
|
||||
|
||||
var line int
|
||||
if vars["line"] != "" {
|
||||
line, err = strconv.Atoi(vars["line"])
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Warnf("Can't parse line %q: %s", vars["line"], err)
|
||||
}
|
||||
}
|
||||
|
||||
return &result.Issue{
|
||||
FromLinter: lint.name,
|
||||
File: vars["path"],
|
||||
LineNumber: line,
|
||||
Text: messageBuffer.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (lint linter) parseLinterOut(out string) []result.Issue {
|
||||
issues := []result.Issue{}
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
vars, err := lint.parseLinterOutLine(scanner.Text())
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Warnf("Can't parse linter out line: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
message := vars["message"]
|
||||
ex := lint.excludeByMessagePattern
|
||||
if message != "" && ex != nil && ex.MatchString(message) {
|
||||
continue
|
||||
}
|
||||
|
||||
issue, err := lint.makeIssue(vars)
|
||||
if err != nil {
|
||||
analytics.Log(context.TODO()).Warnf("Can't make issue: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
issues = append(issues, *issue)
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
21
pkg/golinters/supported_linters.go
Normal file
21
pkg/golinters/supported_linters.go
Normal file
@ -0,0 +1,21 @@
|
||||
package golinters
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg"
|
||||
|
||||
const pathLineColMessage = `^(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.*)$`
|
||||
const pathLineMessage = `^(?P<path>.*?\.go):(?P<line>\d+):\s*(?P<message>.*)$`
|
||||
|
||||
var errCheck = newLinter("errcheck",
|
||||
newLinterConfig(
|
||||
"Error return value is not checked",
|
||||
pathLineColMessage,
|
||||
"\\.Close()", // It's annoying and not critical error to ignore Close() errors),
|
||||
),
|
||||
)
|
||||
|
||||
var golint = newLinter("golint", newLinterConfig("", pathLineColMessage, ""))
|
||||
var govet = newLinter("govet", newLinterConfig("", pathLineMessage, "", "--no-recurse"))
|
||||
|
||||
func GetSupportedLinters() []linters.Linter {
|
||||
return []linters.Linter{gofmt{}, gofmt{useGoimports: true}, golint, govet}
|
||||
}
|
39
pkg/golinters/test.go
Normal file
39
pkg/golinters/test.go
Normal file
@ -0,0 +1,39 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewIssue(linter, message string, line int) result.Issue {
|
||||
return result.Issue{
|
||||
FromLinter: linter,
|
||||
Text: message,
|
||||
File: "p/f.go",
|
||||
LineNumber: line,
|
||||
}
|
||||
}
|
||||
|
||||
func ExpectIssues(t *testing.T, linter linters.Linter, source string, issues []result.Issue) {
|
||||
exec, err := executors.NewTempDirShell("test.expectissues")
|
||||
assert.NoError(t, err)
|
||||
defer exec.Clean()
|
||||
|
||||
subDir := path.Join(exec.WorkDir(), "p")
|
||||
assert.NoError(t, os.Mkdir(subDir, os.ModePerm))
|
||||
err = ioutil.WriteFile(path.Join(subDir, "f.go"), []byte(source), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := linter.Run(context.Background(), exec)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, issues, res.Issues)
|
||||
}
|
68
pkg/golinters/utils.go
Normal file
68
pkg/golinters/utils.go
Normal file
@ -0,0 +1,68 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/fsutils"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
)
|
||||
|
||||
type ProjectPaths struct {
|
||||
files []string
|
||||
dirs []string
|
||||
}
|
||||
|
||||
func processPaths(root string, paths []string, maxPaths int) ([]string, error) {
|
||||
if len(paths) >= maxPaths {
|
||||
analytics.Log(context.TODO()).Warnf("Gofmt: got too much paths (%d), analyze first %d", len(paths), maxPaths)
|
||||
paths = paths[:maxPaths]
|
||||
}
|
||||
|
||||
ret := []string{}
|
||||
for i := range paths {
|
||||
relPath, err := filepath.Rel(root, paths[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't get relative path for path %s and root %s: %s",
|
||||
paths[i], root, err)
|
||||
}
|
||||
ret = append(ret, relPath)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getPathsForGoProject(root string) (*ProjectPaths, error) {
|
||||
excludeDirs := []string{"vendor", "testdata", "examples", "Godeps"}
|
||||
pr := fsutils.NewPathResolver(excludeDirs, []string{".go"})
|
||||
log.Printf("root is %q, paths are %q", root, path.Join(root, "..."))
|
||||
paths, err := pr.Resolve(path.Join(root, "..."))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't resolve paths: %s", err)
|
||||
}
|
||||
|
||||
files, err := processPaths(root, paths.Files(), 10000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process resolved files: %s", err)
|
||||
}
|
||||
|
||||
dirs, err := processPaths(root, paths.Dirs(), 1000)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process resolved dirs: %s", err)
|
||||
}
|
||||
|
||||
for i := range dirs {
|
||||
dir := dirs[i]
|
||||
if dir != "." {
|
||||
dirs[i] = "./" + dir
|
||||
}
|
||||
}
|
||||
|
||||
return &ProjectPaths{
|
||||
files: files,
|
||||
dirs: dirs,
|
||||
}, nil
|
||||
}
|
13
pkg/linter.go
Normal file
13
pkg/linter.go
Normal file
@ -0,0 +1,13 @@
|
||||
package linters
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
type Linter interface {
|
||||
Run(ctx context.Context, exec executors.Executor) (*result.Result, error)
|
||||
Name() string
|
||||
}
|
61
pkg/linter_mock.go
Normal file
61
pkg/linter_mock.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./app/analyze/linters/linter.go
|
||||
|
||||
package linters
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
result "github.com/golangci/golangci-lint/pkg/result"
|
||||
executors "github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
// MockLinter is a mock of Linter interface
|
||||
type MockLinter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLinterMockRecorder
|
||||
}
|
||||
|
||||
// MockLinterMockRecorder is the mock recorder for MockLinter
|
||||
type MockLinterMockRecorder struct {
|
||||
mock *MockLinter
|
||||
}
|
||||
|
||||
// NewMockLinter creates a new mock instance
|
||||
func NewMockLinter(ctrl *gomock.Controller) *MockLinter {
|
||||
mock := &MockLinter{ctrl: ctrl}
|
||||
mock.recorder = &MockLinterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *MockLinter) EXPECT() *MockLinterMockRecorder {
|
||||
return _m.recorder
|
||||
}
|
||||
|
||||
// Run mocks base method
|
||||
func (_m *MockLinter) Run(ctx context.Context, exec executors.Executor) (*result.Result, error) {
|
||||
ret := _m.ctrl.Call(_m, "Run", ctx, exec)
|
||||
ret0, _ := ret[0].(*result.Result)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Run indicates an expected call of Run
|
||||
func (_mr *MockLinterMockRecorder) Run(arg0, arg1 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Run", reflect.TypeOf((*MockLinter)(nil).Run), arg0, arg1)
|
||||
}
|
||||
|
||||
// Name mocks base method
|
||||
func (_m *MockLinter) Name() string {
|
||||
ret := _m.ctrl.Call(_m, "Name")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Name indicates an expected call of Name
|
||||
func (_mr *MockLinterMockRecorder) Name() *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Name", reflect.TypeOf((*MockLinter)(nil).Name))
|
||||
}
|
19
pkg/result/issue.go
Normal file
19
pkg/result/issue.go
Normal file
@ -0,0 +1,19 @@
|
||||
package result
|
||||
|
||||
type Issue struct {
|
||||
FromLinter string
|
||||
Text string
|
||||
File string
|
||||
LineNumber int
|
||||
HunkPos int
|
||||
}
|
||||
|
||||
func NewIssue(fromLinter, text, file string, lineNumber, hunkPos int) Issue {
|
||||
return Issue{
|
||||
FromLinter: fromLinter,
|
||||
Text: text,
|
||||
File: file,
|
||||
LineNumber: lineNumber,
|
||||
HunkPos: hunkPos,
|
||||
}
|
||||
}
|
92
pkg/result/processors/diff_processor.go
Normal file
92
pkg/result/processors/diff_processor.go
Normal file
@ -0,0 +1,92 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/bradleyfalzon/revgrep"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type DiffProcessor struct {
|
||||
patch string
|
||||
}
|
||||
|
||||
func NewDiffProcessor(patch string) *DiffProcessor {
|
||||
return &DiffProcessor{
|
||||
patch: patch,
|
||||
}
|
||||
}
|
||||
|
||||
func (p DiffProcessor) Name() string {
|
||||
return "diff"
|
||||
}
|
||||
|
||||
func (p DiffProcessor) processResult(res result.Result) (*result.Result, error) {
|
||||
// Make mapping to restore original issues metadata later
|
||||
fli := makeFilesToLinesToIssuesMap([]result.Result{res})
|
||||
|
||||
rIssues, err := p.runRevgrepOnIssues(res.Issues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newIssues := []result.Issue{}
|
||||
for _, ri := range rIssues {
|
||||
if fli[ri.File] == nil {
|
||||
return nil, fmt.Errorf("can't get original issue file for %v", ri)
|
||||
}
|
||||
|
||||
oi := fli[ri.File][ri.LineNo]
|
||||
if len(oi) != 1 {
|
||||
return nil, fmt.Errorf("can't get original issue for %v: %v", ri, oi)
|
||||
}
|
||||
|
||||
i := result.Issue{
|
||||
File: ri.File,
|
||||
LineNumber: ri.LineNo,
|
||||
Text: ri.Message,
|
||||
HunkPos: ri.HunkPos,
|
||||
FromLinter: oi[0].FromLinter,
|
||||
}
|
||||
newIssues = append(newIssues, i)
|
||||
}
|
||||
|
||||
res.Issues = newIssues
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (p DiffProcessor) Process(results []result.Result) ([]result.Result, error) {
|
||||
retResults := []result.Result{}
|
||||
for _, res := range results {
|
||||
newRes, err := p.processResult(res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't filter only new issues for result %+v: %s", res, err)
|
||||
}
|
||||
retResults = append(retResults, *newRes)
|
||||
}
|
||||
|
||||
return retResults, nil
|
||||
}
|
||||
|
||||
func (p DiffProcessor) runRevgrepOnIssues(issues []result.Issue) ([]revgrep.Issue, error) {
|
||||
// TODO: change revgrep to accept interface with line number, file name
|
||||
fakeIssuesLines := []string{}
|
||||
for _, i := range issues {
|
||||
line := fmt.Sprintf("%s:%d:%d: %s", i.File, i.LineNumber, 0, i.Text)
|
||||
fakeIssuesLines = append(fakeIssuesLines, line)
|
||||
}
|
||||
fakeIssuesOut := strings.Join(fakeIssuesLines, "\n")
|
||||
|
||||
checker := revgrep.Checker{
|
||||
Patch: strings.NewReader(p.patch),
|
||||
Regexp: `^([^:]+):(\d+):(\d+)?:?\s*(.*)$`,
|
||||
}
|
||||
rIssues, err := checker.Check(strings.NewReader(fakeIssuesOut), ioutil.Discard)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't filter only new issues by revgrep: %s", err)
|
||||
}
|
||||
|
||||
return rIssues, nil
|
||||
}
|
44
pkg/result/processors/exclude_processor.go
Normal file
44
pkg/result/processors/exclude_processor.go
Normal file
@ -0,0 +1,44 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type ExcludeProcessor struct {
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
var _ Processor = ExcludeProcessor{}
|
||||
|
||||
func NewExcludeProcessor(pattern string) *ExcludeProcessor {
|
||||
return &ExcludeProcessor{
|
||||
pattern: regexp.MustCompile(pattern),
|
||||
}
|
||||
}
|
||||
|
||||
func (p ExcludeProcessor) Name() string {
|
||||
return "exclude"
|
||||
}
|
||||
|
||||
func (p ExcludeProcessor) processResult(res result.Result) result.Result {
|
||||
newRes := res
|
||||
newRes.Issues = []result.Issue{}
|
||||
for _, i := range res.Issues {
|
||||
if !p.pattern.MatchString(i.Text) {
|
||||
newRes.Issues = append(newRes.Issues, i)
|
||||
}
|
||||
}
|
||||
|
||||
return newRes
|
||||
}
|
||||
|
||||
func (p ExcludeProcessor) Process(results []result.Result) ([]result.Result, error) {
|
||||
retResults := []result.Result{}
|
||||
for _, res := range results {
|
||||
retResults = append(retResults, p.processResult(res))
|
||||
}
|
||||
|
||||
return retResults, nil
|
||||
}
|
57
pkg/result/processors/max_per_file_processor.go
Normal file
57
pkg/result/processors/max_per_file_processor.go
Normal file
@ -0,0 +1,57 @@
|
||||
package processors
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg/result"
|
||||
|
||||
type MaxLinterIssuesPerFile struct{}
|
||||
|
||||
var _ Processor = MaxLinterIssuesPerFile{}
|
||||
|
||||
type fileToIssuesMap map[string][]result.Issue
|
||||
|
||||
func (p MaxLinterIssuesPerFile) Name() string {
|
||||
return "max_issues_per_file"
|
||||
}
|
||||
|
||||
func (p MaxLinterIssuesPerFile) makeFileToIssuesMap(res result.Result) fileToIssuesMap {
|
||||
fti := fileToIssuesMap{}
|
||||
for _, i := range res.Issues {
|
||||
fti[i.File] = append(fti[i.File], i)
|
||||
}
|
||||
|
||||
return fti
|
||||
}
|
||||
|
||||
func (p MaxLinterIssuesPerFile) processResult(res result.Result) result.Result {
|
||||
if len(res.Issues) == 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
if res.MaxIssuesPerFile == 0 {
|
||||
return res // Nothing to process
|
||||
}
|
||||
|
||||
fti := p.makeFileToIssuesMap(res)
|
||||
for file, fileIssues := range fti {
|
||||
if len(fileIssues) > res.MaxIssuesPerFile {
|
||||
fti[file] = fileIssues[:res.MaxIssuesPerFile]
|
||||
}
|
||||
}
|
||||
|
||||
filteredIssues := []result.Issue{}
|
||||
for _, issues := range fti {
|
||||
filteredIssues = append(filteredIssues, issues...)
|
||||
}
|
||||
|
||||
res.Issues = filteredIssues
|
||||
return res
|
||||
}
|
||||
|
||||
func (p MaxLinterIssuesPerFile) Process(results []result.Result) ([]result.Result, error) {
|
||||
newResults := []result.Result{}
|
||||
|
||||
for _, res := range results {
|
||||
newResults = append(newResults, p.processResult(res))
|
||||
}
|
||||
|
||||
return newResults, nil
|
||||
}
|
8
pkg/result/processors/processor.go
Normal file
8
pkg/result/processors/processor.go
Normal file
@ -0,0 +1,8 @@
|
||||
package processors
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg/result"
|
||||
|
||||
type Processor interface {
|
||||
Process(results []result.Result) ([]result.Result, error)
|
||||
Name() string
|
||||
}
|
38
pkg/result/processors/uniq_by_line_processor.go
Normal file
38
pkg/result/processors/uniq_by_line_processor.go
Normal file
@ -0,0 +1,38 @@
|
||||
package processors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
)
|
||||
|
||||
type UniqByLineProcessor struct{}
|
||||
|
||||
var _ Processor = UniqByLineProcessor{}
|
||||
|
||||
func (p UniqByLineProcessor) Name() string {
|
||||
return "uniq_by_line"
|
||||
}
|
||||
|
||||
func (p UniqByLineProcessor) Process(results []result.Result) ([]result.Result, error) {
|
||||
fli := makeFilesToLinesToIssuesMap(results)
|
||||
|
||||
retResults := []result.Result{}
|
||||
for _, res := range results {
|
||||
newRes := res
|
||||
newRes.Issues = []result.Issue{}
|
||||
for _, i := range res.Issues {
|
||||
lineIssues := fli[i.File][i.LineNumber]
|
||||
if len(lineIssues) == 0 {
|
||||
return nil, fmt.Errorf("bug in by line uniqalization")
|
||||
}
|
||||
|
||||
if lineIssues[0] == i { // Use first issue for line
|
||||
newRes.Issues = append(newRes.Issues, i)
|
||||
}
|
||||
}
|
||||
retResults = append(retResults, newRes)
|
||||
}
|
||||
|
||||
return retResults, nil
|
||||
}
|
21
pkg/result/processors/utils.go
Normal file
21
pkg/result/processors/utils.go
Normal file
@ -0,0 +1,21 @@
|
||||
package processors
|
||||
|
||||
import "github.com/golangci/golangci-lint/pkg/result"
|
||||
|
||||
type linesToIssuesMap map[int][]result.Issue
|
||||
type filesToLinesToIssuesMap map[string]linesToIssuesMap
|
||||
|
||||
func makeFilesToLinesToIssuesMap(results []result.Result) filesToLinesToIssuesMap {
|
||||
fli := filesToLinesToIssuesMap{}
|
||||
for _, res := range results {
|
||||
for _, i := range res.Issues {
|
||||
if fli[i.File] == nil {
|
||||
fli[i.File] = linesToIssuesMap{}
|
||||
}
|
||||
li := fli[i.File]
|
||||
li[i.LineNumber] = append(li[i.LineNumber], i)
|
||||
}
|
||||
}
|
||||
|
||||
return fli
|
||||
}
|
6
pkg/result/result.go
Normal file
6
pkg/result/result.go
Normal file
@ -0,0 +1,6 @@
|
||||
package result
|
||||
|
||||
type Result struct {
|
||||
Issues []Issue
|
||||
MaxIssuesPerFile int // Needed for gofmt and goimports where it is 1
|
||||
}
|
68
pkg/runner.go
Normal file
68
pkg/runner.go
Normal file
@ -0,0 +1,68 @@
|
||||
package linters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/golangci-lint/pkg/result/processors"
|
||||
"github.com/golangci/golangci-shared/pkg/analytics"
|
||||
"github.com/golangci/golangci-shared/pkg/executors"
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, linters []Linter, exec executors.Executor) ([]result.Issue, error)
|
||||
}
|
||||
|
||||
type SimpleRunner struct {
|
||||
Processors []processors.Processor
|
||||
}
|
||||
|
||||
func (r SimpleRunner) Run(ctx context.Context, linters []Linter, exec executors.Executor) ([]result.Issue, error) {
|
||||
results := []result.Result{}
|
||||
for _, linter := range linters {
|
||||
res, err := linter.Run(ctx, exec)
|
||||
if err != nil {
|
||||
analytics.Log(ctx).Warnf("Can't run linter %+v: %s", linter, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(res.Issues) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, *res)
|
||||
}
|
||||
|
||||
results, err := r.processResults(results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't process results: %s", err)
|
||||
}
|
||||
|
||||
return r.mergeResults(results), nil
|
||||
}
|
||||
|
||||
func (r SimpleRunner) processResults(results []result.Result) ([]result.Result, error) {
|
||||
if len(r.Processors) == 0 {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
for _, p := range r.Processors {
|
||||
var err error
|
||||
results, err = p.Process(results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (r SimpleRunner) mergeResults(results []result.Result) []result.Issue {
|
||||
issues := []result.Issue{}
|
||||
for _, r := range results {
|
||||
issues = append(issues, r.Issues...)
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user