move code from golangci-worker to golangci-lint

This commit is contained in:
golangci 2018-05-05 07:45:06 +03:00
parent cd8b11773c
commit 0e4998bb4f
29 changed files with 1649 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor/

150
Gopkg.lock generated Normal file
View 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
View 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
View 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
}

View 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
View File

@ -0,0 +1,5 @@
package config
type Config struct {
Paths []string
}

10
pkg/fsutils/fsutils.go Normal file
View 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")
}

View 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
}

View 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)
}
})
}
}

View 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
View 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
}

View 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{})
}

View 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)})
}

View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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,
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
View 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
View 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
}