diff --git a/.golangci.example.yml b/.golangci.example.yml index e79984378c57..dd9cd92fdf33 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -366,6 +366,8 @@ linters-settings: gosimple: # Select the Go version to target. The default is '1.13'. go: "1.15" + # https://staticcheck.io/docs/options#checks + checks: [ "all" ] govet: # report about shadowed variables @@ -514,10 +516,21 @@ linters-settings: staticcheck: # Select the Go version to target. The default is '1.13'. go: "1.15" + # https://staticcheck.io/docs/options#checks + checks: [ "all" ] stylecheck: # Select the Go version to target. The default is '1.13'. go: "1.15" + # https://staticcheck.io/docs/options#checks + checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022" ] + # https://staticcheck.io/docs/options#dot_import_whitelist + dot-import-whitelist: + - fmt + # https://staticcheck.io/docs/options#initialisms + initialisms: [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" ] + # https://staticcheck.io/docs/options#http_status_code_whitelist + http-status-code-whitelist: [ "200", "400", "404", "500" ] tagliatelle: # check the struck tag name case diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 3597dd5b9628..e3355af4c2c0 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -394,6 +394,15 @@ type RowsErrCheckSettings struct { type StaticCheckSettings struct { GoVersion string `mapstructure:"go"` + + Checks []string `mapstructure:"checks"` + Initialisms []string `mapstructure:"initialisms"` // only for stylecheck + DotImportWhitelist []string `mapstructure:"dot-import-whitelist"` // only for stylecheck + HTTPStatusCodeWhitelist []string `mapstructure:"http-status-code-whitelist"` // only for stylecheck +} + +func (s *StaticCheckSettings) HasConfiguration() bool { + return len(s.Initialisms) > 0 || len(s.HTTPStatusCodeWhitelist) > 0 || len(s.DotImportWhitelist) > 0 || len(s.Checks) > 0 } type StructCheckSettings struct { diff --git a/pkg/golinters/gosimple.go b/pkg/golinters/gosimple.go index 82e30b37d294..fa14f1a966f8 100644 --- a/pkg/golinters/gosimple.go +++ b/pkg/golinters/gosimple.go @@ -8,7 +8,9 @@ import ( ) func NewGosimple(settings *config.StaticCheckSettings) *goanalysis.Linter { - analyzers := setupStaticCheckAnalyzers(simple.Analyzers, settings) + cfg := staticCheckConfig(settings) + + analyzers := setupStaticCheckAnalyzers(simple.Analyzers, getGoVersion(settings), cfg.Checks) return goanalysis.NewLinter( "gosimple", diff --git a/pkg/golinters/staticcheck.go b/pkg/golinters/staticcheck.go index b0c2717bfabd..2226eabb4e21 100644 --- a/pkg/golinters/staticcheck.go +++ b/pkg/golinters/staticcheck.go @@ -8,7 +8,9 @@ import ( ) func NewStaticcheck(settings *config.StaticCheckSettings) *goanalysis.Linter { - analyzers := setupStaticCheckAnalyzers(staticcheck.Analyzers, settings) + cfg := staticCheckConfig(settings) + + analyzers := setupStaticCheckAnalyzers(staticcheck.Analyzers, getGoVersion(settings), cfg.Checks) return goanalysis.NewLinter( "staticcheck", diff --git a/pkg/golinters/staticcheck_common.go b/pkg/golinters/staticcheck_common.go index f0338032d001..e7476e230147 100644 --- a/pkg/golinters/staticcheck_common.go +++ b/pkg/golinters/staticcheck_common.go @@ -1,7 +1,11 @@ package golinters import ( + "strings" + "unicode" + "golang.org/x/tools/go/analysis" + scconfig "honnef.co/go/tools/config" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/logutils" @@ -9,25 +13,155 @@ import ( var debugf = logutils.Debug("megacheck") -func setupStaticCheckAnalyzers(m map[string]*analysis.Analyzer, settings *config.StaticCheckSettings) []*analysis.Analyzer { - var ret []*analysis.Analyzer - for _, v := range m { - setAnalyzerGoVersion(v, settings) - ret = append(ret, v) +func getGoVersion(settings *config.StaticCheckSettings) string { + var goVersion string + if settings != nil { + goVersion = settings.GoVersion + } + + if goVersion != "" { + return goVersion } - return ret -} -func setAnalyzerGoVersion(a *analysis.Analyzer, settings *config.StaticCheckSettings) { // TODO: uses "1.13" for backward compatibility, but in the future (v2) must be set by using build.Default.ReleaseTags like staticcheck. - goVersion := "1.13" - if settings != nil && settings.GoVersion != "" { - goVersion = settings.GoVersion + return "1.13" +} + +func setupStaticCheckAnalyzers(src map[string]*analysis.Analyzer, goVersion string, checks []string) []*analysis.Analyzer { + var names []string + for name := range src { + names = append(names, name) + } + + filter := filterAnalyzerNames(names, checks) + + var ret []*analysis.Analyzer + for name, a := range src { + if filter[name] { + setAnalyzerGoVersion(a, goVersion) + ret = append(ret, a) + } } + return ret +} + +func setAnalyzerGoVersion(a *analysis.Analyzer, goVersion string) { if v := a.Flags.Lookup("go"); v != nil { if err := v.Value.Set(goVersion); err != nil { debugf("Failed to set go version: %s", err) } } } + +func staticCheckConfig(settings *config.StaticCheckSettings) *scconfig.Config { + var cfg *scconfig.Config + + if settings == nil || !settings.HasConfiguration() { + return &scconfig.Config{ + Checks: []string{"*"}, // override for compatibility reason. Must drop in the next major version. + Initialisms: scconfig.DefaultConfig.Initialisms, + DotImportWhitelist: scconfig.DefaultConfig.DotImportWhitelist, + HTTPStatusCodeWhitelist: scconfig.DefaultConfig.HTTPStatusCodeWhitelist, + } + } + + cfg = &scconfig.Config{ + Checks: settings.Checks, + Initialisms: settings.Initialisms, + DotImportWhitelist: settings.DotImportWhitelist, + HTTPStatusCodeWhitelist: settings.HTTPStatusCodeWhitelist, + } + + if len(cfg.Checks) == 0 { + cfg.Checks = append(cfg.Checks, "*") // override for compatibility reason. Must drop in the next major version. + } + + if len(cfg.Initialisms) == 0 { + cfg.Initialisms = append(cfg.Initialisms, scconfig.DefaultConfig.Initialisms...) + } + + if len(cfg.DotImportWhitelist) == 0 { + cfg.DotImportWhitelist = append(cfg.DotImportWhitelist, scconfig.DefaultConfig.DotImportWhitelist...) + } + + if len(cfg.HTTPStatusCodeWhitelist) == 0 { + cfg.HTTPStatusCodeWhitelist = append(cfg.HTTPStatusCodeWhitelist, scconfig.DefaultConfig.HTTPStatusCodeWhitelist...) + } + + cfg.Checks = normalizeList(cfg.Checks) + cfg.Initialisms = normalizeList(cfg.Initialisms) + cfg.DotImportWhitelist = normalizeList(cfg.DotImportWhitelist) + cfg.HTTPStatusCodeWhitelist = normalizeList(cfg.HTTPStatusCodeWhitelist) + + return cfg +} + +// https://github.com/dominikh/go-tools/blob/9bf17c0388a65710524ba04c2d821469e639fdc2/lintcmd/lint.go#L437-L477 +// nolint // Keep the original source code. +func filterAnalyzerNames(analyzers []string, checks []string) map[string]bool { + allowedChecks := map[string]bool{} + + for _, check := range checks { + b := true + if len(check) > 1 && check[0] == '-' { + b = false + check = check[1:] + } + + if check == "*" || check == "all" { + // Match all + for _, c := range analyzers { + allowedChecks[c] = b + } + } else if strings.HasSuffix(check, "*") { + // Glob + prefix := check[:len(check)-1] + isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1 + + for _, a := range analyzers { + idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) }) + if isCat { + // Glob is S*, which should match S1000 but not SA1000 + cat := a[:idx] + if prefix == cat { + allowedChecks[a] = b + } + } else { + // Glob is S1* + if strings.HasPrefix(a, prefix) { + allowedChecks[a] = b + } + } + } + } else { + // Literal check name + allowedChecks[check] = b + } + } + return allowedChecks +} + +// https://github.com/dominikh/go-tools/blob/9bf17c0388a65710524ba04c2d821469e639fdc2/config/config.go#L95-L116 +func normalizeList(list []string) []string { + if len(list) > 1 { + nlist := make([]string, 0, len(list)) + nlist = append(nlist, list[0]) + for i, el := range list[1:] { + if el != list[i] { + nlist = append(nlist, el) + } + } + list = nlist + } + + for _, el := range list { + if el == "inherit" { + // This should never happen, because the default config + // should not use "inherit" + panic(`unresolved "inherit"`) + } + } + + return list +} diff --git a/pkg/golinters/stylecheck.go b/pkg/golinters/stylecheck.go index 175553accc32..899f6ff58236 100644 --- a/pkg/golinters/stylecheck.go +++ b/pkg/golinters/stylecheck.go @@ -1,6 +1,8 @@ package golinters import ( + "golang.org/x/tools/go/analysis" + scconfig "honnef.co/go/tools/config" "honnef.co/go/tools/stylecheck" "github.com/golangci/golangci-lint/pkg/config" @@ -8,7 +10,16 @@ import ( ) func NewStylecheck(settings *config.StaticCheckSettings) *goanalysis.Linter { - analyzers := setupStaticCheckAnalyzers(stylecheck.Analyzers, settings) + cfg := staticCheckConfig(settings) + + // `scconfig.Analyzer` is a singleton, then it's not possible to have more than one instance for all staticcheck "sub-linters". + // When we will merge the 4 "sub-linters", the problem will disappear: https://github.com/golangci/golangci-lint/issues/357 + // Currently only stylecheck analyzer has a configuration in staticcheck. + scconfig.Analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + return cfg, nil + } + + analyzers := setupStaticCheckAnalyzers(stylecheck.Analyzers, getGoVersion(settings), cfg.Checks) return goanalysis.NewLinter( "stylecheck", diff --git a/pkg/golinters/unused.go b/pkg/golinters/unused.go index 639929ded28d..09f6f202f064 100644 --- a/pkg/golinters/unused.go +++ b/pkg/golinters/unused.go @@ -54,7 +54,7 @@ func NewUnused(settings *config.StaticCheckSettings) *goanalysis.Linter { }, } - setAnalyzerGoVersion(analyzer, settings) + setAnalyzerGoVersion(analyzer, getGoVersion(settings)) lnt := goanalysis.NewLinter( name,