Fix #78: log all warnings

1. Log all warnings, don't hide none of them
2. Write fatal messages (stop analysis) with error log level
3. Remove ugly timestamp counter from logrus output
4. Print nested module prefix in log
5. Make logger abstraction: no global logging anymore
6. Refactor config reading to config.FileReader struct to avoid passing
logger into every function
7. Replace exit codes hardcoding with constants in exitcodes package
8. Fail test if any warning was logged
9. Fix calculation of relative path if we analyze parent dir ../
10. Move Runner initialization from Executor to NewRunner func
11. Log every AST parsing error
12. Properly print used config file path in verbose mode
13. Print package files if only 1 package is analyzedin verbose mode,
  print not compiling packages in verbose mode
14. Forbid usage of github.com/sirupsen/logrus by DepGuard linter
15. Add default ignore pattern to folint: "comment on exported const"
This commit is contained in:
Denis Isaev 2018-06-13 22:37:48 +03:00 committed by Isaev Denis
parent 219a5479c8
commit 9181ca7175
40 changed files with 694 additions and 432 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/*.pprof /*.pprof
/dist/ /dist/
/.idea/ /.idea/
/test/path

View file

@ -12,6 +12,12 @@ linters-settings:
goconst: goconst:
min-len: 2 min-len: 2
min-occurrences: 2 min-occurrences: 2
depguard:
list-type: blacklist
packages:
# logging is allowed only by logutils.Log, logrus
# is allowed to use only in logutils package
- github.com/sirupsen/logrus
linters: linters:
enable-all: true enable-all: true

View file

@ -1,9 +1,9 @@
test: test:
go install ./cmd/... # needed for govet and golint go install ./cmd/... # needed for govet and golint if go < 1.10
golangci-lint run -v GL_TEST_RUN=1 golangci-lint run -v
golangci-lint run --fast --no-config -v GL_TEST_RUN=1 golangci-lint run --fast --no-config -v
golangci-lint run --no-config -v GL_TEST_RUN=1 golangci-lint run --no-config -v
go test -v ./... GL_TEST_RUN=1 go test -v ./...
assets: assets:
svg-term --cast=183662 --out docs/demo.svg --window --width 110 --height 30 --from 2000 --to 20000 --profile Dracula --term iterm2 svg-term --cast=183662 --out docs/demo.svg --window --width 110 --height 30 --from 2000 --to 20000 --profile Dracula --term iterm2

View file

@ -255,7 +255,7 @@ Flags:
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked
# golint: Annoying issue about not having a comment. The rare codebase has such comments # golint: Annoying issue about not having a comment. The rare codebase has such comments
- (comment on exported (method|function|type)|should have( a package)? comment|comment should be of the form) - (comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form)
# golint: False positive when tests are defined in package 'test' # golint: False positive when tests are defined in package 'test'
- func name will be used as test\.Test.* by other packages, and that stutters; consider calling this - func name will be used as test\.Test.* by other packages, and that stutters; consider calling this
@ -329,6 +329,12 @@ linters-settings:
goconst: goconst:
min-len: 2 min-len: 2
min-occurrences: 2 min-occurrences: 2
depguard:
list-type: blacklist
packages:
# logging is allowed only by logutils.Log, logrus
# is allowed to use only in logutils package
- github.com/sirupsen/logrus
linters: linters:
enable-all: true enable-all: true

View file

@ -2,7 +2,7 @@ package commands
import ( import (
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/sirupsen/logrus" "github.com/golangci/golangci-lint/pkg/logutils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -14,6 +14,8 @@ type Executor struct {
exitCode int exitCode int
version, commit, date string version, commit, date string
log logutils.Log
} }
func NewExecutor(version, commit, date string) *Executor { func NewExecutor(version, commit, date string) *Executor {
@ -22,10 +24,9 @@ func NewExecutor(version, commit, date string) *Executor {
version: version, version: version,
commit: commit, commit: commit,
date: date, date: date,
log: logutils.NewStderrLog(""),
} }
logrus.SetLevel(logrus.WarnLevel)
e.initRoot() e.initRoot()
e.initRun() e.initRun()
e.initLinters() e.initLinters()

View file

@ -2,7 +2,6 @@ package commands
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
@ -10,20 +9,10 @@ import (
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/printers" "github.com/golangci/golangci-lint/pkg/printers"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
func setupLog(isVerbose bool) {
log.SetFlags(0) // don't print time
if logutils.IsDebugEnabled() {
logrus.SetLevel(logrus.DebugLevel)
} else if isVerbose {
logrus.SetLevel(logrus.InfoLevel)
}
}
func (e *Executor) persistentPreRun(cmd *cobra.Command, args []string) { func (e *Executor) persistentPreRun(cmd *cobra.Command, args []string) {
if e.cfg.Run.PrintVersion { if e.cfg.Run.PrintVersion {
fmt.Fprintf(printers.StdOut, "golangci-lint has version %s built from %s on %s\n", e.version, e.commit, e.date) fmt.Fprintf(printers.StdOut, "golangci-lint has version %s built from %s on %s\n", e.version, e.commit, e.date)
@ -32,15 +21,15 @@ func (e *Executor) persistentPreRun(cmd *cobra.Command, args []string) {
runtime.GOMAXPROCS(e.cfg.Run.Concurrency) runtime.GOMAXPROCS(e.cfg.Run.Concurrency)
setupLog(e.cfg.Run.IsVerbose) logutils.SetupVerboseLog(e.log, e.cfg.Run.IsVerbose)
if e.cfg.Run.CPUProfilePath != "" { if e.cfg.Run.CPUProfilePath != "" {
f, err := os.Create(e.cfg.Run.CPUProfilePath) f, err := os.Create(e.cfg.Run.CPUProfilePath)
if err != nil { if err != nil {
logrus.Fatal(err) e.log.Fatalf("Can't create file %s: %s", e.cfg.Run.CPUProfilePath, err)
} }
if err := pprof.StartCPUProfile(f); err != nil { if err := pprof.StartCPUProfile(f); err != nil {
logrus.Fatal(err) e.log.Fatalf("Can't start CPU profiling: %s", err)
} }
} }
} }
@ -52,11 +41,11 @@ func (e *Executor) persistentPostRun(cmd *cobra.Command, args []string) {
if e.cfg.Run.MemProfilePath != "" { if e.cfg.Run.MemProfilePath != "" {
f, err := os.Create(e.cfg.Run.MemProfilePath) f, err := os.Create(e.cfg.Run.MemProfilePath)
if err != nil { if err != nil {
logrus.Fatal(err) e.log.Fatalf("Can't create file %s: %s", e.cfg.Run.MemProfilePath, err)
} }
runtime.GC() // get up-to-date statistics runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil { if err := pprof.WriteHeapProfile(f); err != nil {
logrus.Fatal("could not write memory profile: ", err) e.log.Fatalf("Can't write heap profile: %s", err)
} }
} }
@ -78,7 +67,7 @@ func (e *Executor) initRoot() {
Long: `Smart, fast linters runner. Run it in cloud for every GitHub pull request on https://golangci.com`, Long: `Smart, fast linters runner. Run it in cloud for every GitHub pull request on https://golangci.com`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := cmd.Help(); err != nil { if err := cmd.Help(); err != nil {
logrus.Fatal(err) e.log.Fatalf("Can't run help: %s", err)
} }
}, },
PersistentPreRun: e.persistentPreRun, PersistentPreRun: e.persistentPreRun,
@ -89,6 +78,10 @@ func (e *Executor) initRoot() {
e.rootCmd = rootCmd e.rootCmd = rootCmd
} }
func (e *Executor) needVersionOption() bool {
return e.date != ""
}
func initRootFlagSet(fs *pflag.FlagSet, cfg *config.Config, needVersionOption bool) { func initRootFlagSet(fs *pflag.FlagSet, cfg *config.Config, needVersionOption bool) {
fs.BoolVarP(&cfg.Run.IsVerbose, "verbose", "v", false, wh("verbose output")) fs.BoolVarP(&cfg.Run.IsVerbose, "verbose", "v", false, wh("verbose output"))
fs.StringVar(&cfg.Run.CPUProfilePath, "cpu-profile-path", "", wh("Path to CPU profile output file")) fs.StringVar(&cfg.Run.CPUProfilePath, "cpu-profile-path", "", wh("Path to CPU profile output file"))

View file

@ -12,22 +12,16 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/golangci/golangci-lint/pkg/lint" "github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb" "github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/printers" "github.com/golangci/golangci-lint/pkg/printers"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/result/processors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
const (
exitCodeIfFailure = 3
exitCodeIfTimeout = 4
)
func getDefaultExcludeHelp() string { func getDefaultExcludeHelp() string {
parts := []string{"Use or not use default excludes:"} parts := []string{"Use or not use default excludes:"}
for _, ep := range config.DefaultExcludePatterns { for _, ep := range config.DefaultExcludePatterns {
@ -64,7 +58,7 @@ func initFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
// Run config // Run config
rc := &cfg.Run rc := &cfg.Run
fs.IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code", fs.IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code",
1, wh("Exit code when issues were found")) exitcodes.IssuesFound, wh("Exit code when issues were found"))
fs.StringSliceVar(&rc.BuildTags, "build-tags", nil, wh("Build tags")) fs.StringSliceVar(&rc.BuildTags, "build-tags", nil, wh("Build tags"))
fs.DurationVar(&rc.Deadline, "deadline", time.Minute, wh("Deadline for total work")) fs.DurationVar(&rc.Deadline, "deadline", time.Minute, wh("Deadline for total work"))
fs.BoolVar(&rc.AnalyzeTests, "tests", true, wh("Analyze tests (*_test.go)")) fs.BoolVar(&rc.AnalyzeTests, "tests", true, wh("Analyze tests (*_test.go)"))
@ -165,65 +159,76 @@ func (e *Executor) initRun() {
// init e.cfg by values from config: flags parse will see these values // init e.cfg by values from config: flags parse will see these values
// like the default ones. It will overwrite them only if the same option // like the default ones. It will overwrite them only if the same option
// is found in command-line: it's ok, command-line has higher priority. // is found in command-line: it's ok, command-line has higher priority.
e.parseConfig()
// Slice options must be explicitly set for properly merging. r := config.NewFileReader(e.cfg, e.log.Child("config_reader"), func(fs *pflag.FlagSet, cfg *config.Config) {
// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
// `changed` variable inside string slice vars will be shared.
// Use another config variable here, not e.cfg, to not
// affect main parsing by this parsing of only config option.
initFlagSet(fs, cfg)
// Parse max options, even force version option: don't want
// to get access to Executor here: it's error-prone to use
// cfg vs e.cfg.
initRootFlagSet(fs, cfg, true)
})
if err := r.Read(); err != nil {
e.log.Fatalf("Can't read config: %s", err)
}
// Slice options must be explicitly set for proper merging of config and command-line options.
fixSlicesFlags(fs) fixSlicesFlags(fs)
} }
func fixSlicesFlags(fs *pflag.FlagSet) {
// It's a dirty hack to set flag.Changed to true for every string slice flag.
// It's necessary to merge config and command-line slices: otherwise command-line
// flags will always overwrite ones from the config.
fs.VisitAll(func(f *pflag.Flag) {
if f.Value.Type() != "stringSlice" {
return
}
s, err := fs.GetStringSlice(f.Name)
if err != nil {
return
}
if s == nil { // assume that every string slice flag has nil as the default
return
}
// calling Set sets Changed to true: next Set calls will append, not overwrite
_ = f.Value.Set(strings.Join(s, ","))
})
}
func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan result.Issue, error) { func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan result.Issue, error) {
e.cfg.Run.Args = args e.cfg.Run.Args = args
linters, err := lintersdb.GetEnabledLinters(e.cfg) linters, err := lintersdb.GetEnabledLinters(e.cfg, e.log.Child("lintersdb"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
lintCtx, err := lint.LoadContext(ctx, linters, e.cfg) lintCtx, err := lint.LoadContext(ctx, linters, e.cfg, e.log.Child("load"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
excludePatterns := e.cfg.Issues.ExcludePatterns runner, err := lint.NewRunner(lintCtx.ASTCache, e.cfg, e.log.Child("runner"))
if e.cfg.Issues.UseDefaultExcludes {
excludePatterns = append(excludePatterns, config.GetDefaultExcludePatternsStrings()...)
}
var excludeTotalPattern string
if len(excludePatterns) != 0 {
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
}
skipFilesProcessor, err := processors.NewSkipFiles(e.cfg.Run.SkipFiles)
if err != nil { if err != nil {
return nil, err return nil, err
} }
runner := lint.SimpleRunner{
Processors: []processors.Processor{
processors.NewPathPrettifier(), // must be before diff, nolint and exclude autogenerated processor at least
processors.NewCgo(),
skipFilesProcessor,
processors.NewAutogeneratedExclude(lintCtx.ASTCache),
processors.NewExclude(excludeTotalPattern),
processors.NewNolint(lintCtx.ASTCache),
processors.NewUniqByLine(),
processors.NewDiff(e.cfg.Issues.Diff, e.cfg.Issues.DiffFromRevision, e.cfg.Issues.DiffPatchFilePath),
processors.NewMaxPerFileFromLinter(),
processors.NewMaxSameIssues(e.cfg.Issues.MaxSameIssues),
processors.NewMaxFromLinter(e.cfg.Issues.MaxIssuesPerLinter),
},
}
return runner.Run(ctx, linters, lintCtx), nil return runner.Run(ctx, linters, lintCtx), nil
} }
func setOutputToDevNull() (savedStdout, savedStderr *os.File) { func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) {
savedStdout, savedStderr = os.Stdout, os.Stderr savedStdout, savedStderr = os.Stdout, os.Stderr
devNull, err := os.Open(os.DevNull) devNull, err := os.Open(os.DevNull)
if err != nil { if err != nil {
logrus.Warnf("can't open null device %q: %s", os.DevNull, err) e.log.Warnf("Can't open null device %q: %s", os.DevNull, err)
return return
} }
@ -235,7 +240,7 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
if !logutils.HaveDebugTag("linters_output") { if !logutils.HaveDebugTag("linters_output") {
// Don't allow linters and loader to print anything // Don't allow linters and loader to print anything
log.SetOutput(ioutil.Discard) log.SetOutput(ioutil.Discard)
savedStdout, savedStderr := setOutputToDevNull() savedStdout, savedStderr := e.setOutputToDevNull()
defer func() { defer func() {
os.Stdout, os.Stderr = savedStdout, savedStderr os.Stdout, os.Stderr = savedStdout, savedStderr
}() }()
@ -253,9 +258,11 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
p = printers.NewJSON() p = printers.NewJSON()
case config.OutFormatColoredLineNumber, config.OutFormatLineNumber: case config.OutFormatColoredLineNumber, config.OutFormatLineNumber:
p = printers.NewText(e.cfg.Output.PrintIssuedLine, p = printers.NewText(e.cfg.Output.PrintIssuedLine,
format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName) format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName,
e.log.Child("text_printer"))
case config.OutFormatTab: case config.OutFormatTab:
p = printers.NewTab(e.cfg.Output.PrintLinterName) p = printers.NewTab(e.cfg.Output.PrintLinterName,
e.log.Child("tab_printer"))
case config.OutFormatCheckstyle: case config.OutFormatCheckstyle:
p = printers.NewCheckstyle() p = printers.NewCheckstyle()
default: default:
@ -288,22 +295,22 @@ func (e *Executor) executeRun(cmd *cobra.Command, args []string) {
defer cancel() defer cancel()
if needTrackResources { if needTrackResources {
go watchResources(ctx, trackResourcesEndCh) go watchResources(ctx, trackResourcesEndCh, e.log)
} }
if err := e.runAndPrint(ctx, args); err != nil { if err := e.runAndPrint(ctx, args); err != nil {
logrus.Warnf("running error: %s", err) e.log.Errorf("Running error: %s", err)
if e.exitCode == 0 { if e.exitCode == exitcodes.Success {
e.exitCode = exitCodeIfFailure e.exitCode = exitcodes.Failure
} }
} }
if e.exitCode == 0 && ctx.Err() != nil { if e.exitCode == exitcodes.Success && ctx.Err() != nil {
e.exitCode = exitCodeIfTimeout e.exitCode = exitcodes.Timeout
} }
} }
func watchResources(ctx context.Context, done chan struct{}) { func watchResources(ctx context.Context, done chan struct{}, log logutils.Log) {
startedAt := time.Now() startedAt := time.Now()
rssValues := []uint64{} rssValues := []uint64{}
@ -339,8 +346,8 @@ func watchResources(ctx context.Context, done chan struct{}) {
const MB = 1024 * 1024 const MB = 1024 * 1024
maxMB := float64(max) / MB maxMB := float64(max) / MB
logrus.Infof("Memory: %d samples, avg is %.1fMB, max is %.1fMB", log.Infof("Memory: %d samples, avg is %.1fMB, max is %.1fMB",
len(rssValues), float64(avg)/MB, maxMB) len(rssValues), float64(avg)/MB, maxMB)
logrus.Infof("Execution took %s", time.Since(startedAt)) log.Infof("Execution took %s", time.Since(startedAt))
close(done) close(done)
} }

View file

@ -1,217 +0,0 @@
package commands
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/printers"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
func (e *Executor) parseConfigImpl() {
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return
}
logrus.Fatalf("Can't read viper config: %s", err)
}
usedConfigFile := viper.ConfigFileUsed()
if usedConfigFile == "" {
return
}
logrus.Infof("Used config file %s", getRelPath(usedConfigFile))
if err := viper.Unmarshal(&e.cfg); err != nil {
logrus.Fatalf("Can't unmarshal config by viper: %s", err)
}
if err := e.validateConfig(); err != nil {
logrus.Fatal(err)
}
if e.cfg.InternalTest { // just for testing purposes: to detect config file usage
fmt.Fprintln(printers.StdOut, "test")
os.Exit(0)
}
}
func (e *Executor) validateConfig() error {
c := e.cfg
if len(c.Run.Args) != 0 {
return errors.New("option run.args in config isn't supported now")
}
if c.Run.CPUProfilePath != "" {
return errors.New("option run.cpuprofilepath in config isn't allowed")
}
if c.Run.MemProfilePath != "" {
return errors.New("option run.memprofilepath in config isn't allowed")
}
if c.Run.IsVerbose {
return errors.New("can't set run.verbose option with config: only on command-line")
}
return nil
}
func setupConfigFileSearch(args []string) {
// skip all args ([golangci-lint, run/linters]) before files/dirs list
for len(args) != 0 {
if args[0] == "run" {
args = args[1:]
break
}
args = args[1:]
}
// find first file/dir arg
firstArg := "./..."
if len(args) != 0 {
firstArg = args[0]
}
absStartPath, err := filepath.Abs(firstArg)
if err != nil {
logutils.HiddenWarnf("Can't make abs path for %q: %s", firstArg, err)
absStartPath = filepath.Clean(firstArg)
}
// start from it
var curDir string
if fsutils.IsDir(absStartPath) {
curDir = absStartPath
} else {
curDir = filepath.Dir(absStartPath)
}
// find all dirs from it up to the root
configSearchPaths := []string{"./"}
for {
configSearchPaths = append(configSearchPaths, curDir)
newCurDir := filepath.Dir(curDir)
if curDir == newCurDir || newCurDir == "" {
break
}
curDir = newCurDir
}
logrus.Infof("Config search paths: %s", configSearchPaths)
viper.SetConfigName(".golangci")
for _, p := range configSearchPaths {
viper.AddConfigPath(p)
}
}
func getRelPath(p string) string {
wd, err := os.Getwd()
if err != nil {
logutils.HiddenWarnf("Can't get wd: %s", err)
return p
}
r, err := filepath.Rel(wd, p)
if err != nil {
logutils.HiddenWarnf("Can't make path %s relative to %s: %s", p, wd, err)
return p
}
return r
}
func (e *Executor) needVersionOption() bool {
return e.date != ""
}
func parseConfigOption() (string, []string, error) {
// We use another pflag.FlagSet here to not set `changed` flag
// on cmd.Flags() options. Otherwise string slice options will be duplicated.
fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
// `changed` variable inside string slice vars will be shared.
// Use another config variable here, not e.cfg, to not
// affect main parsing by this parsing of only config option.
var cfg config.Config
initFlagSet(fs, &cfg)
// Parse max options, even force version option: don't want
// to get access to Executor here: it's error-prone to use
// cfg vs e.cfg.
initRootFlagSet(fs, &cfg, true)
fs.Usage = func() {} // otherwise help text will be printed twice
if err := fs.Parse(os.Args); err != nil {
if err == pflag.ErrHelp {
return "", nil, err
}
logrus.Fatalf("Can't parse args: %s", err)
}
setupLog(cfg.Run.IsVerbose) // for `-v` to work until running of preRun function
configFile := cfg.Run.Config
if cfg.Run.NoConfig && configFile != "" {
logrus.Fatal("can't combine option --config and --no-config")
}
if cfg.Run.NoConfig {
return "", nil, fmt.Errorf("no need to use config")
}
return configFile, fs.Args(), nil
}
func (e *Executor) parseConfig() {
// XXX: hack with double parsing for 2 purposes:
// 1. to access "config" option here.
// 2. to give config less priority than command line.
configFile, restArgs, err := parseConfigOption()
if err != nil {
return // skippable error, e.g. --no-config
}
if configFile != "" {
viper.SetConfigFile(configFile)
} else {
setupConfigFileSearch(restArgs)
}
e.parseConfigImpl()
}
func fixSlicesFlags(fs *pflag.FlagSet) {
// It's a dirty hack to set flag.Changed to true for every string slice flag.
// It's necessary to merge config and command-line slices: otherwise command-line
// flags will always overwrite ones from the config.
fs.VisitAll(func(f *pflag.Flag) {
if f.Value.Type() != "stringSlice" {
return
}
s, err := fs.GetStringSlice(f.Name)
if err != nil {
return
}
if s == nil { // assume that every string slice flag has nil as the default
return
}
// calling Set sets Changed to true: next Set calls will append, not overwrite
_ = f.Value.Set(strings.Join(s, ","))
})
}

View file

@ -35,7 +35,7 @@ var DefaultExcludePatterns = []ExcludePattern{
Why: "Almost all programs ignore errors on these functions and in most cases it's ok", Why: "Almost all programs ignore errors on these functions and in most cases it's ok",
}, },
{ {
Pattern: "(comment on exported (method|function|type)|should have( a package)? comment|comment should be of the form)", Pattern: "(comment on exported (method|function|type|const)|should have( a package)? comment|comment should be of the form)",
Linter: "golint", Linter: "golint",
Why: "Annoying issue about not having a comment. The rare codebase has such comments", Why: "Annoying issue about not having a comment. The rare codebase has such comments",
}, },

193
pkg/config/reader.go Normal file
View file

@ -0,0 +1,193 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/printers"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type FlagSetInit func(fs *pflag.FlagSet, cfg *Config)
type FileReader struct {
log logutils.Log
cfg *Config
flagSetInit FlagSetInit
}
func NewFileReader(toCfg *Config, log logutils.Log, flagSetInit FlagSetInit) *FileReader {
return &FileReader{
log: log,
cfg: toCfg,
flagSetInit: flagSetInit,
}
}
func (r *FileReader) Read() error {
// XXX: hack with double parsing for 2 purposes:
// 1. to access "config" option here.
// 2. to give config less priority than command line.
configFile, restArgs, err := r.parseConfigOption()
if err != nil {
if err == errConfigDisabled || err == pflag.ErrHelp {
return nil
}
return fmt.Errorf("can't parse --config option: %s", err)
}
if configFile != "" {
viper.SetConfigFile(configFile)
} else {
r.setupConfigFileSearch(restArgs)
}
return r.parseConfig()
}
func (r *FileReader) parseConfig() error {
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return nil
}
return fmt.Errorf("can't read viper config: %s", err)
}
usedConfigFile := viper.ConfigFileUsed()
if usedConfigFile == "" {
return nil
}
usedConfigFile, err := fsutils.ShortestRelPath(usedConfigFile, "")
if err != nil {
r.log.Warnf("Can't pretty print config file path: %s", err)
}
r.log.Infof("Used config file %s", usedConfigFile)
if err := viper.Unmarshal(r.cfg); err != nil {
return fmt.Errorf("can't unmarshal config by viper: %s", err)
}
if err := r.validateConfig(); err != nil {
return fmt.Errorf("can't validate config: %s", err)
}
if r.cfg.InternalTest { // just for testing purposes: to detect config file usage
fmt.Fprintln(printers.StdOut, "test")
os.Exit(0)
}
return nil
}
func (r *FileReader) validateConfig() error {
c := r.cfg
if len(c.Run.Args) != 0 {
return errors.New("option run.args in config isn't supported now")
}
if c.Run.CPUProfilePath != "" {
return errors.New("option run.cpuprofilepath in config isn't allowed")
}
if c.Run.MemProfilePath != "" {
return errors.New("option run.memprofilepath in config isn't allowed")
}
if c.Run.IsVerbose {
return errors.New("can't set run.verbose option with config: only on command-line")
}
return nil
}
func (r *FileReader) setupConfigFileSearch(args []string) {
// skip all args ([golangci-lint, run/linters]) before files/dirs list
for len(args) != 0 {
if args[0] == "run" {
args = args[1:]
break
}
args = args[1:]
}
// find first file/dir arg
firstArg := "./..."
if len(args) != 0 {
firstArg = args[0]
}
absStartPath, err := filepath.Abs(firstArg)
if err != nil {
r.log.Warnf("Can't make abs path for %q: %s", firstArg, err)
absStartPath = filepath.Clean(firstArg)
}
// start from it
var curDir string
if fsutils.IsDir(absStartPath) {
curDir = absStartPath
} else {
curDir = filepath.Dir(absStartPath)
}
// find all dirs from it up to the root
configSearchPaths := []string{"./"}
for {
configSearchPaths = append(configSearchPaths, curDir)
newCurDir := filepath.Dir(curDir)
if curDir == newCurDir || newCurDir == "" {
break
}
curDir = newCurDir
}
r.log.Infof("Config search paths: %s", configSearchPaths)
viper.SetConfigName(".golangci")
for _, p := range configSearchPaths {
viper.AddConfigPath(p)
}
}
var errConfigDisabled = errors.New("config is disabled by --no-config")
func (r *FileReader) parseConfigOption() (string, []string, error) {
// We use another pflag.FlagSet here to not set `changed` flag
// on cmd.Flags() options. Otherwise string slice options will be duplicated.
fs := pflag.NewFlagSet("config flag set", pflag.ContinueOnError)
var cfg Config
r.flagSetInit(fs, &cfg)
fs.Usage = func() {} // otherwise help text will be printed twice
if err := fs.Parse(os.Args); err != nil {
if err == pflag.ErrHelp {
return "", nil, err
}
return "", nil, fmt.Errorf("can't parse args: %s", err)
}
// for `-v` to work until running of preRun function
logutils.SetupVerboseLog(r.log, cfg.Run.IsVerbose)
configFile := cfg.Run.Config
if cfg.Run.NoConfig && configFile != "" {
return "", nil, fmt.Errorf("can't combine option --config and --no-config")
}
if cfg.Run.NoConfig {
return "", nil, errConfigDisabled
}
return configFile, fs.Args(), nil
}

View file

@ -0,0 +1,9 @@
package exitcodes
const (
Success = 0
IssuesFound = 1
WarningInTest = 2
Failure = 3
Timeout = 4
)

View file

@ -1,10 +1,40 @@
package fsutils package fsutils
import ( import (
"fmt"
"os" "os"
"path/filepath"
) )
func IsDir(filename string) bool { func IsDir(filename string) bool {
fi, err := os.Stat(filename) fi, err := os.Stat(filename)
return err == nil && fi.IsDir() return err == nil && fi.IsDir()
} }
func ShortestRelPath(path string, wd string) (string, error) {
if wd == "" { // get it if user don't have cached working dir
var err error
wd, err = os.Getwd()
if err != nil {
return "", fmt.Errorf("can't get working directory: %s", err)
}
}
// make path absolute and then relative to be able to fix this case:
// we'are in /test dir, we want to normalize ../test, and have file file.go in this dir;
// it must have normalized path file.go, not ../test/file.go,
var absPath string
if filepath.IsAbs(path) {
absPath = path
} else {
absPath = filepath.Join(wd, path)
}
relPath, err := filepath.Rel(wd, absPath)
if err != nil {
return "", fmt.Errorf("can't get relative path for path %s and root %s: %s",
absPath, wd, err)
}
return relPath, nil
}

View file

@ -11,7 +11,6 @@ import (
"github.com/GoASTScanner/gas" "github.com/GoASTScanner/gas"
"github.com/GoASTScanner/gas/rules" "github.com/GoASTScanner/gas/rules"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
@ -46,7 +45,7 @@ func (lint Gas) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issu
if err != nil { if err != nil {
r = &result.Range{} r = &result.Range{}
if n, rerr := fmt.Sscanf(i.Line, "%d-%d", &r.From, &r.To); rerr != nil || n != 2 { if n, rerr := fmt.Sscanf(i.Line, "%d-%d", &r.From, &r.To); rerr != nil || n != 2 {
logutils.HiddenWarnf("Can't convert gas line number %q of %v to int: %s", i.Line, i, err) lintCtx.Log.Warnf("Can't convert gas line number %q of %v to int: %s", i.Line, i, err)
continue continue
} }
line = r.From line = r.From

View file

@ -9,8 +9,8 @@ import (
gofmtAPI "github.com/golangci/gofmt/gofmt" gofmtAPI "github.com/golangci/gofmt/gofmt"
goimportsAPI "github.com/golangci/gofmt/goimports" goimportsAPI "github.com/golangci/gofmt/goimports"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
"sourcegraph.com/sourcegraph/go-diff/diff" "sourcegraph.com/sourcegraph/go-diff/diff"
) )
@ -55,7 +55,7 @@ func getFirstDeletedAndAddedLineNumberInHunk(h *diff.Hunk) (int, int, error) {
return 0, firstAddedLineNumber, fmt.Errorf("didn't find deletion line in hunk %s", string(h.Body)) return 0, firstAddedLineNumber, fmt.Errorf("didn't find deletion line in hunk %s", string(h.Body))
} }
func (g Gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) { func (g Gofmt) extractIssuesFromPatch(patch string, log logutils.Log) ([]result.Issue, error) {
diffs, err := diff.ParseMultiFileDiff([]byte(patch)) diffs, err := diff.ParseMultiFileDiff([]byte(patch))
if err != nil { if err != nil {
return nil, fmt.Errorf("can't parse patch: %s", err) return nil, fmt.Errorf("can't parse patch: %s", err)
@ -68,7 +68,7 @@ func (g Gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
issues := []result.Issue{} issues := []result.Issue{}
for _, d := range diffs { for _, d := range diffs {
if len(d.Hunks) == 0 { if len(d.Hunks) == 0 {
logrus.Warnf("Got no hunks in diff %+v", d) log.Warnf("Got no hunks in diff %+v", d)
continue continue
} }
@ -119,7 +119,7 @@ func (g Gofmt) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue
continue continue
} }
is, err := g.extractIssuesFromPatch(string(diff)) is, err := g.extractIssuesFromPatch(string(diff), lintCtx.Log)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't extract issues from gofmt diff output %q: %s", string(diff), err) return nil, fmt.Errorf("can't extract issues from gofmt diff output %q: %s", string(diff), err)
} }

View file

@ -7,7 +7,6 @@ import (
"go/token" "go/token"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
lintAPI "github.com/golangci/lint-1" lintAPI "github.com/golangci/lint-1"
) )
@ -39,7 +38,7 @@ func (g Golint) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issu
issues = append(issues, i...) issues = append(issues, i...)
} }
if lintErr != nil { if lintErr != nil {
logutils.HiddenWarnf("golint: %s", lintErr) lintCtx.Log.Warnf("Golint: %s", lintErr)
} }
return issues, nil return issues, nil

View file

@ -8,7 +8,6 @@ import (
megacheckAPI "github.com/golangci/go-tools/cmd/megacheck" megacheckAPI "github.com/golangci/go-tools/cmd/megacheck"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
) )
type Megacheck struct { type Megacheck struct {
@ -57,7 +56,7 @@ func (m Megacheck) Run(ctx context.Context, lintCtx *linter.Context) ([]result.I
for _, p := range lintCtx.NotCompilingPackages { for _, p := range lintCtx.NotCompilingPackages {
packages = append(packages, p.String()) packages = append(packages, p.String())
} }
logrus.Warnf("Can't run megacheck because of compilation errors in packages "+ lintCtx.Log.Warnf("Can't run megacheck because of compilation errors in packages "+
"%s: run `typecheck` linter to see errors", packages) "%s: run `typecheck` linter to see errors", packages)
// megacheck crashes if there are not compiling packages // megacheck crashes if there are not compiling packages
return nil, nil return nil, nil

View file

@ -35,7 +35,7 @@ func getASTFilesForPkg(ctx *linter.Context, pkg *packages.Package) ([]*ast.File,
for _, filename := range filenames { for _, filename := range filenames {
f := ctx.ASTCache.Get(filename) f := ctx.ASTCache.Get(filename)
if f == nil { if f == nil {
return nil, nil, fmt.Errorf("no AST for file %s in cache", filename) return nil, nil, fmt.Errorf("no AST for file %s in cache: %+v", filename, *ctx.ASTCache)
} }
if f.Err != nil { if f.Err != nil {

View file

@ -8,7 +8,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/sirupsen/logrus" "github.com/golangci/golangci-lint/pkg/logutils"
"golang.org/x/tools/go/loader" "golang.org/x/tools/go/loader"
) )
@ -20,13 +20,15 @@ type File struct {
} }
type Cache struct { type Cache struct {
m map[string]*File m map[string]*File
s []*File s []*File
log logutils.Log
} }
func NewCache() *Cache { func NewCache(log logutils.Log) *Cache {
return &Cache{ return &Cache{
m: map[string]*File{}, m: map[string]*File{},
log: log,
} }
} }
@ -40,7 +42,7 @@ func (c Cache) GetOrParse(filename string) *File {
return f return f
} }
logrus.Infof("Parse AST for file %s on demand", filename) c.log.Infof("Parse AST for file %s on demand", filename)
c.parseFile(filename, nil) c.parseFile(filename, nil)
return c.m[filename] return c.m[filename]
} }
@ -79,7 +81,7 @@ func LoadFromProgram(prog *loader.Program) (*Cache, error) {
relPath, err := filepath.Rel(root, pos.Filename) relPath, err := filepath.Rel(root, pos.Filename)
if err != nil { if err != nil {
logrus.Warnf("Can't get relative path for %s and %s: %s", c.log.Warnf("Can't get relative path for %s and %s: %s",
root, pos.Filename, err) root, pos.Filename, err)
continue continue
} }
@ -108,6 +110,9 @@ func (c *Cache) parseFile(filePath string, fset *token.FileSet) {
Err: err, Err: err,
Name: filePath, Name: filePath,
} }
if err != nil {
c.log.Warnf("Can't parse AST of %s: %s", filePath, err)
}
} }
func LoadFromFiles(files []string) (*Cache, error) { func LoadFromFiles(files []string) (*Cache, error) {

View file

@ -4,6 +4,7 @@ import (
"github.com/golangci/go-tools/ssa" "github.com/golangci/go-tools/ssa"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/lint/astcache" "github.com/golangci/golangci-lint/pkg/lint/astcache"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/packages" "github.com/golangci/golangci-lint/pkg/packages"
"golang.org/x/tools/go/loader" "golang.org/x/tools/go/loader"
) )
@ -16,6 +17,7 @@ type Context struct {
LoaderConfig *loader.Config LoaderConfig *loader.Config
ASTCache *astcache.Cache ASTCache *astcache.Cache
NotCompilingPackages []*loader.PackageInfo NotCompilingPackages []*loader.PackageInfo
Log logutils.Log
} }
func (c *Context) Settings() *config.LintersSettings { func (c *Context) Settings() *config.LintersSettings {

View file

@ -10,7 +10,7 @@ import (
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/golinters" "github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/sirupsen/logrus" "github.com/golangci/golangci-lint/pkg/logutils"
) )
func AllPresets() []string { func AllPresets() []string {
@ -398,7 +398,7 @@ func optimizeLintersSet(linters map[string]*linter.Config) {
linters[m.Name()] = &lc linters[m.Name()] = &lc
} }
func GetEnabledLinters(cfg *config.Config) ([]linter.Config, error) { func GetEnabledLinters(cfg *config.Config, log logutils.Log) ([]linter.Config, error) {
if err := validateEnabledDisabledLintersConfig(&cfg.Linters); err != nil { if err := validateEnabledDisabledLintersConfig(&cfg.Linters); err != nil {
return nil, err return nil, err
} }
@ -410,21 +410,21 @@ func GetEnabledLinters(cfg *config.Config) ([]linter.Config, error) {
resultLinters = append(resultLinters, *lc) resultLinters = append(resultLinters, *lc)
} }
verbosePrintLintersStatus(cfg, resultLinters) verbosePrintLintersStatus(cfg, resultLinters, log)
return resultLinters, nil return resultLinters, nil
} }
func verbosePrintLintersStatus(cfg *config.Config, lcs []linter.Config) { func verbosePrintLintersStatus(cfg *config.Config, lcs []linter.Config, log logutils.Log) {
var linterNames []string var linterNames []string
for _, lc := range lcs { for _, lc := range lcs {
linterNames = append(linterNames, lc.Linter.Name()) linterNames = append(linterNames, lc.Linter.Name())
} }
sort.StringSlice(linterNames).Sort() sort.StringSlice(linterNames).Sort()
logrus.Infof("Active %d linters: %s", len(linterNames), linterNames) log.Infof("Active %d linters: %s", len(linterNames), linterNames)
if len(cfg.Linters.Presets) != 0 { if len(cfg.Linters.Presets) != 0 {
sort.StringSlice(cfg.Linters.Presets).Sort() sort.StringSlice(cfg.Linters.Presets).Sort()
logrus.Infof("Active presets: %s", cfg.Linters.Presets) log.Infof("Active presets: %s", cfg.Linters.Presets)
} }
} }

View file

@ -19,7 +19,6 @@ import (
"github.com/golangci/golangci-lint/pkg/lint/astcache" "github.com/golangci/golangci-lint/pkg/lint/astcache"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/packages" "github.com/golangci/golangci-lint/pkg/packages"
"github.com/sirupsen/logrus"
"golang.org/x/tools/go/loader" "golang.org/x/tools/go/loader"
) )
@ -105,7 +104,7 @@ func isLocalProjectAnalysis(args []string) bool {
return true return true
} }
func getTypeCheckFuncBodies(cfg *config.Run, linters []linter.Config, pkgProg *packages.Program) func(string) bool { func getTypeCheckFuncBodies(cfg *config.Run, linters []linter.Config, pkgProg *packages.Program, log logutils.Log) func(string) bool {
if !isLocalProjectAnalysis(cfg.Args) { if !isLocalProjectAnalysis(cfg.Args) {
loadDebugf("analysis in nonlocal, don't optimize loading by not typechecking func bodies") loadDebugf("analysis in nonlocal, don't optimize loading by not typechecking func bodies")
return nil return nil
@ -123,7 +122,7 @@ func getTypeCheckFuncBodies(cfg *config.Run, linters []linter.Config, pkgProg *p
projPath, err := getCurrentProjectImportPath() projPath, err := getCurrentProjectImportPath()
if err != nil { if err != nil {
logrus.Infof("can't get cur project path: %s", err) log.Infof("Can't get cur project path: %s", err)
return nil return nil
} }
@ -151,14 +150,14 @@ func getTypeCheckFuncBodies(cfg *config.Run, linters []linter.Config, pkgProg *p
} }
} }
func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *config.Run, pkgProg *packages.Program) (*loader.Program, *loader.Config, error) { func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *config.Run, pkgProg *packages.Program, log logutils.Log) (*loader.Program, *loader.Config, error) {
if !isFullImportNeeded(linters) { if !isFullImportNeeded(linters) {
return nil, nil, nil return nil, nil, nil
} }
startedAt := time.Now() startedAt := time.Now()
defer func() { defer func() {
logrus.Infof("Program loading took %s", time.Since(startedAt)) log.Infof("Program loading took %s", time.Since(startedAt))
}() }()
bctx := pkgProg.BuildContext() bctx := pkgProg.BuildContext()
@ -166,7 +165,7 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con
Build: bctx, Build: bctx,
AllowErrors: true, // Try to analyze partially AllowErrors: true, // Try to analyze partially
ParserMode: parser.ParseComments, // AST will be reused by linters ParserMode: parser.ParseComments, // AST will be reused by linters
TypeCheckFuncBodies: getTypeCheckFuncBodies(cfg, linters, pkgProg), TypeCheckFuncBodies: getTypeCheckFuncBodies(cfg, linters, pkgProg, log),
} }
var loaderArgs []string var loaderArgs []string
@ -195,13 +194,22 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con
return nil, nil, fmt.Errorf("can't load program from paths %v: %s", loaderArgs, err) return nil, nil, fmt.Errorf("can't load program from paths %v: %s", loaderArgs, err)
} }
if len(prog.InitialPackages()) == 1 {
pkg := prog.InitialPackages()[0]
var files []string
for _, f := range pkg.Files {
files = append(files, prog.Fset.Position(f.Pos()).Filename)
}
log.Infof("pkg %s files: %s", pkg, files)
}
return prog, loadcfg, nil return prog, loadcfg, nil
} }
func buildSSAProgram(ctx context.Context, lprog *loader.Program) *ssa.Program { func buildSSAProgram(ctx context.Context, lprog *loader.Program, log logutils.Log) *ssa.Program {
startedAt := time.Now() startedAt := time.Now()
defer func() { defer func() {
logrus.Infof("SSA repr building took %s", time.Since(startedAt)) log.Infof("SSA repr building took %s", time.Since(startedAt))
}() }()
ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug) ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug)
@ -249,10 +257,14 @@ func separateNotCompilingPackages(lintCtx *linter.Context) {
} }
} }
} }
if len(lintCtx.NotCompilingPackages) != 0 {
lintCtx.Log.Infof("Not compiling packages: %+v", lintCtx.NotCompilingPackages)
}
} }
//nolint:gocyclo //nolint:gocyclo
func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Config) (*linter.Context, error) { func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Config, log logutils.Log) (*linter.Context, error) {
// Set GOROOT to have working cross-compilation: cross-compiled binaries // Set GOROOT to have working cross-compilation: cross-compiled binaries
// have invalid GOROOT. XXX: can't use runtime.GOROOT(). // have invalid GOROOT. XXX: can't use runtime.GOROOT().
goroot, err := discoverGoRoot() goroot, err := discoverGoRoot()
@ -269,7 +281,7 @@ func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Confi
skipDirs := append([]string{}, packages.StdExcludeDirRegexps...) skipDirs := append([]string{}, packages.StdExcludeDirRegexps...)
skipDirs = append(skipDirs, cfg.Run.SkipDirs...) skipDirs = append(skipDirs, cfg.Run.SkipDirs...)
r, err := packages.NewResolver(cfg.Run.BuildTags, skipDirs) r, err := packages.NewResolver(cfg.Run.BuildTags, skipDirs, log.Child("path_resolver"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -279,14 +291,14 @@ func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Confi
return nil, err return nil, err
} }
prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, pkgProg) prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, pkgProg, log)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var ssaProg *ssa.Program var ssaProg *ssa.Program
if prog != nil && isSSAReprNeeded(linters) { if prog != nil && isSSAReprNeeded(linters) {
ssaProg = buildSSAProgram(ctx, prog) ssaProg = buildSSAProgram(ctx, prog, log)
} }
var astCache *astcache.Cache var astCache *astcache.Cache
@ -306,6 +318,7 @@ func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Confi
SSAProgram: ssaProg, SSAProgram: ssaProg,
LoaderConfig: loaderConfig, LoaderConfig: loaderConfig,
ASTCache: astCache, ASTCache: astCache,
Log: log,
} }
if prog != nil { if prog != nil {

View file

@ -4,6 +4,8 @@ import (
"context" "context"
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/lint/astcache" "github.com/golangci/golangci-lint/pkg/lint/astcache"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
@ -19,7 +21,7 @@ func TestASTCacheLoading(t *testing.T) {
inputPaths := []string{"./...", "./", "./load.go", "load.go"} inputPaths := []string{"./...", "./", "./load.go", "load.go"}
for _, inputPath := range inputPaths { for _, inputPath := range inputPaths {
r, err := packages.NewResolver(nil, nil) r, err := packages.NewResolver(nil, nil, logutils.NewStderrLog(""))
assert.NoError(t, err) assert.NoError(t, err)
pkgProg, err := r.Resolve(inputPath) pkgProg, err := r.Resolve(inputPath)
@ -30,7 +32,7 @@ func TestASTCacheLoading(t *testing.T) {
prog, _, err := loadWholeAppIfNeeded(ctx, linters, &config.Run{ prog, _, err := loadWholeAppIfNeeded(ctx, linters, &config.Run{
AnalyzeTests: true, AnalyzeTests: true,
}, pkgProg) }, pkgProg, logutils.NewStderrLog(""))
assert.NoError(t, err) assert.NoError(t, err)
astCacheFromProg, err := astcache.LoadFromProgram(prog) astCacheFromProg, err := astcache.LoadFromProgram(prog)

View file

@ -9,16 +9,55 @@ import (
"sync" "sync"
"time" "time"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/lint/astcache"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/result/processors" "github.com/golangci/golangci-lint/pkg/result/processors"
"github.com/golangci/golangci-lint/pkg/timeutils" "github.com/golangci/golangci-lint/pkg/timeutils"
"github.com/sirupsen/logrus"
) )
type SimpleRunner struct { type Runner struct {
Processors []processors.Processor Processors []processors.Processor
Log logutils.Log
}
func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log) (*Runner, error) {
icfg := cfg.Issues
excludePatterns := icfg.ExcludePatterns
if icfg.UseDefaultExcludes {
excludePatterns = append(excludePatterns, config.GetDefaultExcludePatternsStrings()...)
}
var excludeTotalPattern string
if len(excludePatterns) != 0 {
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
}
skipFilesProcessor, err := processors.NewSkipFiles(cfg.Run.SkipFiles)
if err != nil {
return nil, err
}
return &Runner{
Processors: []processors.Processor{
processors.NewPathPrettifier(), // must be before diff, nolint and exclude autogenerated processor at least
processors.NewCgo(),
skipFilesProcessor,
processors.NewAutogeneratedExclude(astCache),
processors.NewExclude(excludeTotalPattern),
processors.NewNolint(astCache),
processors.NewUniqByLine(),
processors.NewDiff(icfg.Diff, icfg.DiffFromRevision, icfg.DiffPatchFilePath),
processors.NewMaxPerFileFromLinter(),
processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues")),
processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter")),
},
Log: log,
}, nil
} }
type lintRes struct { type lintRes struct {
@ -27,15 +66,17 @@ type lintRes struct {
issues []result.Issue issues []result.Issue
} }
func runLinterSafe(ctx context.Context, lintCtx *linter.Context, lc linter.Config) (ret []result.Issue, err error) { func (r Runner) runLinterSafe(ctx context.Context, lintCtx *linter.Context, lc linter.Config) (ret []result.Issue, err error) {
defer func() { defer func() {
if panicData := recover(); panicData != nil { if panicData := recover(); panicData != nil {
err = fmt.Errorf("panic occured: %s", panicData) err = fmt.Errorf("panic occured: %s", panicData)
logutils.HiddenWarnf("Panic stack trace: %s", debug.Stack()) r.Log.Warnf("Panic stack trace: %s", debug.Stack())
} }
}() }()
issues, err := lc.Linter.Run(ctx, lintCtx) specificLintCtx := *lintCtx
specificLintCtx.Log = lintCtx.Log.Child(lc.Linter.Name())
issues, err := lc.Linter.Run(ctx, &specificLintCtx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -47,8 +88,8 @@ func runLinterSafe(ctx context.Context, lintCtx *linter.Context, lc linter.Confi
return issues, nil return issues, nil
} }
func runWorker(ctx context.Context, lintCtx *linter.Context, tasksCh <-chan linter.Config, lintResultsCh chan<- lintRes, name string) { func (r Runner) runWorker(ctx context.Context, lintCtx *linter.Context, tasksCh <-chan linter.Config, lintResultsCh chan<- lintRes, name string) {
sw := timeutils.NewStopwatch(name) sw := timeutils.NewStopwatch(name, r.Log)
defer sw.Print() defer sw.Print()
for { for {
@ -67,7 +108,7 @@ func runWorker(ctx context.Context, lintCtx *linter.Context, tasksCh <-chan lint
var issues []result.Issue var issues []result.Issue
var err error var err error
sw.TrackStage(lc.Linter.Name(), func() { sw.TrackStage(lc.Linter.Name(), func() {
issues, err = runLinterSafe(ctx, lintCtx, lc) issues, err = r.runLinterSafe(ctx, lintCtx, lc)
}) })
lintResultsCh <- lintRes{ lintResultsCh <- lintRes{
linter: lc, linter: lc,
@ -78,7 +119,7 @@ func runWorker(ctx context.Context, lintCtx *linter.Context, tasksCh <-chan lint
} }
} }
func logWorkersStat(workersFinishTimes []time.Time) { func (r Runner) logWorkersStat(workersFinishTimes []time.Time) {
lastFinishTime := workersFinishTimes[0] lastFinishTime := workersFinishTimes[0]
for _, t := range workersFinishTimes { for _, t := range workersFinishTimes {
if t.After(lastFinishTime) { if t.After(lastFinishTime) {
@ -95,7 +136,7 @@ func logWorkersStat(workersFinishTimes []time.Time) {
logStrings = append(logStrings, fmt.Sprintf("#%d: %s", i+1, lastFinishTime.Sub(t))) logStrings = append(logStrings, fmt.Sprintf("#%d: %s", i+1, lastFinishTime.Sub(t)))
} }
logrus.Infof("Workers idle times: %s", strings.Join(logStrings, ", ")) r.Log.Infof("Workers idle times: %s", strings.Join(logStrings, ", "))
} }
func getSortedLintersConfigs(linters []linter.Config) []linter.Config { func getSortedLintersConfigs(linters []linter.Config) []linter.Config {
@ -109,7 +150,7 @@ func getSortedLintersConfigs(linters []linter.Config) []linter.Config {
return ret return ret
} }
func (r *SimpleRunner) runWorkers(ctx context.Context, lintCtx *linter.Context, linters []linter.Config) <-chan lintRes { func (r *Runner) runWorkers(ctx context.Context, lintCtx *linter.Context, linters []linter.Config) <-chan lintRes {
tasksCh := make(chan linter.Config, len(linters)) tasksCh := make(chan linter.Config, len(linters))
lintResultsCh := make(chan lintRes, len(linters)) lintResultsCh := make(chan lintRes, len(linters))
var wg sync.WaitGroup var wg sync.WaitGroup
@ -121,7 +162,7 @@ func (r *SimpleRunner) runWorkers(ctx context.Context, lintCtx *linter.Context,
go func(i int) { go func(i int) {
defer wg.Done() defer wg.Done()
name := fmt.Sprintf("worker.%d", i+1) name := fmt.Sprintf("worker.%d", i+1)
runWorker(ctx, lintCtx, tasksCh, lintResultsCh, name) r.runWorker(ctx, lintCtx, tasksCh, lintResultsCh, name)
workersFinishTimes[i] = time.Now() workersFinishTimes[i] = time.Now()
}(i) }(i)
} }
@ -136,23 +177,23 @@ func (r *SimpleRunner) runWorkers(ctx context.Context, lintCtx *linter.Context,
wg.Wait() wg.Wait()
close(lintResultsCh) close(lintResultsCh)
logWorkersStat(workersFinishTimes) r.logWorkersStat(workersFinishTimes)
}() }()
return lintResultsCh return lintResultsCh
} }
func (r SimpleRunner) processLintResults(ctx context.Context, inCh <-chan lintRes) <-chan lintRes { func (r Runner) processLintResults(ctx context.Context, inCh <-chan lintRes) <-chan lintRes {
outCh := make(chan lintRes, 64) outCh := make(chan lintRes, 64)
go func() { go func() {
sw := timeutils.NewStopwatch("processing") sw := timeutils.NewStopwatch("processing", r.Log)
defer close(outCh) defer close(outCh)
for res := range inCh { for res := range inCh {
if res.err != nil { if res.err != nil {
logutils.HiddenWarnf("Can't run linter %s: %s", res.linter.Linter.Name(), res.err) r.Log.Warnf("Can't run linter %s: %s", res.linter.Linter.Name(), res.err)
continue continue
} }
@ -195,7 +236,7 @@ func collectIssues(ctx context.Context, resCh <-chan lintRes) <-chan result.Issu
return retIssues return retIssues
} }
func (r SimpleRunner) Run(ctx context.Context, linters []linter.Config, lintCtx *linter.Context) <-chan result.Issue { func (r Runner) Run(ctx context.Context, linters []linter.Config, lintCtx *linter.Context) <-chan result.Issue {
lintResultsCh := r.runWorkers(ctx, lintCtx, linters) lintResultsCh := r.runWorkers(ctx, lintCtx, linters)
processedLintResultsCh := r.processLintResults(ctx, lintResultsCh) processedLintResultsCh := r.processLintResults(ctx, lintResultsCh)
if ctx.Err() != nil { if ctx.Err() != nil {
@ -205,14 +246,14 @@ func (r SimpleRunner) Run(ctx context.Context, linters []linter.Config, lintCtx
finishedLintersN++ finishedLintersN++
} }
logrus.Warnf("%d/%d linters finished: deadline exceeded: try increase it by passing --deadline option", r.Log.Errorf("%d/%d linters finished: deadline exceeded: try increase it by passing --deadline option",
finishedLintersN, len(linters)) finishedLintersN, len(linters))
} }
return collectIssues(ctx, processedLintResultsCh) return collectIssues(ctx, processedLintResultsCh)
} }
func (r *SimpleRunner) processIssues(ctx context.Context, issues []result.Issue, sw *timeutils.Stopwatch) []result.Issue { func (r *Runner) processIssues(ctx context.Context, issues []result.Issue, sw *timeutils.Stopwatch) []result.Issue {
for _, p := range r.Processors { for _, p := range r.Processors {
var newIssues []result.Issue var newIssues []result.Issue
var err error var err error
@ -221,7 +262,7 @@ func (r *SimpleRunner) processIssues(ctx context.Context, issues []result.Issue,
}) })
if err != nil { if err != nil {
logutils.HiddenWarnf("Can't process result by %s processor: %s", p.Name(), err) r.Log.Warnf("Can't process result by %s processor: %s", p.Name(), err)
} else { } else {
issues = newIssues issues = newIssues
} }

30
pkg/logutils/log.go Normal file
View file

@ -0,0 +1,30 @@
package logutils
type Log interface {
Fatalf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Infof(format string, args ...interface{})
Child(name string) Log
SetLevel(level LogLevel)
}
type LogLevel int
const (
// debug message, write to debug logs only by logutils.Debug
LogLevelDebug LogLevel = 0
// information messages, don't write too much messages,
// only useful ones: they are shown when running with -v
LogLevelInfo LogLevel = 1
// hidden errors: non critical errors: work can be continued, no need to fail whole program;
// tests will crash if any warning occured.
LogLevelWarn LogLevel = 2
// only not hidden from user errors: whole program failing, usually
// error logging happens in 1-2 places: in the "main" function.
LogLevelError LogLevel = 3
)

View file

@ -3,20 +3,8 @@ package logutils
import ( import (
"os" "os"
"strings" "strings"
"github.com/sirupsen/logrus"
) )
var isGolangCIRun = os.Getenv("GOLANGCI_COM_RUN") == "1"
func HiddenWarnf(format string, args ...interface{}) {
if isGolangCIRun {
logrus.Warnf(format, args...)
} else {
logrus.Infof(format, args...)
}
}
func getEnabledDebugs() map[string]bool { func getEnabledDebugs() map[string]bool {
ret := map[string]bool{} ret := map[string]bool{}
debugVar := os.Getenv("GL_DEBUG") debugVar := os.Getenv("GL_DEBUG")
@ -43,8 +31,9 @@ func Debug(tag string) DebugFunc {
} }
return func(format string, args ...interface{}) { return func(format string, args ...interface{}) {
newArgs := append([]interface{}{tag}, args...) logger := NewStderrLog(tag)
logrus.Debugf("%s: "+format, newArgs...) logger.SetLevel(LogLevelDebug)
logger.Debugf(format, args...)
} }
} }
@ -55,3 +44,9 @@ func IsDebugEnabled() bool {
func HaveDebugTag(tag string) bool { func HaveDebugTag(tag string) bool {
return enabledDebugs[tag] return enabledDebugs[tag]
} }
func SetupVerboseLog(log Log, isVerbose bool) {
if isVerbose {
log.SetLevel(LogLevelInfo)
}
}

106
pkg/logutils/stderr_log.go Normal file
View file

@ -0,0 +1,106 @@
package logutils
import (
"fmt"
"os"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/sirupsen/logrus" //nolint:depguard
)
var isTestRun = os.Getenv("GL_TEST_RUN") == "1"
type StderrLog struct {
name string
logger *logrus.Logger
level LogLevel
}
var _ Log = NewStderrLog("")
func NewStderrLog(name string) *StderrLog {
sl := &StderrLog{
name: name,
logger: logrus.New(),
level: LogLevelWarn,
}
// control log level in logutils, not in logrus
sl.logger.SetLevel(logrus.DebugLevel)
sl.logger.Formatter = &logrus.TextFormatter{
DisableTimestamp: true, // `INFO[0007] msg` -> `INFO msg`
}
return sl
}
func exitIfTest() {
if isTestRun {
os.Exit(exitcodes.WarningInTest)
}
}
func (sl StderrLog) prefix() string {
prefix := ""
if sl.name != "" {
prefix = fmt.Sprintf("[%s] ", sl.name)
}
return prefix
}
func (sl StderrLog) Fatalf(format string, args ...interface{}) {
sl.logger.Errorf("%s%s", sl.prefix(), fmt.Sprintf(format, args...))
os.Exit(exitcodes.Failure)
}
func (sl StderrLog) Errorf(format string, args ...interface{}) {
if sl.level > LogLevelError {
return
}
sl.logger.Errorf("%s%s", sl.prefix(), fmt.Sprintf(format, args...))
// don't call exitIfTest() because the idea is to
// crash on hidden errors (warnings); but Errorf MUST NOT be
// called on hidden errors, see log levels comments.
}
func (sl StderrLog) Warnf(format string, args ...interface{}) {
if sl.level > LogLevelWarn {
return
}
sl.logger.Warnf("%s%s", sl.prefix(), fmt.Sprintf(format, args...))
exitIfTest()
}
func (sl StderrLog) Infof(format string, args ...interface{}) {
if sl.level > LogLevelInfo {
return
}
sl.logger.Infof("%s%s", sl.prefix(), fmt.Sprintf(format, args...))
}
func (sl StderrLog) Debugf(format string, args ...interface{}) {
if sl.level > LogLevelDebug {
return
}
sl.logger.Debugf("%s%s", sl.prefix(), fmt.Sprintf(format, args...))
}
func (sl StderrLog) Child(name string) Log {
prefix := ""
if sl.name != "" {
prefix = sl.name + ":"
}
child := sl
child.name = prefix + name
return &child
}
func (sl *StderrLog) SetLevel(level LogLevel) {
sl.level = level
}

View file

@ -10,7 +10,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus" "github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
) )
type Resolver struct { type Resolver struct {
@ -18,9 +19,12 @@ type Resolver struct {
buildTags []string buildTags []string
skippedDirs []string skippedDirs []string
log logutils.Log
wd string // working directory
} }
func NewResolver(buildTags, excludeDirs []string) (*Resolver, error) { func NewResolver(buildTags, excludeDirs []string, log logutils.Log) (*Resolver, error) {
excludeDirsMap := map[string]*regexp.Regexp{} excludeDirsMap := map[string]*regexp.Regexp{}
for _, dir := range excludeDirs { for _, dir := range excludeDirs {
re, err := regexp.Compile(dir) re, err := regexp.Compile(dir)
@ -31,9 +35,16 @@ func NewResolver(buildTags, excludeDirs []string) (*Resolver, error) {
excludeDirsMap[dir] = re excludeDirsMap[dir] = re
} }
wd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("can't get working dir: %s", err)
}
return &Resolver{ return &Resolver{
excludeDirs: excludeDirsMap, excludeDirs: excludeDirsMap,
buildTags: buildTags, buildTags: buildTags,
log: log,
wd: wd,
}, nil }, nil
} }
@ -80,6 +91,15 @@ func (r *Resolver) resolveRecursively(root string, prog *Program) error {
subdir := filepath.Join(root, fi.Name()) subdir := filepath.Join(root, fi.Name())
// Normalize each subdir because working directory can be one of these subdirs:
// working dir = /app/subdir, resolve root is ../, without this normalization
// path of subdir will be "../subdir" but it must be ".".
// Normalize path before checking is ignored dir.
subdir, err := r.normalizePath(subdir)
if err != nil {
return err
}
if r.isIgnoredDir(subdir) { if r.isIgnoredDir(subdir) {
r.skippedDirs = append(r.skippedDirs, subdir) r.skippedDirs = append(r.skippedDirs, subdir)
continue continue
@ -128,7 +148,7 @@ func (r Resolver) addFakePackage(filePath string, prog *Program) {
func (r Resolver) Resolve(paths ...string) (prog *Program, err error) { func (r Resolver) Resolve(paths ...string) (prog *Program, err error) {
startedAt := time.Now() startedAt := time.Now()
defer func() { defer func() {
logrus.Infof("Paths resolving took %s: %s", time.Since(startedAt), prog) r.log.Infof("Paths resolving took %s: %s", time.Since(startedAt), prog)
}() }()
if len(paths) == 0 { if len(paths) == 0 {
@ -141,25 +161,24 @@ func (r Resolver) Resolve(paths ...string) (prog *Program, err error) {
bctx: bctx, bctx: bctx,
} }
root, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("can't get working dir: %s", err)
}
for _, path := range paths { for _, path := range paths {
if err := r.resolvePath(path, prog, root); err != nil { if err := r.resolvePath(path, prog); err != nil {
return nil, err return nil, err
} }
} }
if len(r.skippedDirs) != 0 { if len(r.skippedDirs) != 0 {
logrus.Infof("Skipped dirs: %s", r.skippedDirs) r.log.Infof("Skipped dirs: %s", r.skippedDirs)
} }
return prog, nil return prog, nil
} }
func (r *Resolver) resolvePath(path string, prog *Program, root string) error { func (r *Resolver) normalizePath(path string) (string, error) {
return fsutils.ShortestRelPath(path, r.wd)
}
func (r *Resolver) resolvePath(path string, prog *Program) error {
needRecursive := strings.HasSuffix(path, "/...") needRecursive := strings.HasSuffix(path, "/...")
if needRecursive { if needRecursive {
path = filepath.Dir(path) path = filepath.Dir(path)
@ -171,14 +190,9 @@ func (r *Resolver) resolvePath(path string, prog *Program, root string) error {
} }
path = evalPath path = evalPath
if filepath.IsAbs(path) { path, err = r.normalizePath(path)
var relPath string if err != nil {
relPath, err = filepath.Rel(root, path) return err
if err != nil {
return fmt.Errorf("can't get relative path for path %s and root %s: %s",
path, root, err)
}
path = relPath
} }
if needRecursive { if needRecursive {

View file

@ -8,6 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/packages" "github.com/golangci/golangci-lint/pkg/packages"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -57,7 +58,7 @@ func prepareFS(t *testing.T, paths ...string) *fsPreparer {
} }
func newTestResolver(t *testing.T, excludeDirs []string) *packages.Resolver { func newTestResolver(t *testing.T, excludeDirs []string) *packages.Resolver {
r, err := packages.NewResolver(nil, excludeDirs) r, err := packages.NewResolver(nil, excludeDirs, logutils.NewStderrLog(""))
assert.NoError(t, err) assert.NoError(t, err)
return r return r

View file

@ -7,17 +7,19 @@ import (
"text/tabwriter" "text/tabwriter"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
) )
type Tab struct { type Tab struct {
printLinterName bool printLinterName bool
log logutils.Log
} }
func NewTab(printLinterName bool) *Tab { func NewTab(printLinterName bool, log logutils.Log) *Tab {
return &Tab{ return &Tab{
printLinterName: printLinterName, printLinterName: printLinterName,
log: log,
} }
} }
@ -36,14 +38,14 @@ func (p *Tab) Print(ctx context.Context, issues <-chan result.Issue) (bool, erro
} }
if issuesN != 0 { if issuesN != 0 {
logrus.Infof("Found %d issues", issuesN) p.log.Infof("Found %d issues", issuesN)
} else if ctx.Err() == nil { // don't print "congrats" if timeouted } else if ctx.Err() == nil { // don't print "congrats" if timeouted
outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.") outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.")
fmt.Fprintln(StdOut, outStr) fmt.Fprintln(StdOut, outStr)
} }
if err := w.Flush(); err != nil { if err := w.Flush(); err != nil {
logrus.Warnf("Can't flush tab writer: %s", err) p.log.Warnf("Can't flush tab writer: %s", err)
} }
return issuesN != 0, nil return issuesN != 0, nil

View file

@ -9,8 +9,8 @@ import (
"time" "time"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
) )
type linesCache [][]byte type linesCache [][]byte
@ -22,14 +22,16 @@ type Text struct {
printLinterName bool printLinterName bool
cache filesCache cache filesCache
log logutils.Log
} }
func NewText(printIssuedLine, useColors, printLinterName bool) *Text { func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) *Text {
return &Text{ return &Text{
printIssuedLine: printIssuedLine, printIssuedLine: printIssuedLine,
useColors: useColors, useColors: useColors,
printLinterName: printLinterName, printLinterName: printLinterName,
cache: filesCache{}, cache: filesCache{},
log: log,
} }
} }
@ -62,7 +64,7 @@ func (p *Text) getFileLinesForIssue(i *result.Issue) (linesCache, error) {
func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) { func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) {
var issuedLineExtractingDuration time.Duration var issuedLineExtractingDuration time.Duration
defer func() { defer func() {
logrus.Infof("Extracting issued lines took %s", issuedLineExtractingDuration) p.log.Infof("Extracting issued lines took %s", issuedLineExtractingDuration)
}() }()
issuesN := 0 issuesN := 0
@ -88,7 +90,7 @@ func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) (bool, err
} }
if issuesN != 0 { if issuesN != 0 {
logrus.Infof("Found %d issues", issuesN) p.log.Infof("Found %d issues", issuesN)
} else if ctx.Err() == nil { // don't print "congrats" if timeouted } else if ctx.Err() == nil { // don't print "congrats" if timeouted
outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.") outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.")
fmt.Fprintln(StdOut, outStr) fmt.Fprintln(StdOut, outStr)
@ -119,7 +121,7 @@ func (p Text) printIssuedLines(i *result.Issue, lines linesCache) {
zeroIndexedLine := line - 1 zeroIndexedLine := line - 1
if zeroIndexedLine >= len(lines) { if zeroIndexedLine >= len(lines) {
logrus.Warnf("No line %d in file %s", line, i.FilePath()) p.log.Warnf("No line %d in file %s", line, i.FilePath())
break break
} }

View file

@ -1,21 +1,23 @@
package processors package processors
import ( import (
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
) )
type MaxFromLinter struct { type MaxFromLinter struct {
lc linterToCountMap lc linterToCountMap
limit int limit int
log logutils.Log
} }
var _ Processor = &MaxFromLinter{} var _ Processor = &MaxFromLinter{}
func NewMaxFromLinter(limit int) *MaxFromLinter { func NewMaxFromLinter(limit int, log logutils.Log) *MaxFromLinter {
return &MaxFromLinter{ return &MaxFromLinter{
lc: linterToCountMap{}, lc: linterToCountMap{},
limit: limit, limit: limit,
log: log,
} }
} }
@ -37,7 +39,7 @@ func (p *MaxFromLinter) Process(issues []result.Issue) ([]result.Issue, error) {
func (p MaxFromLinter) Finish() { func (p MaxFromLinter) Finish() {
walkStringToIntMapSortedByValue(p.lc, func(linter string, count int) { walkStringToIntMapSortedByValue(p.lc, func(linter string, count int) {
if count > p.limit { if count > p.limit {
logrus.Infof("%d/%d issues from linter %s were hidden, use --max-issues-per-linter", p.log.Infof("%d/%d issues from linter %s were hidden, use --max-issues-per-linter",
count-p.limit, count, linter) count-p.limit, count, linter)
} }
}) })

View file

@ -2,10 +2,12 @@ package processors
import ( import (
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/logutils"
) )
func TestMaxFromLinter(t *testing.T) { func TestMaxFromLinter(t *testing.T) {
p := NewMaxFromLinter(1) p := NewMaxFromLinter(1, logutils.NewStderrLog(""))
gosimple := newFromLinterIssue("gosimple") gosimple := newFromLinterIssue("gosimple")
gofmt := newFromLinterIssue("gofmt") gofmt := newFromLinterIssue("gofmt")
processAssertSame(t, p, gosimple) // ok processAssertSame(t, p, gosimple) // ok

View file

@ -3,8 +3,8 @@ package processors
import ( import (
"sort" "sort"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
) )
type textToCountMap map[string]int type textToCountMap map[string]int
@ -12,14 +12,16 @@ type textToCountMap map[string]int
type MaxSameIssues struct { type MaxSameIssues struct {
tc textToCountMap tc textToCountMap
limit int limit int
log logutils.Log
} }
var _ Processor = &MaxSameIssues{} var _ Processor = &MaxSameIssues{}
func NewMaxSameIssues(limit int) *MaxSameIssues { func NewMaxSameIssues(limit int, log logutils.Log) *MaxSameIssues {
return &MaxSameIssues{ return &MaxSameIssues{
tc: textToCountMap{}, tc: textToCountMap{},
limit: limit, limit: limit,
log: log,
} }
} }
@ -41,7 +43,7 @@ func (p *MaxSameIssues) Process(issues []result.Issue) ([]result.Issue, error) {
func (p MaxSameIssues) Finish() { func (p MaxSameIssues) Finish() {
walkStringToIntMapSortedByValue(p.tc, func(text string, count int) { walkStringToIntMapSortedByValue(p.tc, func(text string, count int) {
if count > p.limit { if count > p.limit {
logrus.Infof("%d/%d issues with text %q were hidden, use --max-same-issues", p.log.Infof("%d/%d issues with text %q were hidden, use --max-same-issues",
count-p.limit, count, text) count-p.limit, count, text)
} }
}) })

View file

@ -3,11 +3,12 @@ package processors
import ( import (
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
func TestMaxSameIssues(t *testing.T) { func TestMaxSameIssues(t *testing.T) {
p := NewMaxSameIssues(1) p := NewMaxSameIssues(1, logutils.NewStderrLog(""))
i1 := result.Issue{ i1 := result.Issue{
Text: "1", Text: "1",
} }

View file

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/lint/astcache" "github.com/golangci/golangci-lint/pkg/lint/astcache"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -21,7 +22,7 @@ func newNolintFileIssue(line int, fromLinter string) result.Issue {
} }
func TestNolint(t *testing.T) { func TestNolint(t *testing.T) {
p := NewNolint(astcache.NewCache()) p := NewNolint(astcache.NewCache(logutils.NewStderrLog("")))
// test inline comments // test inline comments
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt")) processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt"))

View file

@ -7,22 +7,24 @@ import (
"sync" "sync"
"time" "time"
"github.com/sirupsen/logrus" "github.com/golangci/golangci-lint/pkg/logutils"
) )
type Stopwatch struct { type Stopwatch struct {
name string name string
startedAt time.Time startedAt time.Time
stages map[string]time.Duration stages map[string]time.Duration
log logutils.Log
sync.Mutex sync.Mutex
} }
func NewStopwatch(name string) *Stopwatch { func NewStopwatch(name string, log logutils.Log) *Stopwatch {
return &Stopwatch{ return &Stopwatch{
name: name, name: name,
startedAt: time.Now(), startedAt: time.Now(),
stages: map[string]time.Duration{}, stages: map[string]time.Duration{},
log: log,
} }
} }
@ -53,11 +55,11 @@ func (s *Stopwatch) sprintStages() string {
func (s *Stopwatch) Print() { func (s *Stopwatch) Print() {
p := fmt.Sprintf("%s took %s", s.name, time.Since(s.startedAt)) p := fmt.Sprintf("%s took %s", s.name, time.Since(s.startedAt))
if len(s.stages) == 0 { if len(s.stages) == 0 {
logrus.Info(p) s.log.Infof("%s", p)
return return
} }
logrus.Infof("%s with %s", p, s.sprintStages()) s.log.Infof("%s with %s", p, s.sprintStages())
} }
func (s *Stopwatch) PrintStages() { func (s *Stopwatch) PrintStages() {
@ -65,7 +67,7 @@ func (s *Stopwatch) PrintStages() {
for _, s := range s.stages { for _, s := range s.stages {
stagesDuration += s stagesDuration += s
} }
logrus.Infof("%s took %s with %s", s.name, stagesDuration, s.sprintStages()) s.log.Infof("%s took %s with %s", s.name, stagesDuration, s.sprintStages())
} }
func (s *Stopwatch) TrackStage(name string, f func()) { func (s *Stopwatch) TrackStage(name string, f func()) {

View file

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/sirupsen/logrus" "github.com/golangci/golangci-lint/pkg/logutils"
) )
func Track(from time.Time, format string, args ...interface{}) { func Track(from time.Time, log logutils.Log, format string, args ...interface{}) {
logrus.Infof("%s took %s", fmt.Sprintf(format, args...), time.Since(from)) log.Infof("%s took %s", fmt.Sprintf(format, args...), time.Since(from))
} }

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"go/build" "go/build"
"log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -15,7 +16,6 @@ import (
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
gops "github.com/mitchellh/go-ps" gops "github.com/mitchellh/go-ps"
"github.com/shirou/gopsutil/process" "github.com/shirou/gopsutil/process"
"github.com/sirupsen/logrus"
) )
func chdir(b *testing.B, dir string) { func chdir(b *testing.B, dir string) {
@ -90,7 +90,7 @@ func printCommand(cmd string, args ...string) {
quotedArgs = append(quotedArgs, strconv.Quote(a)) quotedArgs = append(quotedArgs, strconv.Quote(a))
} }
logrus.Warnf("%s %s", cmd, strings.Join(quotedArgs, " ")) log.Printf("%s %s", cmd, strings.Join(quotedArgs, " "))
} }
func runGometalinter(b *testing.B) { func runGometalinter(b *testing.B) {
@ -215,7 +215,7 @@ func compare(b *testing.B, gometalinterRun, golangciLintRun func(*testing.B), re
if mode != "" { if mode != "" {
mode = " " + mode mode = " " + mode
} }
logrus.Warnf("%s (%d kLoC): golangci-lint%s: time: %s, %.1f times faster; memory: %dMB, %.1f times less", log.Printf("%s (%d kLoC): golangci-lint%s: time: %s, %.1f times faster; memory: %dMB, %.1f times less",
repoName, kLOC, mode, repoName, kLOC, mode,
golangciLintRes.duration, gometalinterRes.duration.Seconds()/golangciLintRes.duration.Seconds(), golangciLintRes.duration, gometalinterRes.duration.Seconds()/golangciLintRes.duration.Seconds(),
golangciLintRes.peakMemMB, float64(gometalinterRes.peakMemMB)/float64(golangciLintRes.peakMemMB), golangciLintRes.peakMemMB, float64(gometalinterRes.peakMemMB)/float64(golangciLintRes.peakMemMB),

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -94,10 +95,10 @@ func TestGolintConsumesXTestFiles(t *testing.T) {
const expIssue = "if block ends with a return statement, so drop this else and outdent its block" const expIssue = "if block ends with a return statement, so drop this else and outdent its block"
out, ec := runGolangciLint(t, "--no-config", "--disable-all", "-Egolint", dir) out, ec := runGolangciLint(t, "--no-config", "--disable-all", "-Egolint", dir)
assert.Equal(t, 1, ec) assert.Equal(t, exitcodes.IssuesFound, ec)
assert.Contains(t, out, expIssue) assert.Contains(t, out, expIssue)
out, ec = runGolangciLint(t, "--no-config", "--disable-all", "-Egolint", filepath.Join(dir, "p_test.go")) out, ec = runGolangciLint(t, "--no-config", "--disable-all", "-Egolint", filepath.Join(dir, "p_test.go"))
assert.Equal(t, 1, ec) assert.Equal(t, exitcodes.IssuesFound, ec)
assert.Contains(t, out, expIssue) assert.Contains(t, out, expIssue)
} }

View file

@ -13,6 +13,7 @@ import (
"syscall" "syscall"
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb" "github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -28,7 +29,7 @@ func installBinary(t assert.TestingT) {
} }
func checkNoIssuesRun(t *testing.T, out string, exitCode int) { func checkNoIssuesRun(t *testing.T, out string, exitCode int) {
assert.Equal(t, 0, exitCode) assert.Equal(t, exitcodes.Success, exitCode)
assert.Equal(t, "Congrats! No issues were found.\n", out) assert.Equal(t, "Congrats! No issues were found.\n", out)
} }
@ -47,9 +48,20 @@ func TestSymlinkLoop(t *testing.T) {
checkNoIssuesRun(t, out, exitCode) checkNoIssuesRun(t, out, exitCode)
} }
func TestRunOnAbsPath(t *testing.T) {
absPath, err := filepath.Abs(filepath.Join(testdataDir, ".."))
assert.NoError(t, err)
out, exitCode := runGolangciLint(t, "--no-config", "--fast", absPath)
checkNoIssuesRun(t, out, exitCode)
out, exitCode = runGolangciLint(t, "--no-config", absPath)
checkNoIssuesRun(t, out, exitCode)
}
func TestDeadline(t *testing.T) { func TestDeadline(t *testing.T) {
out, exitCode := runGolangciLint(t, "--deadline=1ms", "../...") out, exitCode := runGolangciLint(t, "--deadline=1ms", filepath.Join("..", "..."))
assert.Equal(t, 4, exitCode) assert.Equal(t, exitcodes.Timeout, exitCode)
assert.Contains(t, out, "deadline exceeded: try increase it by passing --deadline option") assert.Contains(t, out, "deadline exceeded: try increase it by passing --deadline option")
assert.NotContains(t, out, "Congrats! No issues were found.") assert.NotContains(t, out, "Congrats! No issues were found.")
} }
@ -79,7 +91,7 @@ func runGolangciLint(t *testing.T, args ...string) (string, int) {
func runGolangciLintWithYamlConfig(t *testing.T, cfg string, args ...string) string { func runGolangciLintWithYamlConfig(t *testing.T, cfg string, args ...string) string {
out, ec := runGolangciLintWithYamlConfigWithCode(t, cfg, args...) out, ec := runGolangciLintWithYamlConfigWithCode(t, cfg, args...)
assert.Equal(t, 0, ec) assert.Equal(t, exitcodes.Success, ec)
return out return out
} }
@ -107,12 +119,12 @@ func runGolangciLintWithYamlConfigWithCode(t *testing.T, cfg string, args ...str
func TestTestsAreLintedByDefault(t *testing.T) { func TestTestsAreLintedByDefault(t *testing.T) {
out, exitCode := runGolangciLint(t, "./testdata/withtests") out, exitCode := runGolangciLint(t, "./testdata/withtests")
assert.Equal(t, 0, exitCode, out) assert.Equal(t, exitcodes.Success, exitCode, out)
} }
func TestConfigFileIsDetected(t *testing.T) { func TestConfigFileIsDetected(t *testing.T) {
checkGotConfig := func(out string, exitCode int) { checkGotConfig := func(out string, exitCode int) {
assert.Equal(t, 0, exitCode, out) assert.Equal(t, exitcodes.Success, exitCode, out)
assert.Equal(t, "test\n", out) // test config contains InternalTest: true, it triggers such output assert.Equal(t, "test\n", out) // test config contains InternalTest: true, it triggers such output
} }
@ -208,7 +220,7 @@ func TestEnableAllFastAndEnableCanCoexist(t *testing.T) {
checkNoIssuesRun(t, out, exitCode) checkNoIssuesRun(t, out, exitCode)
_, exitCode = runGolangciLint(t, "--enable-all", "--enable=typecheck") _, exitCode = runGolangciLint(t, "--enable-all", "--enable=typecheck")
assert.Equal(t, 3, exitCode) assert.Equal(t, exitcodes.Failure, exitCode)
} }
@ -327,7 +339,7 @@ func TestEnabledLinters(t *testing.T) {
func TestEnabledPresetsAreNotDuplicated(t *testing.T) { func TestEnabledPresetsAreNotDuplicated(t *testing.T) {
out, ec := runGolangciLint(t, "--no-config", "-v", "-p", "style,bugs") out, ec := runGolangciLint(t, "--no-config", "-v", "-p", "style,bugs")
assert.Equal(t, 0, ec) assert.Equal(t, exitcodes.Success, ec)
assert.Contains(t, out, "Active presets: [bugs style]") assert.Contains(t, out, "Active presets: [bugs style]")
} }
@ -371,7 +383,7 @@ func TestDisallowedOptionsInConfig(t *testing.T) {
for _, c := range cases { for _, c := range cases {
// Run with disallowed option set only in config // Run with disallowed option set only in config
_, ec := runGolangciLintWithYamlConfigWithCode(t, c.cfg) _, ec := runGolangciLintWithYamlConfigWithCode(t, c.cfg)
assert.Equal(t, 1, ec) assert.Equal(t, exitcodes.Failure, ec)
if c.option == "" { if c.option == "" {
continue continue
@ -381,10 +393,10 @@ func TestDisallowedOptionsInConfig(t *testing.T) {
// Run with disallowed option set only in command-line // Run with disallowed option set only in command-line
_, ec = runGolangciLint(t, args...) _, ec = runGolangciLint(t, args...)
assert.Equal(t, 0, ec) assert.Equal(t, exitcodes.Success, ec)
// Run with disallowed option set both in command-line and in config // Run with disallowed option set both in command-line and in config
_, ec = runGolangciLintWithYamlConfigWithCode(t, c.cfg, args...) _, ec = runGolangciLintWithYamlConfigWithCode(t, c.cfg, args...)
assert.Equal(t, 1, ec) assert.Equal(t, exitcodes.Failure, ec)
} }
} }