refactor and don't print congrats if timeouted

This commit is contained in:
golangci 2018-05-31 23:53:01 +03:00
parent 2f333be584
commit 0a111acaab
60 changed files with 805 additions and 704 deletions

View file

@ -4,5 +4,7 @@ test:
golangci-lint run --fast --no-config -v
golangci-lint run --fast --no-config -v
golangci-lint run --no-config -v
golangci-lint run --fast --no-config -v ./pkg/testdata/with_issues/typecheck.go
golangci-lint run --fast --no-config -v ./test/testdata/typecheck.go
go test -v -race ./...
.PHONY: test

View file

@ -131,7 +131,7 @@ This issue is important because often you'd like to set concurrency to CPUs coun
3. It will take more time because of different usages and need of tracking of versions of `n` linters.
# Performance
Benchmarks were executed on MacBook Pro (Retina, 13-inch, Late 2013), 2,4 GHz Intel Core i5, 8 GB 1600 MHz DDR3. It has 4 cores and concurrency for linters was default: number of cores. Benchmark runs and measures timings automatically, it's code is [here](https://github.com/golangci/golangci-lint/blob/master/pkg/enabled_linters_test.go) (`BenchmarkWithGometalinter`).
Benchmarks were executed on MacBook Pro (Retina, 13-inch, Late 2013), 2,4 GHz Intel Core i5, 8 GB 1600 MHz DDR3. It has 4 cores and concurrency for linters was default: number of cores. Benchmark runs and measures timings automatically, it's code is [here](https://github.com/golangci/golangci-lint/blob/master/test/bench.go) (`BenchmarkWithGometalinter`).
We measure peak memory usage (RSS) by tracking of processes RSS every 5 ms.

View file

@ -44,10 +44,10 @@ func (e Executor) executeLinters(cmd *cobra.Command, args []string) {
color.Green("\nLinters presets:")
for _, p := range pkg.AllPresets() {
linters := pkg.GetAllLintersForPreset(p)
linters := pkg.GetAllLinterConfigsForPreset(p)
linterNames := []string{}
for _, linter := range linters {
linterNames = append(linterNames, linter.Name())
for _, lc := range linters {
linterNames = append(linterNames, lc.Linter.Name())
}
fmt.Fprintf(printers.StdOut, "%s: %s\n", color.YellowString(p), strings.Join(linterNames, ", "))
}

View file

@ -4,23 +4,17 @@ import (
"context"
"errors"
"fmt"
"go/build"
"go/token"
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/golangci/go-tools/ssa"
"github.com/golangci/go-tools/ssa/ssautil"
"github.com/golangci/golangci-lint/pkg"
"github.com/golangci/golangci-lint/pkg/astcache"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/printers"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/result/processors"
@ -28,7 +22,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"golang.org/x/tools/go/loader"
)
const (
@ -126,170 +119,6 @@ func (e *Executor) initRun() {
e.parseConfig(runCmd)
}
func isFullImportNeeded(linters []pkg.Linter) bool {
for _, linter := range linters {
lc := pkg.GetLinterConfig(linter.Name())
if lc.DoesFullImport {
return true
}
}
return false
}
func isSSAReprNeeded(linters []pkg.Linter) bool {
for _, linter := range linters {
lc := pkg.GetLinterConfig(linter.Name())
if lc.NeedsSSARepr {
return true
}
}
return false
}
func loadWholeAppIfNeeded(ctx context.Context, linters []pkg.Linter, cfg *config.Run, paths *fsutils.ProjectPaths) (*loader.Program, *loader.Config, error) {
if !isFullImportNeeded(linters) {
return nil, nil, nil
}
startedAt := time.Now()
defer func() {
logrus.Infof("Program loading took %s", time.Since(startedAt))
}()
bctx := build.Default
bctx.BuildTags = append(bctx.BuildTags, cfg.BuildTags...)
loadcfg := &loader.Config{
Build: &bctx,
AllowErrors: true, // Try to analyze event partially
}
rest, err := loadcfg.FromArgs(paths.MixedPaths(), cfg.AnalyzeTests)
if err != nil {
return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err)
}
if len(rest) > 0 {
return nil, nil, fmt.Errorf("unhandled loading paths: %v", rest)
}
prog, err := loadcfg.Load()
if err != nil {
return nil, nil, fmt.Errorf("can't load paths: %s", err)
}
return prog, loadcfg, nil
}
func buildSSAProgram(ctx context.Context, lprog *loader.Program) *ssa.Program {
startedAt := time.Now()
defer func() {
logrus.Infof("SSA repr building took %s", time.Since(startedAt))
}()
ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug)
ssaProg.Build()
return ssaProg
}
func discoverGoRoot() (string, error) {
goroot := os.Getenv("GOROOT")
if goroot != "" {
return goroot, nil
}
output, err := exec.Command("go", "env", "GOROOT").Output()
if err != nil {
return "", fmt.Errorf("can't execute go env GOROOT: %s", err)
}
return strings.TrimSpace(string(output)), nil
}
// separateNotCompilingPackages moves not compiling packages into separate slices:
// a lot of linters crash on such packages. Leave them only for those linters
// which can work with them.
func separateNotCompilingPackages(lintCtx *golinters.Context) {
prog := lintCtx.Program
if prog.Created != nil {
compilingCreated := make([]*loader.PackageInfo, 0, len(prog.Created))
for _, info := range prog.Created {
if len(info.Errors) != 0 {
lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
} else {
compilingCreated = append(compilingCreated, info)
}
}
prog.Created = compilingCreated
}
if prog.Imported != nil {
for k, info := range prog.Imported {
if len(info.Errors) != 0 {
lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
delete(prog.Imported, k)
}
}
}
}
func buildLintCtx(ctx context.Context, linters []pkg.Linter, cfg *config.Config) (*golinters.Context, error) {
// Set GOROOT to have working cross-compilation: cross-compiled binaries
// have invalid GOROOT. XXX: can't use runtime.GOROOT().
goroot, err := discoverGoRoot()
if err != nil {
return nil, fmt.Errorf("can't discover GOROOT: %s", err)
}
os.Setenv("GOROOT", goroot)
build.Default.GOROOT = goroot
logrus.Infof("set GOROOT=%q", goroot)
args := cfg.Run.Args
if len(args) == 0 {
args = []string{"./..."}
}
paths, err := fsutils.GetPathsForAnalysis(ctx, args, cfg.Run.AnalyzeTests)
if err != nil {
return nil, err
}
prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, paths)
if err != nil {
return nil, err
}
var ssaProg *ssa.Program
if prog != nil && isSSAReprNeeded(linters) {
ssaProg = buildSSAProgram(ctx, prog)
}
var astCache *astcache.Cache
if prog != nil {
astCache, err = astcache.LoadFromProgram(prog)
if err != nil {
return nil, err
}
} else {
astCache = astcache.LoadFromFiles(paths.Files)
}
ret := &golinters.Context{
Paths: paths,
Cfg: cfg,
Program: prog,
SSAProgram: ssaProg,
LoaderConfig: loaderConfig,
ASTCache: astCache,
}
if prog != nil {
separateNotCompilingPackages(ret)
}
return ret, nil
}
func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan result.Issue, error) {
e.cfg.Run.Args = args
@ -298,7 +127,11 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan resul
return nil, err
}
lintCtx, err := buildLintCtx(ctx, linters, e.cfg)
ctxLinters := make([]lint.LinterConfig, 0, len(linters))
for _, lc := range linters {
ctxLinters = append(ctxLinters, lint.LinterConfig(lc))
}
lintCtx, err := lint.BuildContext(ctx, ctxLinters, e.cfg)
if err != nil {
return nil, err
}
@ -315,7 +148,7 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan resul
if lintCtx.Program != nil {
fset = lintCtx.Program.Fset
}
runner := pkg.SimpleRunner{
runner := lint.SimpleRunner{
Processors: []processors.Processor{
processors.NewPathPrettifier(), // must be before diff processor at least
processors.NewExclude(excludeTotalPattern),
@ -329,7 +162,11 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan resul
},
}
return runner.Run(ctx, linters, lintCtx), nil
runLinters := make([]lint.RunnerLinterConfig, 0, len(linters))
for _, lc := range linters {
runLinters = append(runLinters, lint.RunnerLinterConfig(lc))
}
return runner.Run(ctx, runLinters, lintCtx), nil
}
func setOutputToDevNull() (savedStdout, savedStderr *os.File) {
@ -364,7 +201,7 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
p = printers.NewText(e.cfg.Output.PrintIssuedLine,
e.cfg.Output.Format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName)
}
gotAnyIssues, err := p.Print(issues)
gotAnyIssues, err := p.Print(ctx, issues)
if err != nil {
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
}

