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