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 }