Revamp the build system to be more inline with other Prometheus exporters. Notably add Darwin and Windows build targets, and add support for releases using tar files.
509 lines
16 KiB
Go
509 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
|
|
)
|
|
|
|
var (
|
|
// Locations to look for vendored linters.
|
|
vendoredSearchPaths = [][]string{
|
|
{"github.com", "alecthomas", "gometalinter", "_linters"},
|
|
{"gopkg.in", "alecthomas", "gometalinter.v1", "_linters"},
|
|
}
|
|
)
|
|
|
|
func setupFlags(app *kingpin.Application) {
|
|
app.Flag("config", "Load JSON configuration from file.").Envar("GOMETALINTER_CONFIG").Action(loadConfig).String()
|
|
app.Flag("disable", "Disable previously enabled linters.").PlaceHolder("LINTER").Short('D').Action(disableAction).Strings()
|
|
app.Flag("enable", "Enable previously disabled linters.").PlaceHolder("LINTER").Short('E').Action(enableAction).Strings()
|
|
app.Flag("linter", "Define a linter.").PlaceHolder("NAME:COMMAND:PATTERN").Action(cliLinterOverrides).StringMap()
|
|
app.Flag("message-overrides", "Override message from linter. {message} will be expanded to the original message.").PlaceHolder("LINTER:MESSAGE").StringMapVar(&config.MessageOverride)
|
|
app.Flag("severity", "Map of linter severities.").PlaceHolder("LINTER:SEVERITY").StringMapVar(&config.Severity)
|
|
app.Flag("disable-all", "Disable all linters.").Action(disableAllAction).Bool()
|
|
app.Flag("enable-all", "Enable all linters.").Action(enableAllAction).Bool()
|
|
app.Flag("format", "Output format.").PlaceHolder(config.Format).StringVar(&config.Format)
|
|
app.Flag("vendored-linters", "Use vendored linters (recommended).").BoolVar(&config.VendoredLinters)
|
|
app.Flag("fast", "Only run fast linters.").BoolVar(&config.Fast)
|
|
app.Flag("install", "Attempt to install all known linters.").Short('i').BoolVar(&config.Install)
|
|
app.Flag("update", "Pass -u to go tool when installing.").Short('u').BoolVar(&config.Update)
|
|
app.Flag("force", "Pass -f to go tool when installing.").Short('f').BoolVar(&config.Force)
|
|
app.Flag("download-only", "Pass -d to go tool when installing.").BoolVar(&config.DownloadOnly)
|
|
app.Flag("debug", "Display messages for failed linters, etc.").Short('d').BoolVar(&config.Debug)
|
|
app.Flag("concurrency", "Number of concurrent linters to run.").PlaceHolder(fmt.Sprintf("%d", runtime.NumCPU())).Short('j').IntVar(&config.Concurrency)
|
|
app.Flag("exclude", "Exclude messages matching these regular expressions.").Short('e').PlaceHolder("REGEXP").StringsVar(&config.Exclude)
|
|
app.Flag("include", "Include messages matching these regular expressions.").Short('I').PlaceHolder("REGEXP").StringsVar(&config.Include)
|
|
app.Flag("skip", "Skip directories with this name when expanding '...'.").Short('s').PlaceHolder("DIR...").StringsVar(&config.Skip)
|
|
app.Flag("vendor", "Enable vendoring support (skips 'vendor' directories and sets GO15VENDOREXPERIMENT=1).").BoolVar(&config.Vendor)
|
|
app.Flag("cyclo-over", "Report functions with cyclomatic complexity over N (using gocyclo).").PlaceHolder("10").IntVar(&config.Cyclo)
|
|
app.Flag("line-length", "Report lines longer than N (using lll).").PlaceHolder("80").IntVar(&config.LineLength)
|
|
app.Flag("min-confidence", "Minimum confidence interval to pass to golint.").PlaceHolder(".80").FloatVar(&config.MinConfidence)
|
|
app.Flag("min-occurrences", "Minimum occurrences to pass to goconst.").PlaceHolder("3").IntVar(&config.MinOccurrences)
|
|
app.Flag("min-const-length", "Minimum constant length.").PlaceHolder("3").IntVar(&config.MinConstLength)
|
|
app.Flag("dupl-threshold", "Minimum token sequence as a clone for dupl.").PlaceHolder("50").IntVar(&config.DuplThreshold)
|
|
app.Flag("sort", fmt.Sprintf("Sort output by any of %s.", strings.Join(sortKeys, ", "))).PlaceHolder("none").EnumsVar(&config.Sort, sortKeys...)
|
|
app.Flag("tests", "Include test files for linters that support this option.").Short('t').BoolVar(&config.Test)
|
|
app.Flag("deadline", "Cancel linters if they have not completed within this duration.").PlaceHolder("30s").DurationVar((*time.Duration)(&config.Deadline))
|
|
app.Flag("errors", "Only show errors.").BoolVar(&config.Errors)
|
|
app.Flag("json", "Generate structured JSON rather than standard line-based output.").BoolVar(&config.JSON)
|
|
app.Flag("checkstyle", "Generate checkstyle XML rather than standard line-based output.").BoolVar(&config.Checkstyle)
|
|
app.Flag("enable-gc", "Enable GC for linters (useful on large repositories).").BoolVar(&config.EnableGC)
|
|
app.Flag("aggregate", "Aggregate issues reported by several linters.").BoolVar(&config.Aggregate)
|
|
app.Flag("warn-unmatched-nolint", "Warn if a nolint directive is not matched with an issue.").BoolVar(&config.WarnUnmatchedDirective)
|
|
app.GetFlag("help").Short('h')
|
|
}
|
|
|
|
func cliLinterOverrides(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
|
|
// expected input structure - <name>:<command-spec>
|
|
parts := strings.SplitN(*element.Value, ":", 2)
|
|
if len(parts) < 2 {
|
|
return fmt.Errorf("incorrectly formatted input: %s", *element.Value)
|
|
}
|
|
name := parts[0]
|
|
spec := parts[1]
|
|
conf, err := parseLinterConfigSpec(name, spec)
|
|
if err != nil {
|
|
return fmt.Errorf("incorrectly formatted input: %s", *element.Value)
|
|
}
|
|
config.Linters[name] = StringOrLinterConfig(conf)
|
|
return nil
|
|
}
|
|
|
|
func loadConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
|
|
r, err := os.Open(*element.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close() // nolint: errcheck
|
|
err = json.NewDecoder(r).Decode(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, disable := range config.Disable {
|
|
for i, enable := range config.Enable {
|
|
if enable == disable {
|
|
config.Enable = append(config.Enable[:i], config.Enable[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func disableAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
|
|
out := []string{}
|
|
for _, linter := range config.Enable {
|
|
if linter != *element.Value {
|
|
out = append(out, linter)
|
|
}
|
|
}
|
|
config.Enable = out
|
|
return nil
|
|
}
|
|
|
|
func enableAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
|
|
config.Enable = append(config.Enable, *element.Value)
|
|
return nil
|
|
}
|
|
|
|
func disableAllAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
|
|
config.Enable = []string{}
|
|
return nil
|
|
}
|
|
|
|
func enableAllAction(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
|
|
for linter := range defaultLinters {
|
|
config.Enable = append(config.Enable, linter)
|
|
}
|
|
config.EnableAll = true
|
|
return nil
|
|
}
|
|
|
|
type debugFunction func(format string, args ...interface{})
|
|
|
|
func debug(format string, args ...interface{}) {
|
|
if config.Debug {
|
|
fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...)
|
|
}
|
|
}
|
|
|
|
func namespacedDebug(prefix string) debugFunction {
|
|
return func(format string, args ...interface{}) {
|
|
debug(prefix+format, args...)
|
|
}
|
|
}
|
|
|
|
func warning(format string, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, "WARNING: "+format+"\n", args...)
|
|
}
|
|
|
|
func formatLinters() string {
|
|
w := bytes.NewBuffer(nil)
|
|
for _, linter := range getDefaultLinters() {
|
|
install := "(" + linter.InstallFrom + ")"
|
|
if install == "()" {
|
|
install = ""
|
|
}
|
|
fmt.Fprintf(w, " %s: %s\n\tcommand: %s\n\tregex: %s\n\tfast: %t\n\tdefault enabled: %t\n\n",
|
|
linter.Name, install, linter.Command, linter.Pattern, linter.IsFast, linter.defaultEnabled)
|
|
}
|
|
return w.String()
|
|
}
|
|
|
|
func formatSeverity() string {
|
|
w := bytes.NewBuffer(nil)
|
|
for name, severity := range config.Severity {
|
|
fmt.Fprintf(w, " %s -> %s\n", name, severity)
|
|
}
|
|
return w.String()
|
|
}
|
|
|
|
func main() {
|
|
pathsArg := kingpin.Arg("path", "Directories to lint. Defaults to \".\". <path>/... will recurse.").Strings()
|
|
app := kingpin.CommandLine
|
|
setupFlags(app)
|
|
app.Help = fmt.Sprintf(`Aggregate and normalise the output of a whole bunch of Go linters.
|
|
|
|
PlaceHolder linters:
|
|
|
|
%s
|
|
|
|
Severity override map (default is "warning"):
|
|
|
|
%s
|
|
`, formatLinters(), formatSeverity())
|
|
kingpin.Parse()
|
|
|
|
if config.Install {
|
|
if config.VendoredLinters {
|
|
configureEnvironmentForInstall()
|
|
}
|
|
installLinters()
|
|
return
|
|
}
|
|
|
|
configureEnvironment()
|
|
include, exclude := processConfig(config)
|
|
|
|
start := time.Now()
|
|
paths := resolvePaths(*pathsArg, config.Skip)
|
|
|
|
linters := lintersFromConfig(config)
|
|
err := validateLinters(linters, config)
|
|
kingpin.FatalIfError(err, "")
|
|
|
|
issues, errch := runLinters(linters, paths, config.Concurrency, exclude, include)
|
|
status := 0
|
|
if config.JSON {
|
|
status |= outputToJSON(issues)
|
|
} else if config.Checkstyle {
|
|
status |= outputToCheckstyle(issues)
|
|
} else {
|
|
status |= outputToConsole(issues)
|
|
}
|
|
for err := range errch {
|
|
warning("%s", err)
|
|
status |= 2
|
|
}
|
|
elapsed := time.Since(start)
|
|
debug("total elapsed time %s", elapsed)
|
|
os.Exit(status)
|
|
}
|
|
|
|
// nolint: gocyclo
|
|
func processConfig(config *Config) (include *regexp.Regexp, exclude *regexp.Regexp) {
|
|
tmpl, err := template.New("output").Parse(config.Format)
|
|
kingpin.FatalIfError(err, "invalid format %q", config.Format)
|
|
config.formatTemplate = tmpl
|
|
|
|
// Linters are by their very nature, short lived, so disable GC.
|
|
// Reduced (user) linting time on kingpin from 0.97s to 0.64s.
|
|
if !config.EnableGC {
|
|
_ = os.Setenv("GOGC", "off")
|
|
}
|
|
if config.VendoredLinters && config.Install && config.Update {
|
|
warning(`Linters are now vendored by default, --update ignored. The original
|
|
behaviour can be re-enabled with --no-vendored-linters.
|
|
|
|
To request an update for a vendored linter file an issue at:
|
|
https://github.com/alecthomas/gometalinter/issues/new
|
|
`)
|
|
config.Update = false
|
|
}
|
|
// Force sorting by path if checkstyle mode is selected
|
|
// !jsonFlag check is required to handle:
|
|
// gometalinter --json --checkstyle --sort=severity
|
|
if config.Checkstyle && !config.JSON {
|
|
config.Sort = []string{"path"}
|
|
}
|
|
|
|
// PlaceHolder to skipping "vendor" directory if GO15VENDOREXPERIMENT=1 is enabled.
|
|
// TODO(alec): This will probably need to be enabled by default at a later time.
|
|
if os.Getenv("GO15VENDOREXPERIMENT") == "1" || config.Vendor {
|
|
if err := os.Setenv("GO15VENDOREXPERIMENT", "1"); err != nil {
|
|
warning("setenv GO15VENDOREXPERIMENT: %s", err)
|
|
}
|
|
config.Skip = append(config.Skip, "vendor")
|
|
config.Vendor = true
|
|
}
|
|
if len(config.Exclude) > 0 {
|
|
exclude = regexp.MustCompile(strings.Join(config.Exclude, "|"))
|
|
}
|
|
|
|
if len(config.Include) > 0 {
|
|
include = regexp.MustCompile(strings.Join(config.Include, "|"))
|
|
}
|
|
|
|
runtime.GOMAXPROCS(config.Concurrency)
|
|
return include, exclude
|
|
}
|
|
|
|
func outputToConsole(issues chan *Issue) int {
|
|
status := 0
|
|
for issue := range issues {
|
|
if config.Errors && issue.Severity != Error {
|
|
continue
|
|
}
|
|
fmt.Println(issue.String())
|
|
status = 1
|
|
}
|
|
return status
|
|
}
|
|
|
|
func outputToJSON(issues chan *Issue) int {
|
|
fmt.Println("[")
|
|
status := 0
|
|
for issue := range issues {
|
|
if config.Errors && issue.Severity != Error {
|
|
continue
|
|
}
|
|
if status != 0 {
|
|
fmt.Printf(",\n")
|
|
}
|
|
d, err := json.Marshal(issue)
|
|
kingpin.FatalIfError(err, "")
|
|
fmt.Printf(" %s", d)
|
|
status = 1
|
|
}
|
|
fmt.Printf("\n]\n")
|
|
return status
|
|
}
|
|
|
|
func resolvePaths(paths, skip []string) []string {
|
|
if len(paths) == 0 {
|
|
return []string{"."}
|
|
}
|
|
|
|
skipPath := newPathFilter(skip)
|
|
dirs := newStringSet()
|
|
for _, path := range paths {
|
|
if strings.HasSuffix(path, "/...") {
|
|
root := filepath.Dir(path)
|
|
_ = filepath.Walk(root, func(p string, i os.FileInfo, err error) error {
|
|
if err != nil {
|
|
warning("invalid path %q: %s", p, err)
|
|
return err
|
|
}
|
|
|
|
skip := skipPath(p)
|
|
switch {
|
|
case i.IsDir() && skip:
|
|
return filepath.SkipDir
|
|
case !i.IsDir() && !skip && strings.HasSuffix(p, ".go"):
|
|
dirs.add(filepath.Clean(filepath.Dir(p)))
|
|
}
|
|
return nil
|
|
})
|
|
} else {
|
|
dirs.add(filepath.Clean(path))
|
|
}
|
|
}
|
|
out := make([]string, 0, dirs.size())
|
|
for _, d := range dirs.asSlice() {
|
|
out = append(out, relativePackagePath(d))
|
|
}
|
|
sort.Strings(out)
|
|
for _, d := range out {
|
|
debug("linting path %s", d)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func newPathFilter(skip []string) func(string) bool {
|
|
filter := map[string]bool{}
|
|
for _, name := range skip {
|
|
filter[name] = true
|
|
}
|
|
|
|
return func(path string) bool {
|
|
base := filepath.Base(path)
|
|
if filter[base] || filter[path] {
|
|
return true
|
|
}
|
|
return base != "." && base != ".." && strings.ContainsAny(base[0:1], "_.")
|
|
}
|
|
}
|
|
|
|
func relativePackagePath(dir string) string {
|
|
if filepath.IsAbs(dir) || strings.HasPrefix(dir, ".") {
|
|
return dir
|
|
}
|
|
// package names must start with a ./
|
|
return "./" + dir
|
|
}
|
|
|
|
func lintersFromConfig(config *Config) map[string]*Linter {
|
|
out := map[string]*Linter{}
|
|
config.Enable = replaceWithMegacheck(config.Enable, config.EnableAll)
|
|
for _, name := range config.Enable {
|
|
linter := getLinterByName(name, LinterConfig(config.Linters[name]))
|
|
if config.Fast && !linter.IsFast {
|
|
continue
|
|
}
|
|
out[name] = linter
|
|
}
|
|
for _, linter := range config.Disable {
|
|
delete(out, linter)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// replaceWithMegacheck checks enabled linters if they duplicate megacheck and
|
|
// returns a either a revised list removing those and adding megacheck or an
|
|
// unchanged slice. Emits a warning if linters were removed and swapped with
|
|
// megacheck.
|
|
func replaceWithMegacheck(enabled []string, enableAll bool) []string {
|
|
var (
|
|
staticcheck,
|
|
gosimple,
|
|
unused bool
|
|
revised []string
|
|
)
|
|
for _, linter := range enabled {
|
|
switch linter {
|
|
case "staticcheck":
|
|
staticcheck = true
|
|
case "gosimple":
|
|
gosimple = true
|
|
case "unused":
|
|
unused = true
|
|
case "megacheck":
|
|
// Don't add to revised slice, we'll add it later
|
|
default:
|
|
revised = append(revised, linter)
|
|
}
|
|
}
|
|
if staticcheck && gosimple && unused {
|
|
if !enableAll {
|
|
warning("staticcheck, gosimple and unused are all set, using megacheck instead")
|
|
}
|
|
return append(revised, "megacheck")
|
|
}
|
|
return enabled
|
|
}
|
|
|
|
func findVendoredLinters() string {
|
|
gopaths := getGoPathList()
|
|
for _, home := range vendoredSearchPaths {
|
|
for _, p := range gopaths {
|
|
joined := append([]string{p, "src"}, home...)
|
|
vendorRoot := filepath.Join(joined...)
|
|
if _, err := os.Stat(vendorRoot); err == nil {
|
|
return vendorRoot
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Go 1.8 compatible GOPATH.
|
|
func getGoPath() string {
|
|
path := os.Getenv("GOPATH")
|
|
if path == "" {
|
|
user, err := user.Current()
|
|
kingpin.FatalIfError(err, "")
|
|
path = filepath.Join(user.HomeDir, "go")
|
|
}
|
|
return path
|
|
}
|
|
|
|
func getGoPathList() []string {
|
|
return strings.Split(getGoPath(), string(os.PathListSeparator))
|
|
}
|
|
|
|
// addPath appends path to paths if path does not already exist in paths. Returns
|
|
// the new paths.
|
|
func addPath(paths []string, path string) []string {
|
|
for _, existingpath := range paths {
|
|
if path == existingpath {
|
|
return paths
|
|
}
|
|
}
|
|
return append(paths, path)
|
|
}
|
|
|
|
// configureEnvironment adds all `bin/` directories from $GOPATH to $PATH
|
|
func configureEnvironment() {
|
|
paths := addGoBinsToPath(getGoPathList())
|
|
setEnv("PATH", strings.Join(paths, string(os.PathListSeparator)))
|
|
debugPrintEnv()
|
|
}
|
|
|
|
func addGoBinsToPath(gopaths []string) []string {
|
|
paths := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator))
|
|
for _, p := range gopaths {
|
|
paths = addPath(paths, filepath.Join(p, "bin"))
|
|
}
|
|
gobin := os.Getenv("GOBIN")
|
|
if gobin != "" {
|
|
paths = addPath(paths, gobin)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
// configureEnvironmentForInstall sets GOPATH and GOBIN so that vendored linters
|
|
// can be installed
|
|
func configureEnvironmentForInstall() {
|
|
gopaths := getGoPathList()
|
|
vendorRoot := findVendoredLinters()
|
|
if vendorRoot == "" {
|
|
kingpin.Fatalf("could not find vendored linters in GOPATH=%q", getGoPath())
|
|
}
|
|
debug("found vendored linters at %s, updating environment", vendorRoot)
|
|
|
|
gobin := os.Getenv("GOBIN")
|
|
if gobin == "" {
|
|
gobin = filepath.Join(gopaths[0], "bin")
|
|
}
|
|
setEnv("GOBIN", gobin)
|
|
|
|
// "go install" panics when one GOPATH element is beneath another, so set
|
|
// GOPATH to the vendor root
|
|
setEnv("GOPATH", vendorRoot)
|
|
debugPrintEnv()
|
|
}
|
|
|
|
func setEnv(key string, value string) {
|
|
if err := os.Setenv(key, value); err != nil {
|
|
warning("setenv %s: %s", key, err)
|
|
}
|
|
}
|
|
|
|
func debugPrintEnv() {
|
|
debug("PATH=%s", os.Getenv("PATH"))
|
|
debug("GOPATH=%s", os.Getenv("GOPATH"))
|
|
debug("GOBIN=%s", os.Getenv("GOBIN"))
|
|
}
|