github.com/ferretdb/golangci-lint@v1.10.1/pkg/commands/run.go (about)

     1  package commands
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"runtime"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/fatih/color"
    14  	"github.com/golangci/golangci-lint/pkg/config"
    15  	"github.com/golangci/golangci-lint/pkg/exitcodes"
    16  	"github.com/golangci/golangci-lint/pkg/lint"
    17  	"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
    18  	"github.com/golangci/golangci-lint/pkg/logutils"
    19  	"github.com/golangci/golangci-lint/pkg/printers"
    20  	"github.com/golangci/golangci-lint/pkg/result"
    21  	"github.com/pkg/errors"
    22  	"github.com/spf13/cobra"
    23  	"github.com/spf13/pflag"
    24  )
    25  
    26  func getDefaultExcludeHelp() string {
    27  	parts := []string{"Use or not use default excludes:"}
    28  	for _, ep := range config.DefaultExcludePatterns {
    29  		parts = append(parts, fmt.Sprintf("  # %s: %s", ep.Linter, ep.Why))
    30  		parts = append(parts, fmt.Sprintf("  - %s", color.YellowString(ep.Pattern)))
    31  		parts = append(parts, "")
    32  	}
    33  	return strings.Join(parts, "\n")
    34  }
    35  
    36  const welcomeMessage = "Run this tool in cloud on every github pull " +
    37  	"request in https://golangci.com for free (public repos)"
    38  
    39  func wh(text string) string {
    40  	return color.GreenString(text)
    41  }
    42  
    43  func initFlagSet(fs *pflag.FlagSet, cfg *config.Config) {
    44  	hideFlag := func(name string) {
    45  		if err := fs.MarkHidden(name); err != nil {
    46  			panic(err)
    47  		}
    48  	}
    49  
    50  	// Output config
    51  	oc := &cfg.Output
    52  	fs.StringVar(&oc.Format, "out-format",
    53  		config.OutFormatColoredLineNumber,
    54  		wh(fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|"))))
    55  	fs.BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, wh("Print lines of code with issue"))
    56  	fs.BoolVar(&oc.PrintLinterName, "print-linter-name", true, wh("Print linter name in issue line"))
    57  	fs.BoolVar(&oc.PrintWelcomeMessage, "print-welcome", false, wh("Print welcome message"))
    58  	hideFlag("print-welcome") // no longer used
    59  
    60  	// Run config
    61  	rc := &cfg.Run
    62  	fs.IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code",
    63  		exitcodes.IssuesFound, wh("Exit code when issues were found"))
    64  	fs.StringSliceVar(&rc.BuildTags, "build-tags", nil, wh("Build tags"))
    65  	fs.DurationVar(&rc.Deadline, "deadline", time.Minute, wh("Deadline for total work"))
    66  	fs.BoolVar(&rc.AnalyzeTests, "tests", true, wh("Analyze tests (*_test.go)"))
    67  	fs.BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false,
    68  		wh("Print avg and max memory usage of golangci-lint and total time"))
    69  	fs.StringVarP(&rc.Config, "config", "c", "", wh("Read config from file path `PATH`"))
    70  	fs.BoolVar(&rc.NoConfig, "no-config", false, wh("Don't read config"))
    71  	fs.StringSliceVar(&rc.SkipDirs, "skip-dirs", nil, wh("Regexps of directories to skip"))
    72  	fs.StringSliceVar(&rc.SkipFiles, "skip-files", nil, wh("Regexps of files to skip"))
    73  
    74  	// Linters settings config
    75  	lsc := &cfg.LintersSettings
    76  
    77  	// Hide all linters settings flags: they were initially visible,
    78  	// but when number of linters started to grow it became ovious that
    79  	// we can't fill 90% of flags by linters settings: common flags became hard to find.
    80  	// New linters settings should be done only through config file.
    81  	fs.BoolVar(&lsc.Errcheck.CheckTypeAssertions, "errcheck.check-type-assertions",
    82  		false, "Errcheck: check for ignored type assertion results")
    83  	hideFlag("errcheck.check-type-assertions")
    84  
    85  	fs.BoolVar(&lsc.Errcheck.CheckAssignToBlank, "errcheck.check-blank", false,
    86  		"Errcheck: check for errors assigned to blank identifier: _ = errFunc()")
    87  	hideFlag("errcheck.check-blank")
    88  
    89  	fs.BoolVar(&lsc.Govet.CheckShadowing, "govet.check-shadowing", false,
    90  		"Govet: check for shadowed variables")
    91  	hideFlag("govet.check-shadowing")
    92  
    93  	fs.Float64Var(&lsc.Golint.MinConfidence, "golint.min-confidence", 0.8,
    94  		"Golint: minimum confidence of a problem to print it")
    95  	hideFlag("golint.min-confidence")
    96  
    97  	fs.BoolVar(&lsc.Gofmt.Simplify, "gofmt.simplify", true, "Gofmt: simplify code")
    98  	hideFlag("gofmt.simplify")
    99  
   100  	fs.IntVar(&lsc.Gocyclo.MinComplexity, "gocyclo.min-complexity",
   101  		30, "Minimal complexity of function to report it")
   102  	hideFlag("gocyclo.min-complexity")
   103  
   104  	fs.BoolVar(&lsc.Maligned.SuggestNewOrder, "maligned.suggest-new", false,
   105  		"Maligned: print suggested more optimal struct fields ordering")
   106  	hideFlag("maligned.suggest-new")
   107  
   108  	fs.IntVar(&lsc.Dupl.Threshold, "dupl.threshold",
   109  		150, "Dupl: Minimal threshold to detect copy-paste")
   110  	hideFlag("dupl.threshold")
   111  
   112  	fs.IntVar(&lsc.Goconst.MinStringLen, "goconst.min-len",
   113  		3, "Goconst: minimum constant string length")
   114  	hideFlag("goconst.min-len")
   115  	fs.IntVar(&lsc.Goconst.MinOccurrencesCount, "goconst.min-occurrences",
   116  		3, "Goconst: minimum occurrences of constant string count to trigger issue")
   117  	hideFlag("goconst.min-occurrences")
   118  
   119  	// (@dixonwille) These flag is only used for testing purposes.
   120  	fs.StringSliceVar(&lsc.Depguard.Packages, "depguard.packages", nil,
   121  		"Depguard: packages to add to the list")
   122  	hideFlag("depguard.packages")
   123  
   124  	fs.BoolVar(&lsc.Depguard.IncludeGoRoot, "depguard.include-go-root", false,
   125  		"Depguard: check list against standard lib")
   126  	hideFlag("depguard.include-go-root")
   127  
   128  	fs.IntVar(&lsc.Lll.TabWidth, "lll.tab-width", 1,
   129  		"Lll: tab width in spaces")
   130  	hideFlag("lll.tab-width")
   131  
   132  	// Linters config
   133  	lc := &cfg.Linters
   134  	fs.StringSliceVarP(&lc.Enable, "enable", "E", nil, wh("Enable specific linter"))
   135  	fs.StringSliceVarP(&lc.Disable, "disable", "D", nil, wh("Disable specific linter"))
   136  	fs.BoolVar(&lc.EnableAll, "enable-all", false, wh("Enable all linters"))
   137  	fs.BoolVar(&lc.DisableAll, "disable-all", false, wh("Disable all linters"))
   138  	fs.StringSliceVarP(&lc.Presets, "presets", "p", nil,
   139  		wh(fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint linters' to see "+
   140  			"them. This option implies option --disable-all", strings.Join(lintersdb.AllPresets(), "|"))))
   141  	fs.BoolVar(&lc.Fast, "fast", false, wh("Run only fast linters from enabled linters set"))
   142  
   143  	// Issues config
   144  	ic := &cfg.Issues
   145  	fs.StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", nil, wh("Exclude issue by regexp"))
   146  	fs.BoolVar(&ic.UseDefaultExcludes, "exclude-use-default", true, getDefaultExcludeHelp())
   147  
   148  	fs.IntVar(&ic.MaxIssuesPerLinter, "max-issues-per-linter", 50,
   149  		wh("Maximum issues count per one linter. Set to 0 to disable"))
   150  	fs.IntVar(&ic.MaxSameIssues, "max-same-issues", 3,
   151  		wh("Maximum count of issues with the same text. Set to 0 to disable"))
   152  
   153  	fs.BoolVarP(&ic.Diff, "new", "n", false,
   154  		wh("Show only new issues: if there are unstaged changes or untracked files, only those changes "+
   155  			"are analyzed, else only changes in HEAD~ are analyzed.\nIt's a super-useful option for integration "+
   156  			"of golangci-lint into existing large codebase.\nIt's not practical to fix all existing issues at "+
   157  			"the moment of integration: much better to not allow issues in new code.\nFor CI setups, prefer "+
   158  			"--new-from-rev=HEAD~, as --new can skip linting the current patch if any scripts generate "+
   159  			"unstaged files before golangci-lint runs."))
   160  	fs.StringVar(&ic.DiffFromRevision, "new-from-rev", "",
   161  		wh("Show only new issues created after git revision `REV`"))
   162  	fs.StringVar(&ic.DiffPatchFilePath, "new-from-patch", "",
   163  		wh("Show only new issues created in git patch with file path `PATH`"))
   164  
   165  }
   166  
   167  func (e *Executor) initRunConfiguration(cmd *cobra.Command) {
   168  	fs := cmd.Flags()
   169  	fs.SortFlags = false // sort them as they are defined here
   170  	initFlagSet(fs, e.cfg)
   171  
   172  	// init e.cfg by values from config: flags parse will see these values
   173  	// like the default ones. It will overwrite them only if the same option
   174  	// is found in command-line: it's ok, command-line has higher priority.
   175  
   176  	r := config.NewFileReader(e.cfg, e.log.Child("config_reader"), func(fs *pflag.FlagSet, cfg *config.Config) {
   177  		// Don't do `fs.AddFlagSet(cmd.Flags())` because it shares flags representations:
   178  		// `changed` variable inside string slice vars will be shared.
   179  		// Use another config variable here, not e.cfg, to not
   180  		// affect main parsing by this parsing of only config option.
   181  		initFlagSet(fs, cfg)
   182  
   183  		// Parse max options, even force version option: don't want
   184  		// to get access to Executor here: it's error-prone to use
   185  		// cfg vs e.cfg.
   186  		initRootFlagSet(fs, cfg, true)
   187  	})
   188  	if err := r.Read(); err != nil {
   189  		e.log.Fatalf("Can't read config: %s", err)
   190  	}
   191  
   192  	// Slice options must be explicitly set for proper merging of config and command-line options.
   193  	fixSlicesFlags(fs)
   194  }
   195  
   196  func (e *Executor) initRun() {
   197  	var runCmd = &cobra.Command{
   198  		Use:   "run",
   199  		Short: welcomeMessage,
   200  		Run:   e.executeRun,
   201  	}
   202  	e.rootCmd.AddCommand(runCmd)
   203  
   204  	runCmd.SetOutput(logutils.StdOut) // use custom output to properly color it in Windows terminals
   205  
   206  	e.initRunConfiguration(runCmd)
   207  }
   208  
   209  func fixSlicesFlags(fs *pflag.FlagSet) {
   210  	// It's a dirty hack to set flag.Changed to true for every string slice flag.
   211  	// It's necessary to merge config and command-line slices: otherwise command-line
   212  	// flags will always overwrite ones from the config.
   213  	fs.VisitAll(func(f *pflag.Flag) {
   214  		if f.Value.Type() != "stringSlice" {
   215  			return
   216  		}
   217  
   218  		s, err := fs.GetStringSlice(f.Name)
   219  		if err != nil {
   220  			return
   221  		}
   222  
   223  		if s == nil { // assume that every string slice flag has nil as the default
   224  			return
   225  		}
   226  
   227  		// calling Set sets Changed to true: next Set calls will append, not overwrite
   228  		_ = f.Value.Set(strings.Join(s, ","))
   229  	})
   230  }
   231  
   232  func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan result.Issue, error) {
   233  	e.cfg.Run.Args = args
   234  
   235  	linters, err := lintersdb.GetEnabledLinters(e.cfg, e.log.Child("lintersdb"))
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  
   240  	for _, lc := range lintersdb.GetAllSupportedLinterConfigs() {
   241  		isEnabled := false
   242  		for _, linter := range linters {
   243  			if linter.Linter.Name() == lc.Linter.Name() {
   244  				isEnabled = true
   245  				break
   246  			}
   247  		}
   248  		e.reportData.AddLinter(lc.Linter.Name(), isEnabled, lc.EnabledByDefault)
   249  	}
   250  
   251  	lintCtx, err := lint.LoadContext(linters, e.cfg, e.log.Child("load"))
   252  	if err != nil {
   253  		return nil, errors.Wrap(err, "context loading failed")
   254  	}
   255  
   256  	runner, err := lint.NewRunner(lintCtx.ASTCache, e.cfg, e.log.Child("runner"))
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	return runner.Run(ctx, linters, lintCtx), nil
   262  }
   263  
   264  func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) {
   265  	savedStdout, savedStderr = os.Stdout, os.Stderr
   266  	devNull, err := os.Open(os.DevNull)
   267  	if err != nil {
   268  		e.log.Warnf("Can't open null device %q: %s", os.DevNull, err)
   269  		return
   270  	}
   271  
   272  	os.Stdout, os.Stderr = devNull, devNull
   273  	return
   274  }
   275  
   276  func (e *Executor) setExitCodeIfIssuesFound(issues <-chan result.Issue) <-chan result.Issue {
   277  	resCh := make(chan result.Issue, 1024)
   278  
   279  	go func() {
   280  		issuesFound := false
   281  		for i := range issues {
   282  			issuesFound = true
   283  			resCh <- i
   284  		}
   285  
   286  		if issuesFound {
   287  			e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
   288  		}
   289  
   290  		close(resCh)
   291  	}()
   292  
   293  	return resCh
   294  }
   295  
   296  func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
   297  	if !logutils.HaveDebugTag("linters_output") {
   298  		// Don't allow linters and loader to print anything
   299  		log.SetOutput(ioutil.Discard)
   300  		savedStdout, savedStderr := e.setOutputToDevNull()
   301  		defer func() {
   302  			os.Stdout, os.Stderr = savedStdout, savedStderr
   303  		}()
   304  	}
   305  
   306  	issues, err := e.runAnalysis(ctx, args)
   307  	if err != nil {
   308  		return err // XXX: don't loose type
   309  	}
   310  
   311  	p, err := e.createPrinter()
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	issues = e.setExitCodeIfIssuesFound(issues)
   317  
   318  	if err = p.Print(ctx, issues); err != nil {
   319  		return fmt.Errorf("can't print %d issues: %s", len(issues), err)
   320  	}
   321  
   322  	return nil
   323  }
   324  
   325  func (e *Executor) createPrinter() (printers.Printer, error) {
   326  	var p printers.Printer
   327  	format := e.cfg.Output.Format
   328  	switch format {
   329  	case config.OutFormatJSON:
   330  		p = printers.NewJSON(&e.reportData)
   331  	case config.OutFormatColoredLineNumber, config.OutFormatLineNumber:
   332  		p = printers.NewText(e.cfg.Output.PrintIssuedLine,
   333  			format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName,
   334  			e.log.Child("text_printer"))
   335  	case config.OutFormatTab:
   336  		p = printers.NewTab(e.cfg.Output.PrintLinterName, e.log.Child("tab_printer"))
   337  	case config.OutFormatCheckstyle:
   338  		p = printers.NewCheckstyle()
   339  	default:
   340  		return nil, fmt.Errorf("unknown output format %s", format)
   341  	}
   342  
   343  	return p, nil
   344  }
   345  
   346  func (e *Executor) executeRun(cmd *cobra.Command, args []string) {
   347  	needTrackResources := e.cfg.Run.IsVerbose || e.cfg.Run.PrintResourcesUsage
   348  	trackResourcesEndCh := make(chan struct{})
   349  	defer func() { // XXX: this defer must be before ctx.cancel defer
   350  		if needTrackResources { // wait until resource tracking finished to print properly
   351  			<-trackResourcesEndCh
   352  		}
   353  	}()
   354  
   355  	ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Deadline)
   356  	defer cancel()
   357  
   358  	if needTrackResources {
   359  		go watchResources(ctx, trackResourcesEndCh, e.log)
   360  	}
   361  
   362  	if err := e.runAndPrint(ctx, args); err != nil {
   363  		e.log.Errorf("Running error: %s", err)
   364  		if e.exitCode == exitcodes.Success {
   365  			if exitErr, ok := errors.Cause(err).(*exitcodes.ExitError); ok {
   366  				e.exitCode = exitErr.Code
   367  			} else {
   368  				e.exitCode = exitcodes.Failure
   369  			}
   370  		}
   371  	}
   372  
   373  	if e.exitCode == exitcodes.Success && ctx.Err() != nil {
   374  		e.exitCode = exitcodes.Timeout
   375  	}
   376  }
   377  
   378  func watchResources(ctx context.Context, done chan struct{}, log logutils.Log) {
   379  	startedAt := time.Now()
   380  
   381  	rssValues := []uint64{}
   382  	ticker := time.NewTicker(100 * time.Millisecond)
   383  	defer ticker.Stop()
   384  
   385  	for {
   386  		var m runtime.MemStats
   387  		runtime.ReadMemStats(&m)
   388  
   389  		rssValues = append(rssValues, m.Sys)
   390  
   391  		stop := false
   392  		select {
   393  		case <-ctx.Done():
   394  			stop = true
   395  		case <-ticker.C: // track every second
   396  		}
   397  
   398  		if stop {
   399  			break
   400  		}
   401  	}
   402  
   403  	var avg, max uint64
   404  	for _, v := range rssValues {
   405  		avg += v
   406  		if v > max {
   407  			max = v
   408  		}
   409  	}
   410  	avg /= uint64(len(rssValues))
   411  
   412  	const MB = 1024 * 1024
   413  	maxMB := float64(max) / MB
   414  	log.Infof("Memory: %d samples, avg is %.1fMB, max is %.1fMB",
   415  		len(rssValues), float64(avg)/MB, maxMB)
   416  	log.Infof("Execution took %s", time.Since(startedAt))
   417  	close(done)
   418  }