github.com/bhcleek/gometalinter@v2.0.6-0.20180316043659-b25b44d18fb6+incompatible/main.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "os" 8 "os/user" 9 "path/filepath" 10 "regexp" 11 "runtime" 12 "sort" 13 "strings" 14 "text/template" 15 "time" 16 17 kingpin "gopkg.in/alecthomas/kingpin.v3-unstable" 18 ) 19 20 var ( 21 // Locations to look for vendored linters. 22 vendoredSearchPaths = [][]string{ 23 {"github.com", "alecthomas", "gometalinter", "_linters"}, 24 {"gopkg.in", "alecthomas", "gometalinter.v2", "_linters"}, 25 } 26 defaultConfigPath = ".gometalinter.json" 27 28 // Populated by goreleaser. 29 version = "master" 30 commit = "?" 31 date = "" 32 ) 33 34 func setupFlags(app *kingpin.Application) { 35 app.Flag("config", "Load JSON configuration from file.").Envar("GOMETALINTER_CONFIG").Action(loadConfig).String() 36 app.Flag("no-config", "Disable automatic loading of config file.").Bool() 37 app.Flag("disable", "Disable previously enabled linters.").PlaceHolder("LINTER").Short('D').Action(disableAction).Strings() 38 app.Flag("enable", "Enable previously disabled linters.").PlaceHolder("LINTER").Short('E').Action(enableAction).Strings() 39 app.Flag("linter", "Define a linter.").PlaceHolder("NAME:COMMAND:PATTERN").Action(cliLinterOverrides).StringMap() 40 app.Flag("message-overrides", "Override message from linter. {message} will be expanded to the original message.").PlaceHolder("LINTER:MESSAGE").StringMapVar(&config.MessageOverride) 41 app.Flag("severity", "Map of linter severities.").PlaceHolder("LINTER:SEVERITY").StringMapVar(&config.Severity) 42 app.Flag("disable-all", "Disable all linters.").Action(disableAllAction).Bool() 43 app.Flag("enable-all", "Enable all linters.").Action(enableAllAction).Bool() 44 app.Flag("format", "Output format.").PlaceHolder(config.Format).StringVar(&config.Format) 45 app.Flag("vendored-linters", "Use vendored linters (recommended) (DEPRECATED - use binary packages).").BoolVar(&config.VendoredLinters) 46 app.Flag("fast", "Only run fast linters.").BoolVar(&config.Fast) 47 app.Flag("install", "Attempt to install all known linters (DEPRECATED - use binary packages).").Short('i').BoolVar(&config.Install) 48 app.Flag("update", "Pass -u to go tool when installing (DEPRECATED - use binary packages).").Short('u').BoolVar(&config.Update) 49 app.Flag("force", "Pass -f to go tool when installing (DEPRECATED - use binary packages).").Short('f').BoolVar(&config.Force) 50 app.Flag("download-only", "Pass -d to go tool when installing (DEPRECATED - use binary packages).").BoolVar(&config.DownloadOnly) 51 app.Flag("debug", "Display messages for failed linters, etc.").Short('d').BoolVar(&config.Debug) 52 app.Flag("concurrency", "Number of concurrent linters to run.").PlaceHolder(fmt.Sprintf("%d", runtime.NumCPU())).Short('j').IntVar(&config.Concurrency) 53 app.Flag("exclude", "Exclude messages matching these regular expressions.").Short('e').PlaceHolder("REGEXP").StringsVar(&config.Exclude) 54 app.Flag("include", "Include messages matching these regular expressions.").Short('I').PlaceHolder("REGEXP").StringsVar(&config.Include) 55 app.Flag("skip", "Skip directories with this name when expanding '...'.").Short('s').PlaceHolder("DIR...").StringsVar(&config.Skip) 56 app.Flag("vendor", "Enable vendoring support (skips 'vendor' directories and sets GO15VENDOREXPERIMENT=1).").BoolVar(&config.Vendor) 57 app.Flag("cyclo-over", "Report functions with cyclomatic complexity over N (using gocyclo).").PlaceHolder("10").IntVar(&config.Cyclo) 58 app.Flag("line-length", "Report lines longer than N (using lll).").PlaceHolder("80").IntVar(&config.LineLength) 59 app.Flag("misspell-locale", "Specify locale to use (using misspell).").PlaceHolder("").StringVar(&config.MisspellLocale) 60 app.Flag("min-confidence", "Minimum confidence interval to pass to golint.").PlaceHolder(".80").FloatVar(&config.MinConfidence) 61 app.Flag("min-occurrences", "Minimum occurrences to pass to goconst.").PlaceHolder("3").IntVar(&config.MinOccurrences) 62 app.Flag("min-const-length", "Minimum constant length.").PlaceHolder("3").IntVar(&config.MinConstLength) 63 app.Flag("dupl-threshold", "Minimum token sequence as a clone for dupl.").PlaceHolder("50").IntVar(&config.DuplThreshold) 64 app.Flag("sort", fmt.Sprintf("Sort output by any of %s.", strings.Join(sortKeys, ", "))).PlaceHolder("none").EnumsVar(&config.Sort, sortKeys...) 65 app.Flag("tests", "Include test files for linters that support this option.").Short('t').BoolVar(&config.Test) 66 app.Flag("deadline", "Cancel linters if they have not completed within this duration.").PlaceHolder("30s").DurationVar((*time.Duration)(&config.Deadline)) 67 app.Flag("errors", "Only show errors.").BoolVar(&config.Errors) 68 app.Flag("json", "Generate structured JSON rather than standard line-based output.").BoolVar(&config.JSON) 69 app.Flag("checkstyle", "Generate checkstyle XML rather than standard line-based output.").BoolVar(&config.Checkstyle) 70 app.Flag("enable-gc", "Enable GC for linters (useful on large repositories).").BoolVar(&config.EnableGC) 71 app.Flag("aggregate", "Aggregate issues reported by several linters.").BoolVar(&config.Aggregate) 72 app.Flag("warn-unmatched-nolint", "Warn if a nolint directive is not matched with an issue.").BoolVar(&config.WarnUnmatchedDirective) 73 app.GetFlag("help").Short('h') 74 } 75 76 func cliLinterOverrides(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error { 77 // expected input structure - <name>:<command-spec> 78 parts := strings.SplitN(*element.Value, ":", 2) 79 if len(parts) < 2 { 80 return fmt.Errorf("incorrectly formatted input: %s", *element.Value) 81 } 82 name := parts[0] 83 spec := parts[1] 84 conf, err := parseLinterConfigSpec(name, spec) 85 if err != nil { 86 return fmt.Errorf("incorrectly formatted input: %s", *element.Value) 87 } 88 config.Linters[name] = StringOrLinterConfig(conf) 89 return nil 90 } 91 92 func loadDefaultConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error { 93 if element != nil { 94 return nil 95 } 96 97 for _, elem := range ctx.Elements { 98 if f := elem.OneOf.Flag; f == app.GetFlag("config") || f == app.GetFlag("no-config") { 99 return nil 100 } 101 } 102 103 configFile, found, err := findDefaultConfigFile() 104 if err != nil || !found { 105 return err 106 } 107 108 return loadConfigFile(configFile) 109 } 110 111 func loadConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error { 112 return loadConfigFile(*element.Value) 113 } 114 115 func disableAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error { 116 out := []string{} 117 for _, linter := range config.Enable { 118 if linter != *element.Value { 119 out = append(out, linter) 120 } 121 } 122 config.Enable = out 123 return nil 124 } 125 126 func enableAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error { 127 config.Enable = append(config.Enable, *element.Value) 128 return nil 129 } 130 131 func disableAllAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error { 132 config.Enable = []string{} 133 return nil 134 } 135 136 func enableAllAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error { 137 for linter := range defaultLinters { 138 config.Enable = append(config.Enable, linter) 139 } 140 config.EnableAll = true 141 return nil 142 } 143 144 type debugFunction func(format string, args ...interface{}) 145 146 func debug(format string, args ...interface{}) { 147 if config.Debug { 148 t := time.Now().UTC() 149 fmt.Fprintf(os.Stderr, "DEBUG: [%s] ", t.Format(time.StampMilli)) 150 fmt.Fprintf(os.Stderr, format+"\n", args...) 151 } 152 } 153 154 func namespacedDebug(prefix string) debugFunction { 155 return func(format string, args ...interface{}) { 156 debug(prefix+format, args...) 157 } 158 } 159 160 func warning(format string, args ...interface{}) { 161 fmt.Fprintf(os.Stderr, "WARNING: "+format+"\n", args...) 162 } 163 164 func formatLinters() string { 165 nameToLinter := map[string]*Linter{} 166 var linterNames []string 167 for _, linter := range getDefaultLinters() { 168 linterNames = append(linterNames, linter.Name) 169 nameToLinter[linter.Name] = linter 170 } 171 sort.Strings(linterNames) 172 173 w := bytes.NewBuffer(nil) 174 for _, linterName := range linterNames { 175 linter := nameToLinter[linterName] 176 177 install := "(" + linter.InstallFrom + ")" 178 if install == "()" { 179 install = "" 180 } 181 fmt.Fprintf(w, " %s: %s\n\tcommand: %s\n\tregex: %s\n\tfast: %t\n\tdefault enabled: %t\n\n", 182 linter.Name, install, linter.Command, linter.Pattern, linter.IsFast, linter.defaultEnabled) 183 } 184 return w.String() 185 } 186 187 func formatSeverity() string { 188 w := bytes.NewBuffer(nil) 189 for name, severity := range config.Severity { 190 fmt.Fprintf(w, " %s -> %s\n", name, severity) 191 } 192 return w.String() 193 } 194 195 func main() { 196 kingpin.Version(fmt.Sprintf("gometalinter version %s built from %s on %s", version, commit, date)) 197 pathsArg := kingpin.Arg("path", "Directories to lint. Defaults to \".\". <path>/... will recurse.").Strings() 198 app := kingpin.CommandLine 199 app.Action(loadDefaultConfig) 200 setupFlags(app) 201 app.Help = fmt.Sprintf(`Aggregate and normalise the output of a whole bunch of Go linters. 202 203 PlaceHolder linters: 204 205 %s 206 207 Severity override map (default is "warning"): 208 209 %s 210 `, formatLinters(), formatSeverity()) 211 kingpin.Parse() 212 213 if config.Install { 214 if config.VendoredLinters { 215 configureEnvironmentForInstall() 216 } 217 installLinters() 218 return 219 } 220 221 configureEnvironment() 222 include, exclude := processConfig(config) 223 224 start := time.Now() 225 paths := resolvePaths(*pathsArg, config.Skip) 226 227 linters := lintersFromConfig(config) 228 err := validateLinters(linters, config) 229 kingpin.FatalIfError(err, "") 230 231 issues, errch := runLinters(linters, paths, config.Concurrency, exclude, include) 232 status := 0 233 if config.JSON { 234 status |= outputToJSON(issues) 235 } else if config.Checkstyle { 236 status |= outputToCheckstyle(issues) 237 } else { 238 status |= outputToConsole(issues) 239 } 240 for err := range errch { 241 warning("%s", err) 242 status |= 2 243 } 244 elapsed := time.Since(start) 245 debug("total elapsed time %s", elapsed) 246 os.Exit(status) 247 } 248 249 // nolint: gocyclo 250 func processConfig(config *Config) (include *regexp.Regexp, exclude *regexp.Regexp) { 251 tmpl, err := template.New("output").Parse(config.Format) 252 kingpin.FatalIfError(err, "invalid format %q", config.Format) 253 config.formatTemplate = tmpl 254 255 // Linters are by their very nature, short lived, so disable GC. 256 // Reduced (user) linting time on kingpin from 0.97s to 0.64s. 257 if !config.EnableGC { 258 _ = os.Setenv("GOGC", "off") 259 } 260 // Force sorting by path if checkstyle mode is selected 261 // !jsonFlag check is required to handle: 262 // gometalinter --json --checkstyle --sort=severity 263 if config.Checkstyle && !config.JSON { 264 config.Sort = []string{"path"} 265 } 266 267 // PlaceHolder to skipping "vendor" directory if GO15VENDOREXPERIMENT=1 is enabled. 268 // TODO(alec): This will probably need to be enabled by default at a later time. 269 if os.Getenv("GO15VENDOREXPERIMENT") == "1" || config.Vendor { 270 if err := os.Setenv("GO15VENDOREXPERIMENT", "1"); err != nil { 271 warning("setenv GO15VENDOREXPERIMENT: %s", err) 272 } 273 config.Skip = append(config.Skip, "vendor") 274 config.Vendor = true 275 } 276 if len(config.Exclude) > 0 { 277 exclude = regexp.MustCompile(strings.Join(config.Exclude, "|")) 278 } 279 280 if len(config.Include) > 0 { 281 include = regexp.MustCompile(strings.Join(config.Include, "|")) 282 } 283 284 runtime.GOMAXPROCS(config.Concurrency) 285 return include, exclude 286 } 287 288 func outputToConsole(issues chan *Issue) int { 289 status := 0 290 for issue := range issues { 291 if config.Errors && issue.Severity != Error { 292 continue 293 } 294 fmt.Println(issue.String()) 295 status = 1 296 } 297 return status 298 } 299 300 func outputToJSON(issues chan *Issue) int { 301 fmt.Println("[") 302 status := 0 303 for issue := range issues { 304 if config.Errors && issue.Severity != Error { 305 continue 306 } 307 if status != 0 { 308 fmt.Printf(",\n") 309 } 310 d, err := json.Marshal(issue) 311 kingpin.FatalIfError(err, "") 312 fmt.Printf(" %s", d) 313 status = 1 314 } 315 fmt.Printf("\n]\n") 316 return status 317 } 318 319 func resolvePaths(paths, skip []string) []string { 320 if len(paths) == 0 { 321 return []string{"."} 322 } 323 324 skipPath := newPathFilter(skip) 325 dirs := newStringSet() 326 for _, path := range paths { 327 if strings.HasSuffix(path, "/...") { 328 root := filepath.Dir(path) 329 _ = filepath.Walk(root, func(p string, i os.FileInfo, err error) error { 330 if err != nil { 331 warning("invalid path %q: %s", p, err) 332 return err 333 } 334 335 skip := skipPath(p) 336 switch { 337 case i.IsDir() && skip: 338 return filepath.SkipDir 339 case !i.IsDir() && !skip && strings.HasSuffix(p, ".go"): 340 dirs.add(filepath.Clean(filepath.Dir(p))) 341 } 342 return nil 343 }) 344 } else { 345 dirs.add(filepath.Clean(path)) 346 } 347 } 348 out := make([]string, 0, dirs.size()) 349 for _, d := range dirs.asSlice() { 350 out = append(out, relativePackagePath(d)) 351 } 352 sort.Strings(out) 353 for _, d := range out { 354 debug("linting path %s", d) 355 } 356 return out 357 } 358 359 func newPathFilter(skip []string) func(string) bool { 360 filter := map[string]bool{} 361 for _, name := range skip { 362 filter[name] = true 363 } 364 365 return func(path string) bool { 366 base := filepath.Base(path) 367 if filter[base] || filter[path] { 368 return true 369 } 370 return base != "." && base != ".." && strings.ContainsAny(base[0:1], "_.") 371 } 372 } 373 374 func relativePackagePath(dir string) string { 375 if filepath.IsAbs(dir) || strings.HasPrefix(dir, ".") { 376 return dir 377 } 378 // package names must start with a ./ 379 return "./" + dir 380 } 381 382 func lintersFromConfig(config *Config) map[string]*Linter { 383 out := map[string]*Linter{} 384 config.Enable = replaceWithMegacheck(config.Enable, config.EnableAll) 385 for _, name := range config.Enable { 386 linter := getLinterByName(name, LinterConfig(config.Linters[name])) 387 if config.Fast && !linter.IsFast { 388 continue 389 } 390 out[name] = linter 391 } 392 for _, linter := range config.Disable { 393 delete(out, linter) 394 } 395 return out 396 } 397 398 // replaceWithMegacheck checks enabled linters if they duplicate megacheck and 399 // returns a either a revised list removing those and adding megacheck or an 400 // unchanged slice. Emits a warning if linters were removed and swapped with 401 // megacheck. 402 func replaceWithMegacheck(enabled []string, enableAll bool) []string { 403 var ( 404 staticcheck, 405 gosimple, 406 unused bool 407 revised []string 408 ) 409 for _, linter := range enabled { 410 switch linter { 411 case "staticcheck": 412 staticcheck = true 413 case "gosimple": 414 gosimple = true 415 case "unused": 416 unused = true 417 case "megacheck": 418 // Don't add to revised slice, we'll add it later 419 default: 420 revised = append(revised, linter) 421 } 422 } 423 if staticcheck && gosimple && unused { 424 if !enableAll { 425 warning("staticcheck, gosimple and unused are all set, using megacheck instead") 426 } 427 return append(revised, "megacheck") 428 } 429 return enabled 430 } 431 432 func findVendoredLinters() string { 433 gopaths := getGoPathList() 434 for _, home := range vendoredSearchPaths { 435 for _, p := range gopaths { 436 joined := append([]string{p, "src"}, home...) 437 vendorRoot := filepath.Join(joined...) 438 if _, err := os.Stat(vendorRoot); err == nil { 439 return vendorRoot 440 } 441 } 442 } 443 return "" 444 } 445 446 // Go 1.8 compatible GOPATH. 447 func getGoPath() string { 448 path := os.Getenv("GOPATH") 449 if path == "" { 450 user, err := user.Current() 451 kingpin.FatalIfError(err, "") 452 path = filepath.Join(user.HomeDir, "go") 453 } 454 return path 455 } 456 457 func getGoPathList() []string { 458 return strings.Split(getGoPath(), string(os.PathListSeparator)) 459 } 460 461 // addPath appends path to paths if path does not already exist in paths. Returns 462 // the new paths. 463 func addPath(paths []string, path string) []string { 464 for _, existingpath := range paths { 465 if path == existingpath { 466 return paths 467 } 468 } 469 return append(paths, path) 470 } 471 472 // configureEnvironment adds all `bin/` directories from $GOPATH to $PATH 473 func configureEnvironment() { 474 paths := addGoBinsToPath(getGoPathList()) 475 setEnv("PATH", strings.Join(paths, string(os.PathListSeparator))) 476 debugPrintEnv() 477 } 478 479 func addGoBinsToPath(gopaths []string) []string { 480 paths := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator)) 481 for _, p := range gopaths { 482 paths = addPath(paths, filepath.Join(p, "bin")) 483 } 484 gobin := os.Getenv("GOBIN") 485 if gobin != "" { 486 paths = addPath(paths, gobin) 487 } 488 return paths 489 } 490 491 // configureEnvironmentForInstall sets GOPATH and GOBIN so that vendored linters 492 // can be installed 493 func configureEnvironmentForInstall() { 494 if config.Update { 495 warning(`Linters are now vendored by default, --update ignored. The original 496 behaviour can be re-enabled with --no-vendored-linters. 497 498 To request an update for a vendored linter file an issue at: 499 https://github.com/alecthomas/gometalinter/issues/new 500 `) 501 } 502 gopaths := getGoPathList() 503 vendorRoot := findVendoredLinters() 504 if vendorRoot == "" { 505 kingpin.Fatalf("could not find vendored linters in GOPATH=%q", getGoPath()) 506 } 507 debug("found vendored linters at %s, updating environment", vendorRoot) 508 509 gobin := os.Getenv("GOBIN") 510 if gobin == "" { 511 gobin = filepath.Join(gopaths[0], "bin") 512 } 513 setEnv("GOBIN", gobin) 514 515 // "go install" panics when one GOPATH element is beneath another, so set 516 // GOPATH to the vendor root 517 setEnv("GOPATH", vendorRoot) 518 debugPrintEnv() 519 } 520 521 func setEnv(key string, value string) { 522 if err := os.Setenv(key, value); err != nil { 523 warning("setenv %s: %s", key, err) 524 } 525 } 526 527 func debugPrintEnv() { 528 debug("PATH=%s", os.Getenv("PATH")) 529 debug("GOPATH=%s", os.Getenv("GOPATH")) 530 debug("GOBIN=%s", os.Getenv("GOBIN")) 531 }