View file

@ -21,6 +21,7 @@ var DefaultExcludePatterns = []string{
// golint
"should have comment",
"comment on exported method",
"func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this",
// gas
"G103:", // Use of unsafe calls should be audited

View file

@ -8,6 +8,7 @@ import (
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/sirupsen/logrus"
)
@ -33,7 +34,7 @@ func allPresetsSet() map[string]bool {
}
type LinterConfig struct {
Linter Linter
Linter lint.Linter
EnabledByDefault bool
DoesFullImport bool
NeedsSSARepr bool
@ -62,7 +63,23 @@ func (lc LinterConfig) WithSpeed(speed int) LinterConfig {
return lc
}
func newLinterConfig(linter Linter) LinterConfig {
func (lc LinterConfig) NeedsProgramLoading() bool {
return lc.DoesFullImport
}
func (lc LinterConfig) NeedsSSARepresentation() bool {
return lc.NeedsSSARepr
}
func (lc LinterConfig) GetSpeed() int {
return lc.Speed
}
func (lc LinterConfig) GetLinter() lint.Linter {
return lc.Linter
}
func newLinterConfig(linter lint.Linter) LinterConfig {
return LinterConfig{
Linter: linter,
}
@ -71,7 +88,7 @@ func newLinterConfig(linter Linter) LinterConfig {
var nameToLC map[string]LinterConfig
var nameToLCOnce sync.Once
func GetLinterConfig(name string) *LinterConfig {
func getLinterConfig(name string) *LinterConfig {
nameToLCOnce.Do(func() {
nameToLC = make(map[string]LinterConfig)
for _, lc := range GetAllSupportedLinterConfigs() {
@ -158,44 +175,22 @@ func GetAllSupportedLinterConfigs() []LinterConfig {
})
}
func getAllSupportedLinters() []Linter {
var ret []Linter
for _, lc := range GetAllSupportedLinterConfigs() {
ret = append(ret, lc.Linter)
}
return ret
}
func getAllEnabledByDefaultLinters() []Linter {
var ret []Linter
func getAllEnabledByDefaultLinters() []LinterConfig {
var ret []LinterConfig
for _, lc := range GetAllSupportedLinterConfigs() {
if lc.EnabledByDefault {
ret = append(ret, lc.Linter)
ret = append(ret, lc)
}
}
return ret
}
var supportedLintersByName map[string]Linter
var linterByNameMapOnce sync.Once
func getLinterByName(name string) Linter {
linterByNameMapOnce.Do(func() {
supportedLintersByName = make(map[string]Linter)
for _, lc := range GetAllSupportedLinterConfigs() {
supportedLintersByName[lc.Linter.Name()] = lc.Linter
}
})
return supportedLintersByName[name]
}
func lintersToMap(linters []Linter) map[string]Linter {
ret := map[string]Linter{}
for _, linter := range linters {
ret[linter.Name()] = linter
func linterConfigsToMap(lcs []LinterConfig) map[string]*LinterConfig {
ret := map[string]*LinterConfig{}
for _, lc := range lcs {
lc := lc // local copy
ret[lc.Linter.Name()] = &lc
}
return ret
@ -205,7 +200,7 @@ func validateLintersNames(cfg *config.Linters) error {
allNames := append([]string{}, cfg.Enable...)
allNames = append(allNames, cfg.Disable...)
for _, name := range allNames {
if getLinterByName(name) == nil {
if getLinterConfig(name) == nil {
return fmt.Errorf("no such linter %q", name)
}
}
@ -281,12 +276,12 @@ func validateEnabledDisabledLintersConfig(cfg *config.Linters) error {
return nil
}
func GetAllLintersForPreset(p string) []Linter {
ret := []Linter{}
func GetAllLinterConfigsForPreset(p string) []LinterConfig {
ret := []LinterConfig{}
for _, lc := range GetAllSupportedLinterConfigs() {
for _, ip := range lc.InPresets {
if p == ip {
ret = append(ret, lc.Linter)
ret = append(ret, lc)
break
}
}
@ -295,23 +290,23 @@ func GetAllLintersForPreset(p string) []Linter {
return ret
}
func getEnabledLintersSet(lcfg *config.Linters, enabledByDefaultLinters []Linter) map[string]Linter { // nolint:gocyclo
resultLintersSet := map[string]Linter{}
func getEnabledLintersSet(lcfg *config.Linters, enabledByDefaultLinters []LinterConfig) map[string]*LinterConfig { // nolint:gocyclo
resultLintersSet := map[string]*LinterConfig{}
switch {
case len(lcfg.Presets) != 0:
break // imply --disable-all
case lcfg.EnableAll:
resultLintersSet = lintersToMap(getAllSupportedLinters())
resultLintersSet = linterConfigsToMap(GetAllSupportedLinterConfigs())
case lcfg.DisableAll:
break
default:
resultLintersSet = lintersToMap(enabledByDefaultLinters)
resultLintersSet = linterConfigsToMap(enabledByDefaultLinters)
}
// --presets can only add linters to default set
for _, p := range lcfg.Presets {
for _, linter := range GetAllLintersForPreset(p) {
resultLintersSet[linter.Name()] = linter
for _, lc := range GetAllLinterConfigsForPreset(p) {
resultLintersSet[lc.Linter.Name()] = &lc
}
}
@ -320,14 +315,14 @@ func getEnabledLintersSet(lcfg *config.Linters, enabledByDefaultLinters []Linter
// It should be before --enable and --disable to be able to enable or disable specific linter.
if lcfg.Fast {
for name := range resultLintersSet {
if GetLinterConfig(name).DoesFullImport {
if getLinterConfig(name).DoesFullImport {
delete(resultLintersSet, name)
}
}
}
for _, name := range lcfg.Enable {
resultLintersSet[name] = getLinterByName(name)
resultLintersSet[name] = getLinterConfig(name)
}
for _, name := range lcfg.Disable {
@ -339,6 +334,7 @@ func getEnabledLintersSet(lcfg *config.Linters, enabledByDefaultLinters []Linter
delete(resultLintersSet, name)
}
optimizeLintersSet(resultLintersSet)
return resultLintersSet
}
@ -349,7 +345,7 @@ func getAllMegacheckSubLinterNames() []string {
return []string{unusedName, gosimpleName, staticcheckName}
}
func optimizeLintersSet(linters map[string]Linter) {
func optimizeLintersSet(linters map[string]*LinterConfig) {
unusedName := golinters.Megacheck{UnusedEnabled: true}.Name()
gosimpleName := golinters.Megacheck{GosimpleEnabled: true}.Name()
staticcheckName := golinters.Megacheck{StaticcheckEnabled: true}.Name()
@ -378,20 +374,21 @@ func optimizeLintersSet(linters map[string]Linter) {
delete(linters, n)
}
linters[m.Name()] = m
lc := *getLinterConfig("megacheck")
lc.Linter = m
linters[m.Name()] = &lc
}
func GetEnabledLinters(cfg *config.Config) ([]Linter, error) {
func GetEnabledLinters(cfg *config.Config) ([]LinterConfig, error) {
if err := validateEnabledDisabledLintersConfig(&cfg.Linters); err != nil {
return nil, err
}
resultLintersSet := getEnabledLintersSet(&cfg.Linters, getAllEnabledByDefaultLinters())
optimizeLintersSet(resultLintersSet)
var resultLinters []Linter
for _, linter := range resultLintersSet {
resultLinters = append(resultLinters, linter)
var resultLinters []LinterConfig
for _, lc := range resultLintersSet {
resultLinters = append(resultLinters, *lc)
}
verbosePrintLintersStatus(cfg, resultLinters)
@ -413,10 +410,10 @@ func uniqStrings(ss []string) []string {
return ret
}
func verbosePrintLintersStatus(cfg *config.Config, linters []Linter) {
func verbosePrintLintersStatus(cfg *config.Config, lcs []LinterConfig) {
var linterNames []string
for _, linter := range linters {
linterNames = append(linterNames, linter.Name())
for _, lc := range lcs {
linterNames = append(linterNames, lc.Linter.Name())
}
logrus.Infof("Active linters: %s", linterNames)

View file

@ -1,379 +1,13 @@
package pkg
import (
"bytes"
"fmt"
"go/build"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"
"github.com/golangci/golangci-lint/pkg/config"
gops "github.com/mitchellh/go-ps"
"github.com/shirou/gopsutil/process"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
var installOnce sync.Once
func installBinary(t assert.TestingT) {
installOnce.Do(func() {
cmd := exec.Command("go", "install", filepath.Join("..", "cmd", binName))
assert.NoError(t, cmd.Run(), "Can't go install %s", binName)
})
}
func runGoErrchk(c *exec.Cmd, t *testing.T) {
output, err := c.CombinedOutput()
assert.NoError(t, err, "Output:\n%s", output)
// Can't check exit code: tool only prints to output
assert.False(t, bytes.Contains(output, []byte("BUG")), "Output:\n%s", output)
}
const testdataDir = "testdata"
var testdataWithIssuesDir = filepath.Join(testdataDir, "with_issues")
const binName = "golangci-lint"
func TestSourcesFromTestdataWithIssuesDir(t *testing.T) {
t.Log(filepath.Join(testdataWithIssuesDir, "*.go"))
sources, err := filepath.Glob(filepath.Join(testdataWithIssuesDir, "*.go"))
assert.NoError(t, err)
assert.NotEmpty(t, sources)
installBinary(t)
for _, s := range sources {
s := s
t.Run(s, func(t *testing.T) {
t.Parallel()
testOneSource(t, s)
})
}
}
func TestDeadlineExitCode(t *testing.T) {
installBinary(t)
exitCode := runGolangciLintGetExitCode(t, "--no-config", "--deadline=1ms")
assert.Equal(t, 4, exitCode)
}
func runGolangciLintGetExitCode(t *testing.T, args ...string) int {
runArgs := append([]string{"run"}, args...)
cmd := exec.Command("golangci-lint", runArgs...)
err := cmd.Run()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
ws := exitError.Sys().(syscall.WaitStatus)
return ws.ExitStatus()
}
t.Fatalf("can't get error code from %s", err)
return -1
}
// success, exitCode should be 0 if go is ok
ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
return ws.ExitStatus()
}
func testOneSource(t *testing.T, sourcePath string) {
goErrchkBin := filepath.Join(runtime.GOROOT(), "test", "errchk")
cmd := exec.Command(goErrchkBin, binName, "run",
"--enable-all",
"--dupl.threshold=20",
"--gocyclo.min-complexity=20",
"--print-issued-lines=false",
"--print-linter-name=false",
"--out-format=line-number",
"--print-welcome=false",
"--govet.check-shadowing=true",
"--depguard.include-go-root",
"--depguard.packages='log'",
sourcePath)
runGoErrchk(cmd, t)
}
func chdir(b *testing.B, dir string) {
if err := os.Chdir(dir); err != nil {
b.Fatalf("can't chdir to %s: %s", dir, err)
}
}
func prepareGoSource(b *testing.B) {
chdir(b, filepath.Join(build.Default.GOROOT, "src"))
}
func prepareGithubProject(owner, name string) func(*testing.B) {
return func(b *testing.B) {
dir := filepath.Join(build.Default.GOPATH, "src", "github.com", owner, name)
_, err := os.Stat(dir)
if os.IsNotExist(err) {
err = exec.Command("git", "clone", fmt.Sprintf("https://github.com/%s/%s.git", owner, name)).Run()
if err != nil {
b.Fatalf("can't git clone %s/%s: %s", owner, name, err)
}
}
chdir(b, dir)
}
}
func getBenchLintersArgsNoMegacheck() []string {
return []string{
"--enable=deadcode",
"--enable=gocyclo",
"--enable=golint",
"--enable=varcheck",
"--enable=structcheck",
"--enable=maligned",
"--enable=errcheck",
"--enable=dupl",
"--enable=ineffassign",
"--enable=interfacer",
"--enable=unconvert",
"--enable=goconst",
"--enable=gas",
}
}
func getBenchLintersArgs() []string {
return append([]string{
"--enable=megacheck",
}, getBenchLintersArgsNoMegacheck()...)
}
func getGometalinterCommonArgs() []string {
return []string{
"--deadline=30m",
"--skip=testdata",
"--skip=builtin",
"--vendor",
"--cyclo-over=30",
"--dupl-threshold=150",
"--exclude", fmt.Sprintf("(%s)", strings.Join(config.DefaultExcludePatterns, "|")),
"--disable-all",
"--enable=vet",
"--enable=vetshadow",
}
}
func printCommand(cmd string, args ...string) {
if os.Getenv("PRINT_CMD") != "1" {
return
}
quotedArgs := []string{}
for _, a := range args {
quotedArgs = append(quotedArgs, strconv.Quote(a))
}
logrus.Warnf("%s %s", cmd, strings.Join(quotedArgs, " "))
}
func runGometalinter(b *testing.B) {
args := []string{}
args = append(args, getGometalinterCommonArgs()...)
args = append(args, getBenchLintersArgs()...)
args = append(args, "./...")
printCommand("gometalinter", args...)
_ = exec.Command("gometalinter", args...).Run()
}
func getGolangciLintCommonArgs() []string {
return []string{"run", "--no-config", "--issues-exit-code=0", "--deadline=30m", "--disable-all", "--enable=govet"}
}
func runGolangciLint(b *testing.B) {
args := getGolangciLintCommonArgs()
args = append(args, getBenchLintersArgs()...)
printCommand("golangci-lint", args...)
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
if err != nil {
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
}
}
func getGoLinesTotalCount(b *testing.B) int {
cmd := exec.Command("bash", "-c", `find . -name "*.go" | fgrep -v vendor | xargs wc -l | tail -1`)
out, err := cmd.CombinedOutput()
if err != nil {
b.Fatalf("can't run go lines counter: %s", err)
}
parts := bytes.Split(bytes.TrimSpace(out), []byte(" "))
n, err := strconv.Atoi(string(parts[0]))
if err != nil {
b.Fatalf("can't parse go lines count: %s", err)
}
return n
}
func getLinterMemoryMB(b *testing.B, progName string) (int, error) {
processes, err := gops.Processes()
if err != nil {
b.Fatalf("Can't get processes: %s", err)
}
var progPID int
for _, p := range processes {
if p.Executable() == progName {
progPID = p.Pid()
break
}
}
if progPID == 0 {
return 0, fmt.Errorf("no process")
}
allProgPIDs := []int{progPID}
for _, p := range processes {
if p.PPid() == progPID {
allProgPIDs = append(allProgPIDs, p.Pid())
}
}
var totalProgMemBytes uint64
for _, pid := range allProgPIDs {
p, err := process.NewProcess(int32(pid))
if err != nil {
continue // subprocess could die
}
mi, err := p.MemoryInfo()
if err != nil {
continue
}
totalProgMemBytes += mi.RSS
}
return int(totalProgMemBytes / 1024 / 1024), nil
}
func trackPeakMemoryUsage(b *testing.B, doneCh <-chan struct{}, progName string) chan int {
resCh := make(chan int)
go func() {
var peakUsedMemMB int
t := time.NewTicker(time.Millisecond * 5)
defer t.Stop()
for {
select {
case <-doneCh:
resCh <- peakUsedMemMB
close(resCh)
return
case <-t.C:
}
m, err := getLinterMemoryMB(b, progName)
if err != nil {
continue
}
if m > peakUsedMemMB {
peakUsedMemMB = m
}
}
}()
return resCh
}
type runResult struct {
peakMemMB int
duration time.Duration
}
func compare(b *testing.B, gometalinterRun, golangciLintRun func(*testing.B), repoName, mode string, kLOC int) { // nolint
gometalinterRes := runOne(b, gometalinterRun, "gometalinter")
golangciLintRes := runOne(b, golangciLintRun, "golangci-lint")
if mode != "" {
mode = " " + mode
}
logrus.Warnf("%s (%d kLoC): golangci-lint%s: time: %s, %.1f times faster; memory: %dMB, %.1f times less",
repoName, kLOC, mode,
golangciLintRes.duration, gometalinterRes.duration.Seconds()/golangciLintRes.duration.Seconds(),
golangciLintRes.peakMemMB, float64(gometalinterRes.peakMemMB)/float64(golangciLintRes.peakMemMB),
)
}
func runOne(b *testing.B, run func(*testing.B), progName string) *runResult {
doneCh := make(chan struct{})
peakMemCh := trackPeakMemoryUsage(b, doneCh, progName)
startedAt := time.Now()
run(b)
duration := time.Since(startedAt)
close(doneCh)
peakUsedMemMB := <-peakMemCh
return &runResult{
peakMemMB: peakUsedMemMB,
duration: duration,
}
}
func BenchmarkWithGometalinter(b *testing.B) {
installBinary(b)
type bcase struct {
name string
prepare func(*testing.B)
}
bcases := []bcase{
{
name: "self repo",
prepare: prepareGithubProject("golangci", "golangci-lint"),
},
{
name: "gometalinter repo",
prepare: prepareGithubProject("alecthomas", "gometalinter"),
},
{
name: "hugo",
prepare: prepareGithubProject("gohugoio", "hugo"),
},
{
name: "go-ethereum",
prepare: prepareGithubProject("ethereum", "go-ethereum"),
},
{
name: "beego",
prepare: prepareGithubProject("astaxie", "beego"),
},
{
name: "terraform",
prepare: prepareGithubProject("hashicorp", "terraform"),
},
{
name: "consul",
prepare: prepareGithubProject("hashicorp", "consul"),
},
{
name: "go source code",
prepare: prepareGoSource,
},
}
for _, bc := range bcases {
bc.prepare(b)
lc := getGoLinesTotalCount(b)
compare(b, runGometalinter, runGolangciLint, bc.name, "", lc/1000)
}
}
func TestGetEnabledLintersSet(t *testing.T) {
type cs struct {
cfg config.Linters
@ -395,19 +29,30 @@ func TestGetEnabledLintersSet(t *testing.T) {
},
name: "disable only staticcheck",
def: getAllMegacheckSubLinterNames(),
exp: []string{"gosimple", "unused"},
exp: []string{"megacheck.{unused,gosimple}"},
},
{
name: "merge into megacheck",
def: getAllMegacheckSubLinterNames(),
exp: []string{"megacheck"},
},
{
name: "don't disable anything",
def: []string{"gofmt", "govet"},
exp: []string{"gofmt", "govet"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defaultLinters := []Linter{}
defaultLinters := []LinterConfig{}
for _, ln := range c.def {
defaultLinters = append(defaultLinters, getLinterByName(ln))
defaultLinters = append(defaultLinters, *getLinterConfig(ln))
}
els := getEnabledLintersSet(&c.cfg, defaultLinters)
var enabledLinters []string
for ln := range els {
for ln, lc := range els {
assert.Equal(t, ln, lc.Linter.Name())
enabledLinters = append(enabledLinters, ln)
}

View file

@ -80,7 +80,7 @@ func (pr PathResolver) isIgnoredDir(dir string) bool {
// https://github.com/golang/dep/issues/298
// https://github.com/tools/godep/issues/140
if strings.HasPrefix(dirName, ".") && dirName != "." {
if strings.HasPrefix(dirName, ".") && dirName != "." && dirName != ".." {
return true
}
if strings.HasPrefix(dirName, "_") {

View file

@ -1,23 +0,0 @@
package golinters
import (
"github.com/golangci/go-tools/ssa"
"github.com/golangci/golangci-lint/pkg/astcache"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"golang.org/x/tools/go/loader"
)
type Context struct {
Paths *fsutils.ProjectPaths
Cfg *config.Config
Program *loader.Program
SSAProgram *ssa.Program
LoaderConfig *loader.Config
ASTCache *astcache.Cache
NotCompilingPackages []*loader.PackageInfo
}
func (c *Context) Settings() *config.LintersSettings {
return &c.Cfg.LintersSettings
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
deadcodeAPI "github.com/golangci/go-misc/deadcode"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -18,7 +19,7 @@ func (Deadcode) Desc() string {
return "Finds unused code"
}
func (d Deadcode) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (d Deadcode) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
issues, err := deadcodeAPI.Run(lintCtx.Program)
if err != nil {
return nil, err

View file

@ -6,6 +6,7 @@ import (
"strings"
depguardAPI "github.com/OpenPeeDeeP/depguard"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -19,7 +20,7 @@ func (Depguard) Desc() string {
return "Go linter that checks if package imports are in a list of acceptable packages"
}
func (d Depguard) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (d Depguard) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
dg := &depguardAPI.Depguard{
Packages: lintCtx.Settings().Depguard.Packages,
IncludeGoRoot: lintCtx.Settings().Depguard.IncludeGoRoot,

View file

@ -5,6 +5,7 @@ import (
"fmt"
"go/token"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
duplAPI "github.com/mibk/dupl"
)
@ -19,7 +20,7 @@ func (Dupl) Desc() string {
return "Tool for code clone detection"
}
func (d Dupl) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (d Dupl) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
issues, err := duplAPI.Run(lintCtx.Paths.Files, lintCtx.Settings().Dupl.Threshold)
if err != nil {
return nil, err

View file

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
errcheckAPI "github.com/kisielk/errcheck/golangci"
)
@ -18,7 +19,7 @@ func (Errcheck) Desc() string {
return "Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases"
}
func (e Errcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (e Errcheck) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
errCfg := &lintCtx.Settings().Errcheck
issues, err := errcheckAPI.Run(lintCtx.Program, errCfg.CheckAssignToBlank, errCfg.CheckTypeAssertions)
if err != nil {

View file

@ -10,6 +10,7 @@ import (
"github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/rules"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
)
@ -24,7 +25,7 @@ func (Gas) Desc() string {
return "Inspects source code for security problems"
}
func (lint Gas) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (lint Gas) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
gasConfig := gas.NewConfig()
enabledRules := rules.Generate()
logger := log.New(ioutil.Discard, "", 0)

View file

@ -5,6 +5,7 @@ import (
"fmt"
goconstAPI "github.com/golangci/goconst"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -18,7 +19,7 @@ func (Goconst) Desc() string {
return "Finds repeated strings that could be replaced by a constant"
}
func (lint Goconst) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (lint Goconst) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
var goconstIssues []goconstAPI.Issue
// TODO: make it cross-package: pass package names inside goconst
for _, files := range lintCtx.Paths.FilesGrouppedByDirs() {

View file

@ -6,6 +6,7 @@ import (
"sort"
gocycloAPI "github.com/golangci/gocyclo/pkg/gocyclo"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -19,7 +20,7 @@ func (Gocyclo) Desc() string {
return "Computes and checks the cyclomatic complexity of functions"
}
func (g Gocyclo) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (g Gocyclo) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
var stats []gocycloAPI.Stat
for _, f := range lintCtx.ASTCache.GetAllValidFiles() {
stats = gocycloAPI.BuildStats(f.F, f.Fset, stats)

View file

@ -8,6 +8,7 @@ import (
gofmtAPI "github.com/golangci/gofmt/gofmt"
goimportsAPI "github.com/golangci/gofmt/goimports"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
"sourcegraph.com/sourcegraph/go-diff/diff"
@ -101,7 +102,7 @@ func (g Gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
return issues, nil
}
func (g Gofmt) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (g Gofmt) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
var issues []result.Issue
for _, f := range lintCtx.Paths.Files {

View file

@ -5,7 +5,8 @@ import (
"fmt"
"io/ioutil"
"github.com/golang/lint"
lintAPI "github.com/golang/lint"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
)
@ -20,7 +21,7 @@ func (Golint) Desc() string {
return "Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes"
}
func (g Golint) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (g Golint) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
var issues []result.Issue
var lintErr error
for _, pkgFiles := range lintCtx.Paths.FilesGrouppedByDirs() {
@ -48,7 +49,7 @@ func (g Golint) lintFiles(minConfidence float64, filenames ...string) ([]result.
files[filename] = src
}
l := new(lint.Linter)
l := new(lintAPI.Linter)
ps, err := l.LintFiles(files)
if err != nil {
return nil, fmt.Errorf("can't lint files %s: %s", filenames, err)

View file

@ -3,6 +3,7 @@ package golinters
import (
"context"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
govetAPI "github.com/golangci/govet"
)
@ -17,7 +18,7 @@ func (Govet) Desc() string {
return "Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string"
}
func (g Govet) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (g Govet) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
// TODO: check .S asm files: govet can do it if pass dirs
var govetIssues []govetAPI.Issue
for _, files := range lintCtx.Paths.FilesGrouppedByDirs() {

View file

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
ineffassignAPI "github.com/golangci/ineffassign"
)
@ -18,7 +19,7 @@ func (Ineffassign) Desc() string {
return "Detects when assignments to existing variables are not used"
}
func (lint Ineffassign) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (lint Ineffassign) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
issues := ineffassignAPI.Run(lintCtx.Paths.Files)
if len(issues) == 0 {
return nil, nil

View file

@ -5,6 +5,7 @@ import (
"mvdan.cc/interfacer/check"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -18,7 +19,7 @@ func (Interfacer) Desc() string {
return "Linter that suggests narrower interface types"
}
func (lint Interfacer) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (lint Interfacer) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
c := new(check.Checker)
c.Program(lintCtx.Program)
c.ProgramSSA(lintCtx.SSAProgram)

View file

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
malignedAPI "github.com/golangci/maligned"
)
@ -18,7 +19,7 @@ func (Maligned) Desc() string {
return "Tool to detect Go structs that would take less memory if their fields were sorted"
}
func (m Maligned) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (m Maligned) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
issues := malignedAPI.Run(lintCtx.Program)
if len(issues) == 0 {
return nil, nil

View file

@ -2,8 +2,11 @@ package golinters
import (
"context"
"fmt"
"strings"
megacheckAPI "github.com/golangci/go-tools/cmd/megacheck"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -14,17 +17,26 @@ type Megacheck struct {
}
func (m Megacheck) Name() string {
if m.UnusedEnabled && !m.GosimpleEnabled && !m.StaticcheckEnabled {
return "unused"
names := []string{}
if m.UnusedEnabled {
names = append(names, "unused")
}
if m.GosimpleEnabled && !m.UnusedEnabled && !m.StaticcheckEnabled {
return "gosimple"
if m.GosimpleEnabled {
names = append(names, "gosimple")
}
if m.StaticcheckEnabled && !m.UnusedEnabled && !m.GosimpleEnabled {
return "staticcheck"
if m.StaticcheckEnabled {
names = append(names, "staticcheck")
}
return "megacheck" // all enabled
if len(names) == 1 {
return names[0] // only one sublinter is enabled
}
if len(names) == 3 {
return "megacheck" // all enabled
}
return fmt.Sprintf("megacheck.{%s}", strings.Join(names, ","))
}
func (m Megacheck) Desc() string {
@ -38,7 +50,7 @@ func (m Megacheck) Desc() string {
return descs[m.Name()]
}
func (m Megacheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (m Megacheck) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
issues := megacheckAPI.Run(lintCtx.Program, lintCtx.LoaderConfig, lintCtx.SSAProgram,
m.StaticcheckEnabled, m.GosimpleEnabled, m.UnusedEnabled)
if len(issues) == 0 {

View file

@ -5,6 +5,7 @@ import (
"fmt"
structcheckAPI "github.com/golangci/check/cmd/structcheck"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -18,7 +19,7 @@ func (Structcheck) Desc() string {
return "Finds unused struct fields"
}
func (s Structcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (s Structcheck) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
issues := structcheckAPI.Run(lintCtx.Program, lintCtx.Settings().Structcheck.CheckExportedFields)
if len(issues) == 0 {
return nil, nil

View file

@ -6,6 +6,7 @@ import (
"strconv"
"strings"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -61,7 +62,7 @@ func (lint TypeCheck) parseError(err error) *result.Issue {
}
}
func (lint TypeCheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (lint TypeCheck) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
if lintCtx.NotCompilingPackages == nil {
return nil, nil
}

View file

@ -3,6 +3,7 @@ package golinters
import (
"context"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
unconvertAPI "github.com/golangci/unconvert"
)
@ -17,7 +18,7 @@ func (Unconvert) Desc() string {
return "Remove unnecessary type conversions"
}
func (lint Unconvert) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (lint Unconvert) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
positions := unconvertAPI.Run(lintCtx.Program)
if len(positions) == 0 {
return nil, nil

View file

@ -5,6 +5,7 @@ import (
"fmt"
varcheckAPI "github.com/golangci/check/cmd/varcheck"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -18,7 +19,7 @@ func (Varcheck) Desc() string {
return "Finds unused global variables and constants"
}
func (v Varcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error) {
func (v Varcheck) Run(ctx context.Context, lintCtx *lint.Context) ([]result.Issue, error) {
issues := varcheckAPI.Run(lintCtx.Program, lintCtx.Settings().Varcheck.CheckExportedFields)
if len(issues) == 0 {
return nil, nil

200
pkg/lint/context.go Normal file
View file

@ -0,0 +1,200 @@
package lint
import (
"context"
"fmt"
"go/build"
"os"
"os/exec"
"strings"
"time"
"github.com/golangci/go-tools/ssa"
"github.com/golangci/go-tools/ssa/ssautil"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/lint/astcache"
"github.com/sirupsen/logrus"
"golang.org/x/tools/go/loader"
)
type Context struct {
Paths *fsutils.ProjectPaths
Cfg *config.Config
Program *loader.Program
SSAProgram *ssa.Program
LoaderConfig *loader.Config
ASTCache *astcache.Cache
NotCompilingPackages []*loader.PackageInfo
}
func (c *Context) Settings() *config.LintersSettings {
return &c.Cfg.LintersSettings
}
type LinterConfig interface {
NeedsProgramLoading() bool
NeedsSSARepresentation() bool
}
func isFullImportNeeded(linters []LinterConfig) bool {
for _, linter := range linters {
if linter.NeedsProgramLoading() {
return true
}
}
return false
}
func isSSAReprNeeded(linters []LinterConfig) bool {
for _, linter := range linters {
if linter.NeedsSSARepresentation() {
return true
}
}
return false
}
func loadWholeAppIfNeeded(ctx context.Context, linters []LinterConfig, cfg *config.Run, paths *fsutils.ProjectPaths) (*loader.Program, *loader.Config, error) {
if !isFullImportNeeded(linters) {
return nil, nil, nil
}
startedAt := time.Now()
defer func() {
logrus.Infof("Program loading took %s", time.Since(startedAt))
}()
bctx := build.Default
bctx.BuildTags = append(bctx.BuildTags, cfg.BuildTags...)
loadcfg := &loader.Config{
Build: &bctx,
AllowErrors: true, // Try to analyze event partially
}
rest, err := loadcfg.FromArgs(paths.MixedPaths(), cfg.AnalyzeTests)
if err != nil {
return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err)
}
if len(rest) > 0 {
return nil, nil, fmt.Errorf("unhandled loading paths: %v", rest)
}
prog, err := loadcfg.Load()
if err != nil {
return nil, nil, fmt.Errorf("can't load program from paths %v: %s", paths.MixedPaths(), err)
}
return prog, loadcfg, nil
}
func buildSSAProgram(ctx context.Context, lprog *loader.Program) *ssa.Program {
startedAt := time.Now()
defer func() {
logrus.Infof("SSA repr building took %s", time.Since(startedAt))
}()
ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug)
ssaProg.Build()
return ssaProg
}
func discoverGoRoot() (string, error) {
goroot := os.Getenv("GOROOT")
if goroot != "" {
return goroot, nil
}
output, err := exec.Command("go", "env", "GOROOT").Output()
if err != nil {
return "", fmt.Errorf("can't execute go env GOROOT: %s", err)
}
return strings.TrimSpace(string(output)), nil
}
// separateNotCompilingPackages moves not compiling packages into separate slices:
// a lot of linters crash on such packages. Leave them only for those linters
// which can work with them.
func separateNotCompilingPackages(lintCtx *Context) {
prog := lintCtx.Program
if prog.Created != nil {
compilingCreated := make([]*loader.PackageInfo, 0, len(prog.Created))
for _, info := range prog.Created {
if len(info.Errors) != 0 {
lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
} else {
compilingCreated = append(compilingCreated, info)
}
}
prog.Created = compilingCreated
}
if prog.Imported != nil {
for k, info := range prog.Imported {
if len(info.Errors) != 0 {
lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
delete(prog.Imported, k)
}
}
}
}
func BuildContext(ctx context.Context, linters []LinterConfig, cfg *config.Config) (*Context, error) {
// Set GOROOT to have working cross-compilation: cross-compiled binaries
// have invalid GOROOT. XXX: can't use runtime.GOROOT().
goroot, err := discoverGoRoot()
if err != nil {
return nil, fmt.Errorf("can't discover GOROOT: %s", err)
}
os.Setenv("GOROOT", goroot)
build.Default.GOROOT = goroot
logrus.Infof("set GOROOT=%q", goroot)
args := cfg.Run.Args
if len(args) == 0 {
args = []string{"./..."}
}
paths, err := fsutils.GetPathsForAnalysis(ctx, args, cfg.Run.AnalyzeTests)
if err != nil {
return nil, err
}
prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, paths)
if err != nil {
return nil, err
}
var ssaProg *ssa.Program
if prog != nil && isSSAReprNeeded(linters) {
ssaProg = buildSSAProgram(ctx, prog)
}
var astCache *astcache.Cache
if prog != nil {
astCache, err = astcache.LoadFromProgram(prog)
if err != nil {
return nil, err
}
} else {
astCache = astcache.LoadFromFiles(paths.Files)
}
ret := &Context{
Paths: paths,
Cfg: cfg,
Program: prog,
SSAProgram: ssaProg,
LoaderConfig: loaderConfig,
ASTCache: astCache,
}
if prog != nil {
separateNotCompilingPackages(ret)
}
return ret, nil
}

View file

@ -1,22 +1,30 @@
package commands
package lint
import (
"context"
"testing"
"github.com/golangci/golangci-lint/pkg"
"github.com/golangci/golangci-lint/pkg/astcache"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/lint/astcache"
"github.com/stretchr/testify/assert"
)
type testLinterConfig struct{}
func (t testLinterConfig) NeedsProgramLoading() bool {
return true
}
func (t testLinterConfig) NeedsSSARepresentation() bool {
return true
}
func TestASTCacheLoading(t *testing.T) {
ctx := context.Background()
linters := []pkg.Linter{golinters.Errcheck{}}
linters := []LinterConfig{testLinterConfig{}}
inputPaths := []string{"./...", "./", "./run.go", "run.go"}
inputPaths := []string{"./...", "./", "./context.go", "context.go"}
for _, inputPath := range inputPaths {
paths, err := fsutils.GetPathsForAnalysis(ctx, []string{inputPath}, true)
assert.NoError(t, err)

13
pkg/lint/linter.go Normal file
View file

@ -0,0 +1,13 @@
package lint
import (
"context"
"github.com/golangci/golangci-lint/pkg/result"
)
type Linter interface {
Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error)
Name() string
Desc() string
}

View file

@ -1,4 +1,4 @@
package pkg
package lint
import (
"context"
@ -9,7 +9,6 @@ import (
"sync"
"time"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/result/processors"
"github.com/golangci/golangci-lint/pkg/timeutils"
@ -26,7 +25,7 @@ type lintRes struct {
issues []result.Issue
}
func runLinterSafe(ctx context.Context, lintCtx *golinters.Context, linter Linter) (ret []result.Issue, err error) {
func runLinterSafe(ctx context.Context, lintCtx *Context, linter Linter) (ret []result.Issue, err error) {
defer func() {
if panicData := recover(); panicData != nil {
err = fmt.Errorf("panic occured: %s", panicData)
@ -37,7 +36,7 @@ func runLinterSafe(ctx context.Context, lintCtx *golinters.Context, linter Linte
return linter.Run(ctx, lintCtx)
}
func runWorker(ctx context.Context, lintCtx *golinters.Context, tasksCh <-chan Linter, lintResultsCh chan<- lintRes, name string) {
func runWorker(ctx context.Context, lintCtx *Context, tasksCh <-chan Linter, lintResultsCh chan<- lintRes, name string) {
sw := timeutils.NewStopwatch(name)
defer sw.Print()
@ -88,20 +87,23 @@ func logWorkersStat(workersFinishTimes []time.Time) {
logrus.Infof("Workers idle times: %s", strings.Join(logStrings, ", "))
}
func getSortedLintersConfigs(linters []Linter) []LinterConfig {
ret := make([]LinterConfig, 0, len(linters))
for _, linter := range linters {
ret = append(ret, *GetLinterConfig(linter.Name()))
}
type RunnerLinterConfig interface {
GetSpeed() int
GetLinter() Linter
}
func getSortedLintersConfigs(linters []RunnerLinterConfig) []RunnerLinterConfig {
ret := make([]RunnerLinterConfig, len(linters))
copy(ret, linters)
sort.Slice(ret, func(i, j int) bool {
return ret[i].Speed < ret[j].Speed
return ret[i].GetSpeed() < ret[j].GetSpeed()
})
return ret
}
func (r *SimpleRunner) runWorkers(ctx context.Context, lintCtx *golinters.Context, linters []Linter) <-chan lintRes {
func (r *SimpleRunner) runWorkers(ctx context.Context, lintCtx *Context, linters []RunnerLinterConfig) <-chan lintRes {
tasksCh := make(chan Linter, len(linters))
lintResultsCh := make(chan lintRes, len(linters))
var wg sync.WaitGroup
@ -120,7 +122,7 @@ func (r *SimpleRunner) runWorkers(ctx context.Context, lintCtx *golinters.Contex
lcs := getSortedLintersConfigs(linters)
for _, lc := range lcs {
tasksCh <- lc.Linter
tasksCh <- lc.GetLinter()
}
close(tasksCh)
@ -187,7 +189,7 @@ func collectIssues(ctx context.Context, resCh <-chan lintRes) <-chan result.Issu
return retIssues
}
func (r SimpleRunner) Run(ctx context.Context, linters []Linter, lintCtx *golinters.Context) <-chan result.Issue {
func (r SimpleRunner) Run(ctx context.Context, linters []RunnerLinterConfig, lintCtx *Context) <-chan result.Issue {
lintResultsCh := r.runWorkers(ctx, lintCtx, linters)
processedLintResultsCh := r.processLintResults(ctx, lintResultsCh)
if ctx.Err() != nil {

View file

@ -1,14 +0,0 @@
package pkg
import (
"context"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/result"
)
type Linter interface {
Run(ctx context.Context, lintCtx *golinters.Context) ([]result.Issue, error)
Name() string
Desc() string
}

View file

@ -1,6 +1,7 @@
package printers
import (
"context"
"encoding/json"
"fmt"
@ -13,7 +14,7 @@ func NewJSON() *JSON {
return &JSON{}
}
func (JSON) Print(issues <-chan result.Issue) (bool, error) {
func (JSON) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) {
var allIssues []result.Issue
for i := range issues {
allIssues = append(allIssues, i)

View file

@ -1,7 +1,11 @@
package printers
import "github.com/golangci/golangci-lint/pkg/result"
import (
"context"
"github.com/golangci/golangci-lint/pkg/result"
)
type Printer interface {
Print(issues <-chan result.Issue) (bool, error)
Print(ctx context.Context, issues <-chan result.Issue) (bool, error)
}

View file

@ -2,6 +2,7 @@ package printers
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"strings"
@ -58,7 +59,7 @@ func (p *Text) getFileLinesForIssue(i *result.Issue) (linesCache, error) {
return fc, nil
}
func (p *Text) Print(issues <-chan result.Issue) (bool, error) {
func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) {
var issuedLineExtractingDuration time.Duration
defer func() {
logrus.Infof("Extracting issued lines took %s", issuedLineExtractingDuration)
@ -86,11 +87,11 @@ func (p *Text) Print(issues <-chan result.Issue) (bool, error) {
}
}
if issuesN == 0 {
if issuesN != 0 {
logrus.Infof("Found %d issues", issuesN)
} else if ctx.Err() == nil { // don't print "congrats" if timeouted
outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.")
fmt.Fprintln(StdOut, outStr)
} else {
logrus.Infof("Found %d issues", issuesN)
}
return issuesN != 0, nil

View file

@ -1,7 +1,6 @@
package processors
import (
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -25,9 +24,9 @@ func (p MaxPerFileFromLinter) Name() string {
}
var maxPerFileFromLinterConfig = map[string]int{
golinters.Gofmt{}.Name(): 1,
golinters.Gofmt{UseGoimports: true}.Name(): 1,
golinters.TypeCheck{}.Name(): 3,
"gofmt": 1,
"goimports": 1,
"typecheck": 3,
}
func (p *MaxPerFileFromLinter) Process(issues []result.Issue) ([]result.Issue, error) {

287
test/bench_test.go Normal file
View file

@ -0,0 +1,287 @@
package test
import (
"bytes"
"fmt"
"go/build"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/golangci/golangci-lint/pkg/config"
gops "github.com/mitchellh/go-ps"
"github.com/shirou/gopsutil/process"
"github.com/sirupsen/logrus"
)
func chdir(b *testing.B, dir string) {
if err := os.Chdir(dir); err != nil {
b.Fatalf("can't chdir to %s: %s", dir, err)
}
}
func prepareGoSource(b *testing.B) {
chdir(b, filepath.Join(build.Default.GOROOT, "src"))
}
func prepareGithubProject(owner, name string) func(*testing.B) {
return func(b *testing.B) {
dir := filepath.Join(build.Default.GOPATH, "src", "github.com", owner, name)
_, err := os.Stat(dir)
if os.IsNotExist(err) {
err = exec.Command("git", "clone", fmt.Sprintf("https://github.com/%s/%s.git", owner, name)).Run()
if err != nil {
b.Fatalf("can't git clone %s/%s: %s", owner, name, err)
}
}
chdir(b, dir)
}
}
func getBenchLintersArgsNoMegacheck() []string {
return []string{
"--enable=deadcode",
"--enable=gocyclo",
"--enable=golint",
"--enable=varcheck",
"--enable=structcheck",
"--enable=maligned",
"--enable=errcheck",
"--enable=dupl",
"--enable=ineffassign",
"--enable=interfacer",
"--enable=unconvert",
"--enable=goconst",
"--enable=gas",
}
}
func getBenchLintersArgs() []string {
return append([]string{
"--enable=megacheck",
}, getBenchLintersArgsNoMegacheck()...)
}
func getGometalinterCommonArgs() []string {
return []string{
"--deadline=30m",
"--skip=testdata",
"--skip=builtin",
"--vendor",
"--cyclo-over=30",
"--dupl-threshold=150",
"--exclude", fmt.Sprintf("(%s)", strings.Join(config.DefaultExcludePatterns, "|")),
"--disable-all",
"--enable=vet",
"--enable=vetshadow",
}
}
func printCommand(cmd string, args ...string) {
if os.Getenv("PRINT_CMD") != "1" {
return
}
quotedArgs := []string{}
for _, a := range args {
quotedArgs = append(quotedArgs, strconv.Quote(a))
}
logrus.Warnf("%s %s", cmd, strings.Join(quotedArgs, " "))
}
func runGometalinter(b *testing.B) {
args := []string{}
args = append(args, getGometalinterCommonArgs()...)
args = append(args, getBenchLintersArgs()...)
args = append(args, "./...")
printCommand("gometalinter", args...)
_ = exec.Command("gometalinter", args...).Run()
}
func getGolangciLintCommonArgs() []string {
return []string{"run", "--no-config", "--issues-exit-code=0", "--deadline=30m", "--disable-all", "--enable=govet"}
}
func runGolangciLintForBench(b *testing.B) {
args := getGolangciLintCommonArgs()
args = append(args, getBenchLintersArgs()...)
printCommand("golangci-lint", args...)
out, err := exec.Command("golangci-lint", args...).CombinedOutput()
if err != nil {
b.Fatalf("can't run golangci-lint: %s, %s", err, out)
}
}
func getGoLinesTotalCount(b *testing.B) int {
cmd := exec.Command("bash", "-c", `find . -name "*.go" | fgrep -v vendor | xargs wc -l | tail -1`)
out, err := cmd.CombinedOutput()
if err != nil {
b.Fatalf("can't run go lines counter: %s", err)
}
parts := bytes.Split(bytes.TrimSpace(out), []byte(" "))
n, err := strconv.Atoi(string(parts[0]))
if err != nil {
b.Fatalf("can't parse go lines count: %s", err)
}
return n
}
func getLinterMemoryMB(b *testing.B, progName string) (int, error) {
processes, err := gops.Processes()
if err != nil {
b.Fatalf("Can't get processes: %s", err)
}
var progPID int
for _, p := range processes {
if p.Executable() == progName {
progPID = p.Pid()
break
}
}
if progPID == 0 {
return 0, fmt.Errorf("no process")
}
allProgPIDs := []int{progPID}
for _, p := range processes {
if p.PPid() == progPID {
allProgPIDs = append(allProgPIDs, p.Pid())
}
}
var totalProgMemBytes uint64
for _, pid := range allProgPIDs {
p, err := process.NewProcess(int32(pid))
if err != nil {
continue // subprocess could die
}
mi, err := p.MemoryInfo()
if err != nil {
continue
}
totalProgMemBytes += mi.RSS
}
return int(totalProgMemBytes / 1024 / 1024), nil
}
func trackPeakMemoryUsage(b *testing.B, doneCh <-chan struct{}, progName string) chan int {
resCh := make(chan int)
go func() {
var peakUsedMemMB int
t := time.NewTicker(time.Millisecond * 5)
defer t.Stop()
for {
select {
case <-doneCh:
resCh <- peakUsedMemMB
close(resCh)
return
case <-t.C:
}
m, err := getLinterMemoryMB(b, progName)
if err != nil {
continue
}
if m > peakUsedMemMB {
peakUsedMemMB = m
}
}
}()
return resCh
}
type runResult struct {
peakMemMB int
duration time.Duration
}
func compare(b *testing.B, gometalinterRun, golangciLintRun func(*testing.B), repoName, mode string, kLOC int) { // nolint
gometalinterRes := runOne(b, gometalinterRun, "gometalinter")
golangciLintRes := runOne(b, golangciLintRun, "golangci-lint")
if mode != "" {
mode = " " + mode
}
logrus.Warnf("%s (%d kLoC): golangci-lint%s: time: %s, %.1f times faster; memory: %dMB, %.1f times less",
repoName, kLOC, mode,
golangciLintRes.duration, gometalinterRes.duration.Seconds()/golangciLintRes.duration.Seconds(),
golangciLintRes.peakMemMB, float64(gometalinterRes.peakMemMB)/float64(golangciLintRes.peakMemMB),
)
}
func runOne(b *testing.B, run func(*testing.B), progName string) *runResult {
doneCh := make(chan struct{})
peakMemCh := trackPeakMemoryUsage(b, doneCh, progName)
startedAt := time.Now()
run(b)
duration := time.Since(startedAt)
close(doneCh)
peakUsedMemMB := <-peakMemCh
return &runResult{
peakMemMB: peakUsedMemMB,
duration: duration,
}
}
func BenchmarkWithGometalinter(b *testing.B) {
installBinary(b)
type bcase struct {
name string
prepare func(*testing.B)
}
bcases := []bcase{
{
name: "self repo",
prepare: prepareGithubProject("golangci", "golangci-lint"),
},
{
name: "gometalinter repo",
prepare: prepareGithubProject("alecthomas", "gometalinter"),
},
{
name: "hugo",
prepare: prepareGithubProject("gohugoio", "hugo"),
},
{
name: "go-ethereum",
prepare: prepareGithubProject("ethereum", "go-ethereum"),
},
{
name: "beego",
prepare: prepareGithubProject("astaxie", "beego"),
},
{
name: "terraform",
prepare: prepareGithubProject("hashicorp", "terraform"),
},
{
name: "consul",
prepare: prepareGithubProject("hashicorp", "consul"),
},
{
name: "go source code",
prepare: prepareGoSource,
},
}
for _, bc := range bcases {
bc.prepare(b)
lc := getGoLinesTotalCount(b)
compare(b, runGometalinter, runGolangciLintForBench, bc.name, "", lc/1000)
}
}

56
test/linters_test.go Normal file
View file

@ -0,0 +1,56 @@
package test
import (
"bytes"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func runGoErrchk(c *exec.Cmd, t *testing.T) {
output, err := c.CombinedOutput()
assert.NoError(t, err, "Output:\n%s", output)
// Can't check exit code: tool only prints to output
assert.False(t, bytes.Contains(output, []byte("BUG")), "Output:\n%s", output)
}
const testdataDir = "testdata"
const binName = "golangci-lint"
func TestSourcesFromTestdataWithIssuesDir(t *testing.T) {
t.Log(filepath.Join(testdataDir, "*.go"))
sources, err := filepath.Glob(filepath.Join(testdataDir, "*.go"))
assert.NoError(t, err)
assert.NotEmpty(t, sources)
installBinary(t)
for _, s := range sources {
s := s
t.Run(s, func(t *testing.T) {
t.Parallel()
testOneSource(t, s)
})
}
}
func testOneSource(t *testing.T, sourcePath string) {
goErrchkBin := filepath.Join(runtime.GOROOT(), "test", "errchk")
cmd := exec.Command(goErrchkBin, binName, "run",
"--enable-all",
"--dupl.threshold=20",
"--gocyclo.min-complexity=20",
"--print-issued-lines=false",
"--print-linter-name=false",
"--out-format=line-number",
"--print-welcome=false",
"--govet.check-shadowing=true",
"--depguard.include-go-root",
"--depguard.packages='log'",
sourcePath)
runGoErrchk(cmd, t)
}

56
test/run_test.go Normal file
View file

@ -0,0 +1,56 @@
package test
import (
"os/exec"
"path/filepath"
"sync"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
)
var installOnce sync.Once
func installBinary(t assert.TestingT) {
installOnce.Do(func() {
cmd := exec.Command("go", "install", filepath.Join("..", "cmd", binName))
assert.NoError(t, cmd.Run(), "Can't go install %s", binName)
})
}
func TestCongratsMessageIfNoIssues(t *testing.T) {
installBinary(t)
out, exitCode := runGolangciLint(t, "../...")
assert.Equal(t, 0, exitCode)
assert.Equal(t, "Congrats! No issues were found.\n", out)
}
func TestDeadline(t *testing.T) {
installBinary(t)
out, exitCode := runGolangciLint(t, "--no-config", "--deadline=1ms", "../...")
assert.Equal(t, 4, exitCode)
assert.Equal(t, "", out) // no 'Congrats! No issues were found.'
}
func runGolangciLint(t *testing.T, args ...string) (string, int) {
runArgs := append([]string{"run"}, args...)
cmd := exec.Command("golangci-lint", runArgs...)
out, err := cmd.Output()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
t.Logf("stderr: %s", exitError.Stderr)
ws := exitError.Sys().(syscall.WaitStatus)
return string(out), ws.ExitStatus()
}
t.Fatalf("can't get error code from %s", err)
return "", -1
}
// success, exitCode should be 0 if go is ok
ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
return string(out), ws.ExitStatus()
}

View file

@ -9,7 +9,7 @@ func (DuplLogger) level() int {
func (DuplLogger) Debug(args ...interface{}) {}
func (DuplLogger) Info(args ...interface{}) {}
func (logger *DuplLogger) First(args ...interface{}) { // ERROR "12-21 lines are duplicate of `testdata/with_issues/dupl.go:23-32`"
func (logger *DuplLogger) First(args ...interface{}) { // ERROR "12-21 lines are duplicate of `testdata/dupl.go:23-32`"
if logger.level() >= 0 {
logger.Debug(args...)
logger.Debug(args...)
@ -20,7 +20,7 @@ func (logger *DuplLogger) First(args ...interface{}) { // ERROR "12-21 lines are
}
}
func (logger *DuplLogger) Second(args ...interface{}) { // ERROR "23-32 lines are duplicate of `testdata/with_issues/dupl.go:12-21`"
func (logger *DuplLogger) Second(args ...interface{}) { // ERROR "23-32 lines are duplicate of `testdata/dupl.go:12-21`"
if logger.level() >= 1 {
logger.Info(args...)
logger.Info(args...)

View file

@ -11,7 +11,7 @@ func Govet() error {
func GovetShadow(f io.Reader, buf []byte) (err error) {
if f != nil {
_, err := f.Read(buf) // ERROR "declaration of .err. shadows declaration at testdata/with_issues/govet.go:\d+"
_, err := f.Read(buf) // ERROR "declaration of .err. shadows declaration at testdata/govet.go:\d+"
if err != nil {
return err
}