github.com/dvyukov/gometalinter@v2.0.12-0.20181028185006-9777a28a8438+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  			_ = filepath.Walk(root, func(p string, i os.FileInfo, err error) error {
   328  				if err != nil {
   329  					warning("invalid path %q: %s", p, err)
   330  					return err
   331  				}
   332  
   333  				skip := skipPath(p)
   334  				switch {
   335  				case i.IsDir() && skip:
   336  					return filepath.SkipDir
   337  				case !i.IsDir() && !skip && strings.HasSuffix(p, ".go"):
   338  					dirs.add(filepath.Clean(filepath.Dir(p)))
   339  				}
   340  				return nil
   341  			})
   342  		} else {
   343  			dirs.add(filepath.Clean(path))
   344  		}
   345  	}
   346  	out := make([]string, 0, dirs.size())
   347  	for _, d := range dirs.asSlice() {
   348  		out = append(out, relativePackagePath(d))
   349  	}
   350  	sort.Strings(out)
   351  	for _, d := range out {
   352  		debug("linting path %s", d)
   353  	}
   354  	return out
   355  }
   356  
   357  func newPathFilter(skip []string) func(string) bool {
   358  	filter := map[string]bool{}
   359  	for _, name := range skip {
   360  		filter[name] = true
   361  	}
   362  
   363  	return func(path string) bool {
   364  		base := filepath.Base(path)
   365  		if filter[base] || filter[path] {
   366  			return true
   367  		}
   368  		return base != "." && base != ".." && strings.ContainsAny(base[0:1], "_.")
   369  	}
   370  }
   371  
   372  func relativePackagePath(dir string) string {
   373  	if filepath.IsAbs(dir) || strings.HasPrefix(dir, ".") {
   374  		return dir
   375  	}
   376  	// package names must start with a ./
   377  	return "./" + dir
   378  }
   379  
   380  func lintersFromConfig(config *Config) map[string]*Linter {
   381  	out := map[string]*Linter{}
   382  	config.Enable = replaceWithMegacheck(config.Enable, config.EnableAll)
   383  	for _, name := range config.Enable {
   384  		linter := getLinterByName(name, LinterConfig(config.Linters[name]))
   385  		if config.Fast && !linter.IsFast {
   386  			continue
   387  		}
   388  		out[name] = linter
   389  	}
   390  	for _, linter := range config.Disable {
   391  		delete(out, linter)
   392  	}
   393  	return out
   394  }
   395  
   396  // replaceWithMegacheck checks enabled linters if they duplicate megacheck and
   397  // returns a either a revised list removing those and adding megacheck or an
   398  // unchanged slice. Emits a warning if linters were removed and swapped with
   399  // megacheck.
   400  func replaceWithMegacheck(enabled []string, enableAll bool) []string {
   401  	var (
   402  		staticcheck,
   403  		gosimple,
   404  		unused bool
   405  		revised []string
   406  	)
   407  	for _, linter := range enabled {
   408  		switch linter {
   409  		case "staticcheck":
   410  			staticcheck = true
   411  		case "gosimple":
   412  			gosimple = true
   413  		case "unused":
   414  			unused = true
   415  		case "megacheck":
   416  			// Don't add to revised slice, we'll add it later
   417  		default:
   418  			revised = append(revised, linter)
   419  		}
   420  	}
   421  	if staticcheck && gosimple && unused {
   422  		if !enableAll {
   423  			warning("staticcheck, gosimple and unused are all set, using megacheck instead")
   424  		}
   425  		return append(revised, "megacheck")
   426  	}
   427  	return enabled
   428  }
   429  
   430  func findVendoredLinters() string {
   431  	gopaths := getGoPathList()
   432  	for _, home := range vendoredSearchPaths {
   433  		for _, p := range gopaths {
   434  			joined := append([]string{p, "src"}, home...)
   435  			vendorRoot := filepath.Join(joined...)
   436  			if _, err := os.Stat(vendorRoot); err == nil {
   437  				return vendorRoot
   438  			}
   439  		}
   440  	}
   441  	return ""
   442  }
   443  
   444  // Go 1.8 compatible GOPATH.
   445  func getGoPath() string {
   446  	path := os.Getenv("GOPATH")
   447  	if path == "" {
   448  		user, err := user.Current()
   449  		kingpin.FatalIfError(err, "")
   450  		path = filepath.Join(user.HomeDir, "go")
   451  	}
   452  	return path
   453  }
   454  
   455  func getGoPathList() []string {
   456  	return strings.Split(getGoPath(), string(os.PathListSeparator))
   457  }
   458  
   459  // addPath appends path to paths if path does not already exist in paths. Returns
   460  // the new paths.
   461  func addPath(paths []string, path string) []string {
   462  	for _, existingpath := range paths {
   463  		if path == existingpath {
   464  			return paths
   465  		}
   466  	}
   467  	return append(paths, path)
   468  }
   469  
   470  // configureEnvironment adds all `bin/` directories from $GOPATH to $PATH
   471  func configureEnvironment() {
   472  	paths := addGoBinsToPath(getGoPathList())
   473  	setEnv("PATH", strings.Join(paths, string(os.PathListSeparator)))
   474  	setEnv("GOROOT", discoverGoRoot())
   475  	debugPrintEnv()
   476  }
   477  
   478  func discoverGoRoot() string {
   479  	goroot := os.Getenv("GOROOT")
   480  	if goroot == "" {
   481  		output, err := exec.Command("go", "env", "GOROOT").Output()
   482  		kingpin.FatalIfError(err, "could not find go binary")
   483  		goroot = string(output)
   484  	}
   485  	return strings.TrimSpace(goroot)
   486  }
   487  
   488  func addGoBinsToPath(gopaths []string) []string {
   489  	paths := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator))
   490  	for _, p := range gopaths {
   491  		paths = addPath(paths, filepath.Join(p, "bin"))
   492  	}
   493  	gobin := os.Getenv("GOBIN")
   494  	if gobin != "" {
   495  		paths = addPath(paths, gobin)
   496  	}
   497  	return paths
   498  }
   499  
   500  // configureEnvironmentForInstall sets GOPATH and GOBIN so that vendored linters
   501  // can be installed
   502  func configureEnvironmentForInstall() {
   503  	if config.Update {
   504  		warning(`Linters are now vendored by default, --update ignored. The original
   505  behaviour can be re-enabled with --no-vendored-linters.
   506  
   507  To request an update for a vendored linter file an issue at:
   508  https://github.com/alecthomas/gometalinter/issues/new
   509  `)
   510  	}
   511  	gopaths := getGoPathList()
   512  	vendorRoot := findVendoredLinters()
   513  	if vendorRoot == "" {
   514  		kingpin.Fatalf("could not find vendored linters in GOPATH=%q", getGoPath())
   515  	}
   516  	debug("found vendored linters at %s, updating environment", vendorRoot)
   517  
   518  	gobin := os.Getenv("GOBIN")
   519  	if gobin == "" {
   520  		gobin = filepath.Join(gopaths[0], "bin")
   521  	}
   522  	setEnv("GOBIN", gobin)
   523  
   524  	// "go install" panics when one GOPATH element is beneath another, so set
   525  	// GOPATH to the vendor root
   526  	setEnv("GOPATH", vendorRoot)
   527  	debugPrintEnv()
   528  }
   529  
   530  func setEnv(key, value string) {
   531  	if err := os.Setenv(key, value); err != nil {
   532  		warning("setenv %s: %s", key, err)
   533  	} else {
   534  		debug("setenv %s=%q", key, value)
   535  	}
   536  }
   537  
   538  func debugPrintEnv() {
   539  	debug("Current environment:")
   540  	debug("PATH=%q", os.Getenv("PATH"))
   541  	debug("GOPATH=%q", os.Getenv("GOPATH"))
   542  	debug("GOBIN=%q", os.Getenv("GOBIN"))
   543  	debug("GOROOT=%q", os.Getenv("GOROOT"))
   544  }