-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathrun.go
399 lines (331 loc) · 12.3 KB
/
run.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
package commands
import (
"context"
"errors"
"fmt"
"go/build"
"go/token"
"log"
"os"
"runtime"
"strings"
"time"
"github.com/golangci/go-tools/ssa"
"github.com/golangci/go-tools/ssa/ssautil"
"github.com/golangci/golangci-lint/pkg"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/printers"
"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/pflag"
"github.com/spf13/viper"
"golang.org/x/tools/go/loader"
)
const exitCodeIfFailure = 3
func (e *Executor) initRun() {
var runCmd = &cobra.Command{
Use: "run",
Short: "Run linters",
Run: e.executeRun,
}
e.rootCmd.AddCommand(runCmd)
// Output config
oc := &e.cfg.Output
runCmd.Flags().StringVar(&oc.Format, "out-format",
config.OutFormatColoredLineNumber,
fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|")))
runCmd.Flags().BoolVar(&oc.PrintIssuedLine, "print-issued-lines", true, "Print lines of code with issue")
runCmd.Flags().BoolVar(&oc.PrintLinterName, "print-linter-name", true, "Print linter name in issue line")
runCmd.Flags().BoolVar(&oc.PrintWelcomeMessage, "print-welcome", true, "Print welcome message")
// Run config
rc := &e.cfg.Run
runCmd.Flags().IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code",
1, "Exit code when issues were found")
runCmd.Flags().StringSliceVar(&rc.BuildTags, "build-tags", []string{}, "Build tags (not all linters support them)")
runCmd.Flags().DurationVar(&rc.Deadline, "deadline", time.Minute, "Deadline for total work")
runCmd.Flags().BoolVar(&rc.AnalyzeTests, "tests", false, "Analyze tests (*_test.go)")
runCmd.Flags().BoolVar(&rc.PrintResourcesUsage, "print-resources-usage", false, "Print avg and max memory usage of golangci-lint and total time")
runCmd.Flags().StringVarP(&rc.Config, "config", "c", "", "Read config from file path `PATH`")
// Linters settings config
lsc := &e.cfg.LintersSettings
runCmd.Flags().BoolVar(&lsc.Errcheck.CheckTypeAssertions, "errcheck.check-type-assertions", false, "Errcheck: check for ignored type assertion results")
runCmd.Flags().BoolVar(&lsc.Errcheck.CheckAssignToBlank, "errcheck.check-blank", false, "Errcheck: check for errors assigned to blank identifier: _ = errFunc()")
runCmd.Flags().BoolVar(&lsc.Govet.CheckShadowing, "govet.check-shadowing", false, "Govet: check for shadowed variables")
runCmd.Flags().Float64Var(&lsc.Golint.MinConfidence, "golint.min-confidence", 0.8, "Golint: minimum confidence of a problem to print it")
runCmd.Flags().BoolVar(&lsc.Gofmt.Simplify, "gofmt.simplify", true, "Gofmt: simplify code")
runCmd.Flags().IntVar(&lsc.Gocyclo.MinComplexity, "gocyclo.min-complexity",
30, "Minimal complexity of function to report it")
runCmd.Flags().BoolVar(&lsc.Maligned.SuggestNewOrder, "maligned.suggest-new", false, "Maligned: print suggested more optimal struct fields ordering")
runCmd.Flags().IntVar(&lsc.Dupl.Threshold, "dupl.threshold",
150, "Dupl: Minimal threshold to detect copy-paste")
runCmd.Flags().IntVar(&lsc.Goconst.MinStringLen, "goconst.min-len",
3, "Goconst: minimum constant string length")
runCmd.Flags().IntVar(&lsc.Goconst.MinOccurrencesCount, "goconst.min-occurrences",
3, "Goconst: minimum occurences of constant string count to trigger issue")
// Linters config
lc := &e.cfg.Linters
runCmd.Flags().StringSliceVarP(&lc.Enable, "enable", "E", []string{}, "Enable specific linter")
runCmd.Flags().StringSliceVarP(&lc.Disable, "disable", "D", []string{}, "Disable specific linter")
runCmd.Flags().BoolVar(&lc.EnableAll, "enable-all", false, "Enable all linters")
runCmd.Flags().BoolVar(&lc.DisableAll, "disable-all", false, "Disable all linters")
runCmd.Flags().StringSliceVarP(&lc.Presets, "presets", "p", []string{},
fmt.Sprintf("Enable presets (%s) of linters. Run 'golangci-lint linters' to see them. This option implies option --disable-all", strings.Join(pkg.AllPresets(), "|")))
runCmd.Flags().BoolVar(&lc.Fast, "fast", false, "Run only fast linters from enabled linters set")
// Issues config
ic := &e.cfg.Issues
runCmd.Flags().StringSliceVarP(&ic.ExcludePatterns, "exclude", "e", []string{}, "Exclude issue by regexp")
runCmd.Flags().BoolVar(&ic.UseDefaultExcludes, "exclude-use-default", true,
fmt.Sprintf("Use or not use default excludes: (%s)", strings.Join(config.DefaultExcludePatterns, "|")))
runCmd.Flags().IntVar(&ic.MaxIssuesPerLinter, "max-issues-per-linter", 50, "Maximum issues count per one linter. Set to 0 to disable")
runCmd.Flags().IntVar(&ic.MaxSameIssues, "max-same-issues", 3, "Maximum count of issues with the same text. Set to 0 to disable")
runCmd.Flags().BoolVarP(&ic.Diff, "new", "n", false, "Show only new issues: if there are unstaged changes or untracked files, only those changes are analyzed, else only changes in HEAD~ are analyzed")
runCmd.Flags().StringVar(&ic.DiffFromRevision, "new-from-rev", "", "Show only new issues created after git revision `REV`")
runCmd.Flags().StringVar(&ic.DiffPatchFilePath, "new-from-patch", "", "Show only new issues created in git patch with file path `PATH`")
e.parseConfig(runCmd)
}
func isFullImportNeeded(linters []pkg.Linter) bool {
for _, linter := range linters {
lc := pkg.GetLinterConfig(linter.Name())
if lc.DoesFullImport {
return true
}
}
return false
}
func isSSAReprNeeded(linters []pkg.Linter) bool {
for _, linter := range linters {
lc := pkg.GetLinterConfig(linter.Name())
if lc.NeedsSSARepr {
return true
}
}
return false
}
func loadWholeAppIfNeeded(ctx context.Context, linters []pkg.Linter, cfg *config.Run, paths *fsutils.ProjectPaths) (*loader.Program, *loader.Config, error) {
if !isFullImportNeeded(linters) {
return nil, nil, nil
}
startedAt := time.Now()
defer func() {
logrus.Infof("Program loading took %s", time.Since(startedAt))
}()
bctx := build.Default
bctx.BuildTags = append(bctx.BuildTags, cfg.BuildTags...)
loadcfg := &loader.Config{
Build: &bctx,
AllowErrors: true, // Try to analyze event partially
}
rest, err := loadcfg.FromArgs(paths.MixedPaths(), cfg.AnalyzeTests)
if err != nil {
return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err)
}
if len(rest) > 0 {
return nil, nil, fmt.Errorf("unhandled loading paths: %v", rest)
}
prog, err := loadcfg.Load()
if err != nil {
return nil, nil, fmt.Errorf("can't load paths: %s", err)
}
return prog, loadcfg, nil
}
func buildSSAProgram(ctx context.Context, lprog *loader.Program) *ssa.Program {
startedAt := time.Now()
defer func() {
logrus.Infof("SSA repr building took %s", time.Since(startedAt))
}()
ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug)
ssaProg.Build()
return ssaProg
}
func buildLintCtx(ctx context.Context, linters []pkg.Linter, cfg *config.Config) (*golinters.Context, error) {
args := cfg.Run.Args
if len(args) == 0 {
args = []string{"./..."}
}
paths, err := fsutils.GetPathsForAnalysis(ctx, args, cfg.Run.AnalyzeTests)
if err != nil {
return nil, err
}
prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, paths)
if err != nil {
return nil, err
}
var ssaProg *ssa.Program
if prog != nil && isSSAReprNeeded(linters) {
ssaProg = buildSSAProgram(ctx, prog)
}
return &golinters.Context{
Paths: paths,
Cfg: cfg,
Program: prog,
SSAProgram: ssaProg,
LoaderConfig: loaderConfig,
}, nil
}
func (e *Executor) runAnalysis(ctx context.Context, args []string) (chan result.Issue, error) {
e.cfg.Run.Args = args
linters, err := pkg.GetEnabledLinters(e.cfg)
if err != nil {
return nil, err
}
lintCtx, err := buildLintCtx(ctx, linters, e.cfg)
if err != nil {
return nil, err
}
excludePatterns := e.cfg.Issues.ExcludePatterns
if e.cfg.Issues.UseDefaultExcludes {
excludePatterns = append(excludePatterns, config.DefaultExcludePatterns...)
}
var excludeTotalPattern string
if len(excludePatterns) != 0 {
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
}
fset := token.NewFileSet()
if lintCtx.Program != nil {
fset = lintCtx.Program.Fset
}
runner := pkg.SimpleRunner{
Processors: []processors.Processor{
processors.NewPathPrettifier(), // must be before diff processor at least
processors.NewExclude(excludeTotalPattern),
processors.NewCgo(),
processors.NewNolint(fset),
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
}
func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
issues, err := e.runAnalysis(ctx, args)
if err != nil {
return err
}
var p printers.Printer
if e.cfg.Output.Format == config.OutFormatJSON {
p = printers.NewJSON()
} else {
p = printers.NewText(e.cfg.Output.PrintIssuedLine,
e.cfg.Output.Format == config.OutFormatColoredLineNumber, e.cfg.Output.PrintLinterName)
}
gotAnyIssues, err := p.Print(issues)
if err != nil {
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
}
if gotAnyIssues {
e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
return nil
}
return nil
}
func (e *Executor) executeRun(cmd *cobra.Command, args []string) {
needTrackResources := e.cfg.Run.IsVerbose || e.cfg.Run.PrintResourcesUsage
trackResourcesEndCh := make(chan struct{})
defer func() { // XXX: this defer must be before ctx.cancel defer
if needTrackResources { // wait until resource tracking finished to print properly
<-trackResourcesEndCh
}
}()
ctx, cancel := context.WithTimeout(context.Background(), e.cfg.Run.Deadline)
defer cancel()
if needTrackResources {
go watchResources(ctx, trackResourcesEndCh)
}
if e.cfg.Output.PrintWelcomeMessage {
fmt.Println("Run this tool in cloud on every github pull request in https://golangci.com for free (public repos)")
}
if err := e.runAndPrint(ctx, args); err != nil {
log.Print(err)
if e.exitCode == 0 {
e.exitCode = exitCodeIfFailure
}
}
}
func (e *Executor) parseConfig(cmd *cobra.Command) {
// XXX: hack with double parsing to acces "config" option here
if err := cmd.ParseFlags(os.Args); err != nil {
if err == pflag.ErrHelp {
return
}
log.Fatalf("Can't parse args: %s", err)
}
if err := viper.BindPFlags(cmd.Flags()); err != nil {
log.Fatalf("Can't bind cobra's flags to viper: %s", err)
}
viper.SetEnvPrefix("GOLANGCI")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
configFile := viper.GetString("config")
if configFile == "" {
viper.SetConfigName(".golangci")
viper.AddConfigPath("./")
} else {
viper.SetConfigFile(configFile)
}
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return
}
log.Fatalf("Can't read viper config: %s", err)
}
if err := viper.Unmarshal(&e.cfg); err != nil {
log.Fatalf("Can't unmarshal config by viper: %s", err)
}
if err := e.validateConfig(); err != nil {
log.Fatal(err)
}
}
func (e *Executor) validateConfig() error {
c := e.cfg
if len(c.Run.Args) != 0 {
return errors.New("option run.args in config aren't supported now")
}
if c.Run.CPUProfilePath != "" {
return errors.New("option run.cpuprofilepath 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 watchResources(ctx context.Context, done chan struct{}) {
startedAt := time.Now()
rssValues := []uint64{}
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
var m runtime.MemStats
runtime.ReadMemStats(&m)
rssValues = append(rssValues, m.Sys)
stop := false
select {
case <-ctx.Done():
stop = true
case <-ticker.C: // track every second
}
if stop {
break
}
}
var avg, max uint64
for _, v := range rssValues {
avg += v
if v > max {
max = v
}
}
avg /= uint64(len(rssValues))
const MB = 1024 * 1024
maxMB := float64(max) / MB
logrus.Infof("Memory: %d samples, avg is %.1fMB, max is %.1fMB",
len(rssValues), float64(avg)/MB, maxMB)
logrus.Infof("Execution took %s", time.Since(startedAt))
close(done)
}