Add cross-compilation Makefile targets and tar-based releases.

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.
This commit is contained in:
Will Rouesnel
2017-11-30 03:15:53 +11:00
parent 61b93a17a6
commit 5b9fea01ee
98 changed files with 10599 additions and 1487 deletions

View File

@@ -19,7 +19,6 @@
- [2. Analyse the debug output](#2-analyse-the-debug-output)
- [3. Report an issue.](#3-report-an-issue)
- [How do I filter issues between two git refs?](#how-do-i-filter-issues-between-two-git-refs)
- [Details](#details)
- [Checkstyle XML format](#checkstyle-xml-format)
<!-- /MarkdownTOC -->
@@ -57,12 +56,13 @@ It is intended for use with editor/IDE integration.
- [go vet](https://golang.org/cmd/vet/) - Reports potential errors that otherwise compile.
- [go tool vet --shadow](https://golang.org/cmd/vet/#hdr-Shadowed_variables) - Reports variables that may have been unintentionally shadowed.
- [gotype](https://golang.org/x/tools/cmd/gotype) - Syntactic and semantic analysis similar to the Go compiler.
- [gotype -x](https://golang.org/x/tools/cmd/gotype) - Syntactic and semantic analysis in external test packages (similar to the Go compiler).
- [deadcode](https://github.com/tsenart/deadcode) - Finds unused code.
- [gocyclo](https://github.com/alecthomas/gocyclo) - Computes the cyclomatic complexity of functions.
- [golint](https://github.com/golang/lint) - Google's (mostly stylistic) linter.
- [varcheck](https://github.com/opennota/check) - Find unused global variables and constants.
- [structcheck](https://github.com/opennota/check) - Find unused struct fields.
- [aligncheck](https://github.com/opennota/check) - Warn about un-optimally aligned structures.
- [maligned](https://github.com/mdempsky/maligned) - Detect structs that would take less memory if their fields were sorted.
- [errcheck](https://github.com/kisielk/errcheck) - Check that error return values are used.
- [megacheck](https://github.com/dominikh/go-tools/tree/master/cmd/megacheck) - Run staticcheck, gosimple and unused, sharing work.
- [dupl](https://github.com/mibk/dupl) - Reports potentially duplicated code.
@@ -81,6 +81,7 @@ Disabled by default (enable with `--enable=<linter>`):
- [gosimple](https://github.com/dominikh/go-tools/tree/master/cmd/gosimple) - Report simplifications in code.
- [lll](https://github.com/walle/lll) - Report long lines (see `--line-length=N`).
- [misspell](https://github.com/client9/misspell) - Finds commonly misspelled English words.
- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns.
- [unparam](https://github.com/mvdan/unparam) - Find unused function parameters.
- [unused](https://github.com/dominikh/go-tools/tree/master/cmd/unused) - Find unused variables.
- [safesql](https://github.com/stripe/safesql) - Finds potential SQL injection vulnerabilities.
@@ -91,14 +92,15 @@ Additional linters can be added through the command line with `--linter=NAME:COM
## Configuration file
gometalinter now supports a JSON configuration file which can be loaded via
`--config=<file>`. The format of this file is determined by the Config struct
in `config.go`.
`--config=<file>`. The format of this file is determined by the `Config` struct
in [config.go](https://github.com/alecthomas/gometalinter/blob/master/config.go).
The configuration file mostly corresponds to command-line flags, with the following exceptions:
- Linters defined in the configuration file will overlay existing definitions, not replace them.
- "Enable" defines the exact set of linters that will be enabled (default
linters are disabled).
linters are disabled). `--help` displays the list of default linters with the exact names
you must use.
Here is an example configuration file:
@@ -108,6 +110,34 @@ Here is an example configuration file:
}
```
### Adding Custom linters
Linters can be added and customized from the config file using the `Linters` field.
Linters supports the following fields:
* `Command` - the path to the linter binary and any default arguments
* `Pattern` - a regular expression used to parse the linter output
* `IsFast` - if the linter should be run when the `--fast` flag is used
* `PartitionStrategy` - how paths args should be passed to the linter command:
* `directories` - call the linter once with a list of all the directories
* `files` - call the linter once with a list of all the files
* `packages` - call the linter once with a list of all the package paths
* `files-by-package` - call the linter once per package with a list of the
files in the package.
* `single-directory` - call the linter once per directory
The config for default linters can be overridden by using the name of the
linter.
Additional linters can be configured via the command line using the format
`NAME:COMMAND:PATTERN`.
Example:
```
$ gometalinter --linter='vet:go tool vet -printfuncs=Infof,Debugf,Warningf,Errorf:PATH:LINE:MESSAGE' .
```
## Installing
There are two options for installing gometalinter.
@@ -171,7 +201,8 @@ Install all known linters:
$ gometalinter --install
Installing:
structcheck
aligncheck
maligned
nakedret
deadcode
gocyclo
ineffassign
@@ -308,21 +339,6 @@ gometalinter |& revgrep master # Show issues between master and HEAD (or
gometalinter |& revgrep origin/master # Show issues that haven't been pushed.
```
## Details
Additional linters can be configured via the command line:
```
$ gometalinter --linter='vet:go tool vet -printfuncs=Infof,Debugf,Warningf,Errorf {path}:PATH:LINE:MESSAGE' .
stutter.go:21:15:warning: error return value not checked (defer a.Close()) (errcheck)
stutter.go:22:15:warning: error return value not checked (defer a.Close()) (errcheck)
stutter.go:27:6:warning: error return value not checked (doit() // test for errcheck) (errcheck)
stutter.go:9::warning: unused global variable unusedGlobal (varcheck)
stutter.go:13::warning: unused struct field MyStruct.Unused (structcheck)
stutter.go:12:6:warning: exported type MyStruct should have comment or be unexported (golint)
stutter.go:16:6:warning: exported type PublicUndocumented should have comment or be unexported (deadcode)
```
## Checkstyle XML format
`gometalinter` supports [checkstyle](http://checkstyle.sourceforge.net/)

View File

@@ -5,27 +5,21 @@ import (
"strings"
)
type (
issueKey struct {
path string
line, col int
message string
}
multiIssue struct {
*Issue
linterNames []string
}
)
func maybeAggregateIssues(issues chan *Issue) chan *Issue {
if !config.Aggregate {
return issues
}
return aggregateIssues(issues)
type issueKey struct {
path string
line, col int
message string
}
func aggregateIssues(issues chan *Issue) chan *Issue {
type multiIssue struct {
*Issue
linterNames []string
}
// AggregateIssueChan reads issues from a channel, aggregates issues which have
// the same file, line, vol, and message, and returns aggregated issues on
// a new channel.
func AggregateIssueChan(issues chan *Issue) chan *Issue {
out := make(chan *Issue, 1000000)
issueMap := make(map[issueKey]*multiIssue)
go func() {

View File

@@ -4,7 +4,7 @@ import (
"encoding/xml"
"fmt"
"gopkg.in/alecthomas/kingpin.v3-unstable"
kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
)
type checkstyleOutput struct {

View File

@@ -8,12 +8,12 @@ import (
)
// Config for gometalinter. This can be loaded from a JSON file with --config.
type Config struct { // nolint: aligncheck
// A map of linter name to "<command>:<pattern>".
type Config struct { // nolint: maligned
// A map from linter name -> <LinterConfig|string>.
//
// <command> should always include {path} as the target directory to execute. Globs in <command>
// are expanded by gometalinter (not by the shell).
Linters map[string]string
// For backwards compatibility, the value stored in the JSON blob can also
// be a string of the form "<command>:<pattern>".
Linters map[string]StringOrLinterConfig
// The set of linters that should be enabled.
Enable []string
@@ -51,6 +51,35 @@ type Config struct { // nolint: aligncheck
EnableGC bool
Aggregate bool
EnableAll bool
// Warn if a nolint directive was never matched to a linter issue
WarnUnmatchedDirective bool
formatTemplate *template.Template
}
type StringOrLinterConfig LinterConfig
func (c *StringOrLinterConfig) UnmarshalJSON(raw []byte) error {
var linterConfig LinterConfig
// first try to un-marshall directly into struct
origErr := json.Unmarshal(raw, &linterConfig)
if origErr == nil {
*c = StringOrLinterConfig(linterConfig)
return nil
}
// i.e. bytes didn't represent the struct, treat them as a string
var linterSpec string
if err := json.Unmarshal(raw, &linterSpec); err != nil {
return origErr
}
linter, err := parseLinterConfigSpec("", linterSpec)
if err != nil {
return err
}
*c = StringOrLinterConfig(linter)
return nil
}
type jsonDuration time.Duration
@@ -70,17 +99,16 @@ func (td *jsonDuration) Duration() time.Duration {
return time.Duration(*td)
}
// TODO: should be a field on Config struct
var formatTemplate = &template.Template{}
var sortKeys = []string{"none", "path", "line", "column", "severity", "message", "linter"}
// Configuration defaults.
var config = &Config{
Format: "{{.Path}}:{{.Line}}:{{if .Col}}{{.Col}}{{end}}:{{.Severity}}: {{.Message}} ({{.Linter}})",
Format: DefaultIssueFormat,
Linters: map[string]StringOrLinterConfig{},
Severity: map[string]string{
"gotype": "error",
"gotypex": "error",
"test": "error",
"testify": "error",
"vet": "error",

View File

@@ -1,6 +1,7 @@
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
@@ -14,6 +15,7 @@ type ignoredRange struct {
col int
start, end int
linters []string
matched bool
}
func (i *ignoredRange) matches(issue *Issue) bool {
@@ -35,6 +37,14 @@ func (i *ignoredRange) near(col, start int) bool {
return col == i.col && i.end == start-1
}
func (i *ignoredRange) String() string {
linters := strings.Join(i.linters, ",")
if len(i.linters) == 0 {
linters = "all"
}
return fmt.Sprintf("%s:%d-%d", linters, i.start, i.end)
}
type ignoredRanges []*ignoredRange
func (ir ignoredRanges) Len() int { return len(ir) }
@@ -66,12 +76,43 @@ func (d *directiveParser) IsIgnored(issue *Issue) bool {
d.lock.Unlock()
for _, r := range ranges {
if r.matches(issue) {
debug("nolint: matched %s to issue %s", r, issue)
r.matched = true
return true
}
}
return false
}
// Unmatched returns all the ranges which were never used to ignore an issue
func (d *directiveParser) Unmatched() map[string]ignoredRanges {
unmatched := map[string]ignoredRanges{}
for path, ranges := range d.files {
for _, ignore := range ranges {
if !ignore.matched {
unmatched[path] = append(unmatched[path], ignore)
}
}
}
return unmatched
}
// LoadFiles from a list of directories
func (d *directiveParser) LoadFiles(paths []string) error {
d.lock.Lock()
defer d.lock.Unlock()
filenames, err := pathsToFileGlobs(paths)
if err != nil {
return err
}
for _, filename := range filenames {
ranges := d.parseFile(filename)
sort.Sort(ranges)
d.files[filename] = ranges
}
return nil
}
// Takes a set of ignoredRanges, determines if they immediately precede a statement
// construct, and expands the range to include that construct. Why? So you can
// precede a function or struct with //nolint
@@ -150,7 +191,28 @@ func filterIssuesViaDirectives(directives *directiveParser, issues chan *Issue)
out <- issue
}
}
if config.WarnUnmatchedDirective {
for _, issue := range warnOnUnusedDirective(directives) {
out <- issue
}
}
close(out)
}()
return out
}
func warnOnUnusedDirective(directives *directiveParser) []*Issue {
out := []*Issue{}
for path, ranges := range directives.Unmatched() {
for _, ignore := range ranges {
issue, _ := NewIssue("nolint", config.formatTemplate)
issue.Path = path
issue.Line = ignore.start
issue.Col = ignore.col
issue.Message = "nolint directive did not match any issue"
out = append(out, issue)
}
}
return out
}

View File

@@ -8,14 +8,13 @@ import (
"path/filepath"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/google/shlex"
"gopkg.in/alecthomas/kingpin.v3-unstable"
kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
)
type Vars map[string]string
@@ -41,34 +40,8 @@ func (v Vars) Replace(s string) string {
return s
}
// Severity of linter message.
type Severity string
// Linter message severity levels.
const ( // nolint: deadcode
Error Severity = "error"
Warning Severity = "warning"
)
type Issue struct {
Linter string `json:"linter"`
Severity Severity `json:"severity"`
Path string `json:"path"`
Line int `json:"line"`
Col int `json:"col"`
Message string `json:"message"`
}
func (i *Issue) String() string {
buf := new(bytes.Buffer)
err := formatTemplate.Execute(buf, i)
kingpin.FatalIfError(err, "Invalid output format")
return buf.String()
}
type linterState struct {
*Linter
paths []string
issues chan *Issue
vars Vars
exclude *regexp.Regexp
@@ -76,26 +49,34 @@ type linterState struct {
deadline <-chan time.Time
}
func (l *linterState) Partitions() ([][]string, error) {
command := l.vars.Replace(l.Command)
cmdArgs, err := parseCommand(command)
func (l *linterState) Partitions(paths []string) ([][]string, error) {
cmdArgs, err := parseCommand(l.command())
if err != nil {
return nil, err
}
parts, err := l.Linter.PartitionStrategy(cmdArgs, l.paths)
parts, err := l.Linter.PartitionStrategy(cmdArgs, paths)
if err != nil {
return nil, err
}
return parts, nil
}
func (l *linterState) command() string {
return l.vars.Replace(l.Command)
}
func runLinters(linters map[string]*Linter, paths []string, concurrency int, exclude, include *regexp.Regexp) (chan *Issue, chan error) {
errch := make(chan error, len(linters))
concurrencych := make(chan bool, concurrency)
incomingIssues := make(chan *Issue, 1000000)
processedIssues := filterIssuesViaDirectives(
newDirectiveParser(),
maybeSortIssues(maybeAggregateIssues(incomingIssues)))
directiveParser := newDirectiveParser()
if config.WarnUnmatchedDirective {
directiveParser.LoadFiles(paths)
}
processedIssues := maybeSortIssues(filterIssuesViaDirectives(
directiveParser, maybeAggregateIssues(incomingIssues)))
vars := Vars{
"duplthreshold": fmt.Sprintf("%d", config.DuplThreshold),
@@ -105,43 +86,46 @@ func runLinters(linters map[string]*Linter, paths []string, concurrency int, exc
"min_occurrences": fmt.Sprintf("%d", config.MinOccurrences),
"min_const_length": fmt.Sprintf("%d", config.MinConstLength),
"tests": "",
"not_tests": "true",
}
if config.Test {
vars["tests"] = "-t"
vars["tests"] = "true"
vars["not_tests"] = ""
}
wg := &sync.WaitGroup{}
id := 1
for _, linter := range linters {
deadline := time.After(config.Deadline.Duration())
state := &linterState{
Linter: linter,
issues: incomingIssues,
paths: paths,
vars: vars,
exclude: exclude,
include: include,
deadline: deadline,
}
partitions, err := state.Partitions()
partitions, err := state.Partitions(paths)
if err != nil {
errch <- err
continue
}
for _, args := range partitions {
wg.Add(1)
concurrencych <- true
// Call the goroutine with a copy of the args array so that the
// contents of the array are not modified by the next iteration of
// the above for loop
go func(args []string) {
concurrencych <- true
err := executeLinter(state, args)
go func(id int, args []string) {
err := executeLinter(id, state, args)
if err != nil {
errch <- err
}
<-concurrencych
wg.Done()
}(append(args))
}(id, args)
id++
}
}
@@ -153,13 +137,14 @@ func runLinters(linters map[string]*Linter, paths []string, concurrency int, exc
return processedIssues, errch
}
func executeLinter(state *linterState, args []string) error {
func executeLinter(id int, state *linterState, args []string) error {
if len(args) == 0 {
return fmt.Errorf("missing linter command")
}
start := time.Now()
debug("executing %s", strings.Join(args, " "))
dbg := namespacedDebug(fmt.Sprintf("[%s.%d]: ", state.Name, id))
dbg("executing %s", strings.Join(args, " "))
buf := bytes.NewBuffer(nil)
command := args[0]
cmd := exec.Command(command, args[1:]...) // nolint: gas
@@ -191,12 +176,12 @@ func executeLinter(state *linterState, args []string) error {
}
if err != nil {
debug("warning: %s returned %s: %s", command, err, buf.String())
dbg("warning: %s returned %s: %s", command, err, buf.String())
}
processOutput(state, buf.Bytes())
processOutput(dbg, state, buf.Bytes())
elapsed := time.Since(start)
debug("%s linter took %s", state.Name, elapsed)
dbg("%s linter took %s", state.Name, elapsed)
return nil
}
@@ -216,10 +201,10 @@ func parseCommand(command string) ([]string, error) {
}
// nolint: gocyclo
func processOutput(state *linterState, out []byte) {
func processOutput(dbg debugFunction, state *linterState, out []byte) {
re := state.regex
all := re.FindAllSubmatchIndex(out, -1)
debug("%s hits %d: %s", state.Name, len(all), state.Pattern)
dbg("%s hits %d: %s", state.Name, len(all), state.Pattern)
cwd, err := os.Getwd()
if err != nil {
@@ -239,7 +224,9 @@ func processOutput(state *linterState, out []byte) {
group = append(group, fragment)
}
issue := &Issue{Line: 1, Linter: state.Linter.Name}
issue, err := NewIssue(state.Linter.Name, config.formatTemplate)
kingpin.FatalIfError(err, "Invalid output format")
for i, name := range re.SubexpNames() {
if group[i] == nil {
continue
@@ -275,8 +262,6 @@ func processOutput(state *linterState, out []byte) {
}
if sev, ok := config.Severity[state.Name]; ok {
issue.Severity = Severity(sev)
} else {
issue.Severity = Warning
}
if state.exclude != nil && state.exclude.MatchString(issue.String()) {
continue
@@ -319,66 +304,16 @@ func resolvePath(path string) string {
return path
}
type sortedIssues struct {
issues []*Issue
order []string
}
func (s *sortedIssues) Len() int { return len(s.issues) }
func (s *sortedIssues) Swap(i, j int) { s.issues[i], s.issues[j] = s.issues[j], s.issues[i] }
// nolint: gocyclo
func (s *sortedIssues) Less(i, j int) bool {
l, r := s.issues[i], s.issues[j]
for _, key := range s.order {
switch key {
case "path":
if l.Path > r.Path {
return false
}
case "line":
if l.Line > r.Line {
return false
}
case "column":
if l.Col > r.Col {
return false
}
case "severity":
if l.Severity > r.Severity {
return false
}
case "message":
if l.Message > r.Message {
return false
}
case "linter":
if l.Linter > r.Linter {
return false
}
}
}
return true
}
func maybeSortIssues(issues chan *Issue) chan *Issue {
if reflect.DeepEqual([]string{"none"}, config.Sort) {
return issues
}
out := make(chan *Issue, 1000000)
sorted := &sortedIssues{
issues: []*Issue{},
order: config.Sort,
}
go func() {
for issue := range issues {
sorted.issues = append(sorted.issues, issue)
}
sort.Sort(sorted)
for _, issue := range sorted.issues {
out <- issue
}
close(out)
}()
return out
return SortIssueChan(issues, config.Sort)
}
func maybeAggregateIssues(issues chan *Issue) chan *Issue {
if !config.Aggregate {
return issues
}
return AggregateIssueChan(issues)
}

View File

@@ -0,0 +1,114 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"sort"
"strings"
"text/template"
)
// DefaultIssueFormat used to print an issue
const DefaultIssueFormat = "{{.Path}}:{{.Line}}:{{if .Col}}{{.Col}}{{end}}:{{.Severity}}: {{.Message}} ({{.Linter}})"
// Severity of linter message
type Severity string
// Linter message severity levels.
const (
Error Severity = "error"
Warning Severity = "warning"
)
type Issue struct {
Linter string `json:"linter"`
Severity Severity `json:"severity"`
Path string `json:"path"`
Line int `json:"line"`
Col int `json:"col"`
Message string `json:"message"`
formatTmpl *template.Template
}
// NewIssue returns a new issue. Returns an error if formatTmpl is not a valid
// template for an Issue.
func NewIssue(linter string, formatTmpl *template.Template) (*Issue, error) {
issue := &Issue{
Line: 1,
Severity: Warning,
Linter: linter,
formatTmpl: formatTmpl,
}
err := formatTmpl.Execute(ioutil.Discard, issue)
return issue, err
}
func (i *Issue) String() string {
if i.formatTmpl == nil {
col := ""
if i.Col != 0 {
col = fmt.Sprintf("%d", i.Col)
}
return fmt.Sprintf("%s:%d:%s:%s: %s (%s)", strings.TrimSpace(i.Path), i.Line, col, i.Severity, strings.TrimSpace(i.Message), i.Linter)
}
buf := new(bytes.Buffer)
_ = i.formatTmpl.Execute(buf, i)
return buf.String()
}
type sortedIssues struct {
issues []*Issue
order []string
}
func (s *sortedIssues) Len() int { return len(s.issues) }
func (s *sortedIssues) Swap(i, j int) { s.issues[i], s.issues[j] = s.issues[j], s.issues[i] }
func (s *sortedIssues) Less(i, j int) bool {
l, r := s.issues[i], s.issues[j]
return CompareIssue(*l, *r, s.order)
}
// CompareIssue two Issues and return true if left should sort before right
// nolint: gocyclo
func CompareIssue(l, r Issue, order []string) bool {
for _, key := range order {
switch {
case key == "path" && l.Path != r.Path:
return l.Path < r.Path
case key == "line" && l.Line != r.Line:
return l.Line < r.Line
case key == "column" && l.Col != r.Col:
return l.Col < r.Col
case key == "severity" && l.Severity != r.Severity:
return l.Severity < r.Severity
case key == "message" && l.Message != r.Message:
return l.Message < r.Message
case key == "linter" && l.Linter != r.Linter:
return l.Linter < r.Linter
}
}
return true
}
// SortIssueChan reads issues from one channel, sorts them, and returns them to another
// channel
func SortIssueChan(issues chan *Issue, order []string) chan *Issue {
out := make(chan *Issue, 1000000)
sorted := &sortedIssues{
issues: []*Issue{},
order: order,
}
go func() {
for issue := range issues {
sorted.issues = append(sorted.issues, issue)
}
sort.Sort(sorted)
for _, issue := range sorted.issues {
out <- issue
}
close(out)
}()
return out
}

View File

@@ -8,11 +8,10 @@ import (
"sort"
"strings"
"gopkg.in/alecthomas/kingpin.v3-unstable"
kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
)
type LinterConfig struct {
Name string
Command string
Pattern string
InstallFrom string
@@ -23,11 +22,12 @@ type LinterConfig struct {
type Linter struct {
LinterConfig
Name string
regex *regexp.Regexp
}
// NewLinter returns a new linter from a config
func NewLinter(config LinterConfig) (*Linter, error) {
func NewLinter(name string, config LinterConfig) (*Linter, error) {
if p, ok := predefinedPatterns[config.Pattern]; ok {
config.Pattern = p
}
@@ -35,8 +35,12 @@ func NewLinter(config LinterConfig) (*Linter, error) {
if err != nil {
return nil, err
}
if config.PartitionStrategy == nil {
config.PartitionStrategy = partitionPathsAsDirectories
}
return &Linter{
LinterConfig: config,
Name: name,
regex: regex,
}, nil
}
@@ -50,26 +54,41 @@ var predefinedPatterns = map[string]string{
"PATH:LINE:MESSAGE": `^(?P<path>.*?\.go):(?P<line>\d+):\s*(?P<message>.*)$`,
}
func getLinterByName(name string, customSpec string) *Linter {
if customSpec != "" {
return parseLinterSpec(name, customSpec)
func getLinterByName(name string, overrideConf LinterConfig) *Linter {
conf := defaultLinters[name]
if val := overrideConf.Command; val != "" {
conf.Command = val
}
linter, _ := NewLinter(defaultLinters[name])
if val := overrideConf.Pattern; val != "" {
conf.Pattern = val
}
if val := overrideConf.InstallFrom; val != "" {
conf.InstallFrom = val
}
if overrideConf.IsFast {
conf.IsFast = true
}
if val := overrideConf.PartitionStrategy; val != nil {
conf.PartitionStrategy = val
}
linter, _ := NewLinter(name, conf)
return linter
}
func parseLinterSpec(name string, spec string) *Linter {
func parseLinterConfigSpec(name string, spec string) (LinterConfig, error) {
parts := strings.SplitN(spec, ":", 2)
if len(parts) < 2 {
kingpin.Fatalf("invalid linter: %q", spec)
return LinterConfig{}, fmt.Errorf("linter spec needs at least two components")
}
config := defaultLinters[name]
config.Command, config.Pattern = parts[0], parts[1]
if predefined, ok := predefinedPatterns[config.Pattern]; ok {
config.Pattern = predefined
}
linter, err := NewLinter(config)
kingpin.FatalIfError(err, "invalid linter %q", name)
return linter
return config, nil
}
func makeInstallCommand(linters ...string) []string {
@@ -148,9 +167,9 @@ func installLinters() {
func getDefaultLinters() []*Linter {
out := []*Linter{}
for _, config := range defaultLinters {
linter, err := NewLinter(config)
kingpin.FatalIfError(err, "invalid linter %q", config.Name)
for name, config := range defaultLinters {
linter, err := NewLinter(name, config)
kingpin.FatalIfError(err, "invalid linter %q", name)
out = append(out, linter)
}
return out
@@ -166,226 +185,228 @@ func defaultEnabled() []string {
return enabled
}
func validateLinters(linters map[string]*Linter, config *Config) error {
var unknownLinters []string
for name := range linters {
if _, isDefault := defaultLinters[name]; !isDefault {
if _, isCustom := config.Linters[name]; !isCustom {
unknownLinters = append(unknownLinters, name)
}
}
}
if len(unknownLinters) > 0 {
return fmt.Errorf("unknown linters: %s", strings.Join(unknownLinters, ", "))
}
return nil
}
const vetPattern = `^(?:vet:.*?\.go:\s+(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.*))|(?:(?P<path>.*?\.go):(?P<line>\d+):\s*(?P<message>.*))$`
var defaultLinters = map[string]LinterConfig{
"aligncheck": {
Name: "aligncheck",
Command: "aligncheck",
"maligned": {
Command: "maligned",
Pattern: `^(?:[^:]+: )?(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.+)$`,
InstallFrom: "github.com/opennota/check/cmd/aligncheck",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
InstallFrom: "github.com/mdempsky/maligned",
PartitionStrategy: partitionPathsAsPackages,
defaultEnabled: true,
},
"deadcode": {
Name: "deadcode",
Command: "deadcode",
Pattern: `^deadcode: (?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.*)$`,
InstallFrom: "github.com/tsenart/deadcode",
PartitionStrategy: partitionToMaxArgSize,
PartitionStrategy: partitionPathsAsDirectories,
defaultEnabled: true,
},
"dupl": {
Name: "dupl",
Command: `dupl -plumbing -threshold {duplthreshold}`,
Pattern: `^(?P<path>.*?\.go):(?P<line>\d+)-\d+:\s*(?P<message>.*)$`,
InstallFrom: "github.com/mibk/dupl",
PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
PartitionStrategy: partitionPathsAsFiles,
IsFast: true,
},
"errcheck": {
Name: "errcheck",
Command: `errcheck -abspath`,
Command: `errcheck -abspath {not_tests=-ignoretests}`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/kisielk/errcheck",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
defaultEnabled: true,
},
"gas": {
Name: "gas",
Command: `gas -fmt=csv`,
Pattern: `^(?P<path>.*?\.go),(?P<line>\d+),(?P<message>[^,]+,[^,]+,[^,]+)`,
InstallFrom: "github.com/GoASTScanner/gas",
PartitionStrategy: partitionToMaxArgSize,
PartitionStrategy: partitionPathsAsFiles,
defaultEnabled: true,
IsFast: true,
},
"goconst": {
Name: "goconst",
Command: `goconst -min-occurrences {min_occurrences} -min-length {min_const_length}`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/jgautheron/goconst/cmd/goconst",
PartitionStrategy: partitionToMaxArgSize,
PartitionStrategy: partitionPathsAsDirectories,
defaultEnabled: true,
IsFast: true,
},
"gocyclo": {
Name: "gocyclo",
Command: `gocyclo -over {mincyclo}`,
Pattern: `^(?P<cyclo>\d+)\s+\S+\s(?P<function>\S+)\s+(?P<path>.*?\.go):(?P<line>\d+):(\d+)$`,
InstallFrom: "github.com/alecthomas/gocyclo",
PartitionStrategy: partitionToMaxArgSize,
PartitionStrategy: partitionPathsAsDirectories,
defaultEnabled: true,
IsFast: true,
},
"gofmt": {
Name: "gofmt",
Command: `gofmt -l -s`,
Pattern: `^(?P<path>.*?\.go)$`,
PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
PartitionStrategy: partitionPathsAsFiles,
IsFast: true,
},
"goimports": {
Name: "goimports",
Command: `goimports -l`,
Pattern: `^(?P<path>.*?\.go)$`,
InstallFrom: "golang.org/x/tools/cmd/goimports",
PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
PartitionStrategy: partitionPathsAsFiles,
IsFast: true,
},
"golint": {
Name: "golint",
Command: `golint -min_confidence {min_confidence}`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/golang/lint/golint",
PartitionStrategy: partitionToMaxArgSize,
PartitionStrategy: partitionPathsAsDirectories,
defaultEnabled: true,
IsFast: true,
},
"gosimple": {
Name: "gosimple",
Command: `gosimple`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "honnef.co/go/tools/cmd/gosimple",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
},
"gotype": {
Name: "gotype",
Command: `gotype -e {tests=-t}`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "golang.org/x/tools/cmd/gotype",
PartitionStrategy: partitionToMaxArgSize,
PartitionStrategy: partitionPathsByDirectory,
defaultEnabled: true,
IsFast: true,
},
"gotypex": {
Command: `gotype -e -x`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "golang.org/x/tools/cmd/gotype",
PartitionStrategy: partitionPathsByDirectory,
defaultEnabled: true,
IsFast: true,
},
"ineffassign": {
Name: "ineffassign",
Command: `ineffassign -n`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/gordonklaus/ineffassign",
PartitionStrategy: partitionToMaxArgSize,
PartitionStrategy: partitionPathsAsDirectories,
defaultEnabled: true,
IsFast: true,
},
"interfacer": {
Name: "interfacer",
Command: `interfacer`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/mvdan/interfacer/cmd/interfacer",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
InstallFrom: "mvdan.cc/interfacer",
PartitionStrategy: partitionPathsAsPackages,
defaultEnabled: true,
},
"lll": {
Name: "lll",
Command: `lll -g -l {maxlinelength}`,
Pattern: `PATH:LINE:MESSAGE`,
InstallFrom: "github.com/walle/lll/cmd/lll",
PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
PartitionStrategy: partitionPathsAsFiles,
IsFast: true,
},
"megacheck": {
Name: "megacheck",
Command: `megacheck`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "honnef.co/go/tools/cmd/megacheck",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
defaultEnabled: true,
},
"misspell": {
Name: "misspell",
Command: `misspell -j 1`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/client9/misspell/cmd/misspell",
PartitionStrategy: partitionToMaxArgSizeWithFileGlobs,
PartitionStrategy: partitionPathsAsFiles,
IsFast: true,
},
"nakedret": {
Command: `nakedret`,
Pattern: `^(?P<path>.*?\.go):(?P<line>\d+)\s*(?P<message>.*)$`,
InstallFrom: "github.com/alexkohler/nakedret",
PartitionStrategy: partitionPathsAsDirectories,
},
"safesql": {
Name: "safesql",
Command: `safesql`,
Pattern: `^- (?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+)$`,
InstallFrom: "github.com/stripe/safesql",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
},
"staticcheck": {
Name: "staticcheck",
Command: `staticcheck`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "honnef.co/go/tools/cmd/staticcheck",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
},
"structcheck": {
Name: "structcheck",
Command: `structcheck {tests=-t}`,
Pattern: `^(?:[^:]+: )?(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.+)$`,
InstallFrom: "github.com/opennota/check/cmd/structcheck",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
defaultEnabled: true,
},
"test": {
Name: "test",
Command: `go test`,
Pattern: `^--- FAIL: .*$\s+(?P<path>.*?\.go):(?P<line>\d+): (?P<message>.*)$`,
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
},
"testify": {
Name: "testify",
Command: `go test`,
Pattern: `Location:\s+(?P<path>.*?\.go):(?P<line>\d+)$\s+Error:\s+(?P<message>[^\n]+)`,
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
},
"unconvert": {
Name: "unconvert",
Command: `unconvert`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/mdempsky/unconvert",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
defaultEnabled: true,
},
"unparam": {
Name: "unparam",
Command: `unparam`,
Command: `unparam {not_tests=-tests=false}`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "github.com/mvdan/unparam",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
InstallFrom: "mvdan.cc/unparam",
PartitionStrategy: partitionPathsAsPackages,
},
"unused": {
Name: "unused",
Command: `unused`,
Pattern: `PATH:LINE:COL:MESSAGE`,
InstallFrom: "honnef.co/go/tools/cmd/unused",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
},
"varcheck": {
Name: "varcheck",
Command: `varcheck`,
Pattern: `^(?:[^:]+: )?(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.*)$`,
InstallFrom: "github.com/opennota/check/cmd/varcheck",
PartitionStrategy: partitionToMaxArgSizeWithPackagePaths,
PartitionStrategy: partitionPathsAsPackages,
defaultEnabled: true,
},
"vet": {
Name: "vet",
Command: `go tool vet`,
Command: `govet --no-recurse`,
Pattern: vetPattern,
PartitionStrategy: partitionToPackageFileGlobs,
InstallFrom: "github.com/dnephin/govet",
PartitionStrategy: partitionPathsAsDirectories,
defaultEnabled: true,
IsFast: true,
},
"vetshadow": {
Name: "vetshadow",
Command: `go tool vet --shadow`,
Command: `govet --no-recurse --shadow`,
Pattern: vetPattern,
PartitionStrategy: partitionToPackageFileGlobs,
PartitionStrategy: partitionPathsAsDirectories,
defaultEnabled: true,
IsFast: true,
},

View File

@@ -14,7 +14,7 @@ import (
"text/template"
"time"
"gopkg.in/alecthomas/kingpin.v3-unstable"
kingpin "gopkg.in/alecthomas/kingpin.v3-unstable"
)
var (
@@ -26,10 +26,10 @@ var (
)
func setupFlags(app *kingpin.Application) {
app.Flag("config", "Load JSON configuration from file.").Action(loadConfig).String()
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").StringMapVar(&config.Linters)
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()
@@ -51,19 +51,36 @@ func setupFlags(app *kingpin.Application) {
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", "Minimumum constant length.").PlaceHolder("3").IntVar(&config.MinConstLength)
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("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 {
@@ -114,12 +131,20 @@ func enableAllAction(app *kingpin.Application, element *kingpin.ParseElement, ct
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...)
}
@@ -131,8 +156,8 @@ func formatLinters() string {
if install == "()" {
install = ""
}
fmt.Fprintf(w, " %s %s\n %s\n %s\n",
linter.Name, install, linter.Command, linter.Pattern)
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()
}
@@ -176,6 +201,9 @@ Severity override map (default is "warning"):
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 {
@@ -198,7 +226,7 @@ Severity override map (default is "warning"):
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)
formatTemplate = tmpl
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.
@@ -340,8 +368,7 @@ 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, config.Linters[name])
linter := getLinterByName(name, LinterConfig(config.Linters[name]))
if config.Fast && !linter.IsFast {
continue
}

View File

@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"path/filepath"
)
@@ -10,6 +11,29 @@ const MaxCommandBytes = 32000
type partitionStrategy func([]string, []string) ([][]string, error)
func (ps *partitionStrategy) UnmarshalJSON(raw []byte) error {
var strategyName string
if err := json.Unmarshal(raw, &strategyName); err != nil {
return err
}
switch strategyName {
case "directories":
*ps = partitionPathsAsDirectories
case "files":
*ps = partitionPathsAsFiles
case "packages":
*ps = partitionPathsAsPackages
case "files-by-package":
*ps = partitionPathsAsFilesGroupedByPackage
case "single-directory":
*ps = partitionPathsByDirectory
default:
return fmt.Errorf("unknown parition strategy %s", strategyName)
}
return nil
}
func pathsToFileGlobs(paths []string) ([]string, error) {
filePaths := []string{}
for _, dir := range paths {
@@ -22,7 +46,7 @@ func pathsToFileGlobs(paths []string) ([]string, error) {
return filePaths, nil
}
func partitionToMaxArgSize(cmdArgs []string, paths []string) ([][]string, error) {
func partitionPathsAsDirectories(cmdArgs []string, paths []string) ([][]string, error) {
return partitionToMaxSize(cmdArgs, paths, MaxCommandBytes), nil
}
@@ -72,15 +96,15 @@ func (p *sizePartitioner) end() [][]string {
return p.parts
}
func partitionToMaxArgSizeWithFileGlobs(cmdArgs []string, paths []string) ([][]string, error) {
func partitionPathsAsFiles(cmdArgs []string, paths []string) ([][]string, error) {
filePaths, err := pathsToFileGlobs(paths)
if err != nil || len(filePaths) == 0 {
return nil, err
}
return partitionToMaxArgSize(cmdArgs, filePaths)
return partitionPathsAsDirectories(cmdArgs, filePaths)
}
func partitionToPackageFileGlobs(cmdArgs []string, paths []string) ([][]string, error) {
func partitionPathsAsFilesGroupedByPackage(cmdArgs []string, paths []string) ([][]string, error) {
parts := [][]string{}
for _, path := range paths {
filePaths, err := pathsToFileGlobs([]string{path})
@@ -95,12 +119,12 @@ func partitionToPackageFileGlobs(cmdArgs []string, paths []string) ([][]string,
return parts, nil
}
func partitionToMaxArgSizeWithPackagePaths(cmdArgs []string, paths []string) ([][]string, error) {
func partitionPathsAsPackages(cmdArgs []string, paths []string) ([][]string, error) {
packagePaths, err := pathsToPackagePaths(paths)
if err != nil || len(packagePaths) == 0 {
return nil, err
}
return partitionToMaxArgSize(cmdArgs, packagePaths)
return partitionPathsAsDirectories(cmdArgs, packagePaths)
}
func pathsToPackagePaths(paths []string) ([]string, error) {
@@ -129,3 +153,11 @@ func packageNameFromPath(path string) (string, error) {
}
return "", fmt.Errorf("%s not in GOPATH", path)
}
func partitionPathsByDirectory(cmdArgs []string, paths []string) ([][]string, error) {
parts := [][]string{}
for _, path := range paths {
parts = append(parts, append(cmdArgs, path))
}
return parts, nil
}