Published on

staticcheck -- golangci-lint 中最令人困惑的一个 linter

Authors
  • avatar
    Name
    ttyS3
    Twitter

如果你不想看过程,那就直接跳到最后看一句话总结吧。

megacheck

megacheck 其实是一个 deprecated linter, 不过只是被原作者deprecated, 目前 golangci-lint 并没有 deprecated 它。原因主要是向后兼容性 (see https://github.com/golangci/golangci-lint/issues/357 )。

这可能是最令人费解的一个linter了,为什么呢?我这里简单总结一下:

megacheck 是 https://github.com/dominikh/go-tools 早期代码里的一个linter, 它包含了3个linter: gosimple staticcheck unused 而 go-tools 的 go module 名称为: honnef.co/go/tools, 没错,并不是 github.com/dominikh/go-tools 到这里还没结束,嗯。 新版本的go-tools 代码里已经没有 megacheck 这个 linter了。 新版本取了一个新名字叫 Staticcheck , 自称 "The advanced Go linter", 自我描述为 "The best static analysis for Go that money can't buy" 代码仓库地址保持不变, 还在 https://github.com/dominikh/go-tools 不单有自己的官网,而且把它所有的检测文档化了, 访问 https://staticcheck.io/docs/checks 可以查看所有检测.

早期的 go-tools (也就是现在的 staticcheck) 其实是包含一系列linter的, gosimple staticcheck unused 其中 staticcheck 里面包含了 stylecheck`.

而新版本 的 staticcheck (这里指的是 cmd, 不是staticcheck package "honnef.co/go/tools/staticcheck")

包含了4种linter:

gosimple  "honnef.co/go/tools/simple"
staticcheck "honnef.co/go/tools/staticcheck"
stylecheck "honnef.co/go/tools/stylecheck"
unused "honnef.co/go/tools/unused"

see https://github.com/dominikh/go-tools/blob/master/cmd/staticcheck/staticcheck.go

而 当前版本的 golangci-lint (现在最新是 v1.40.1 ) 里的 staticcheck 实际上指的是 "honnef.co/go/tools/staticcheck", 并不是指 https://github.com/dominikh/go-tools/blob/master/cmd/staticcheck/staticcheck.go

https://github.com/golangci/golangci-lint/issues/357 说的 staticcheck 是指 cmd/staticcheck, 也就是真正的 四合一版的 staticcheck

但是由于 当前旧版本的 staticcheck 是在 golangci-lint 的 "Enabled By Default Linters" 里,基于 semver, 这个提议只能放在 v2.0.0 版的 golangci-lint, see https://github.com/golangci/golangci-lint/issues/357#issuecomment-841850534

staticcheck is up-to-date in golangci-lint, and we update the dependency automatically via dependabot.

The issue is still opened because the merge of the 4 linters is breaking, so it will be done in the next major version (v2).

再回到 staticcheck, 为什么仓库名这么奇怪,还是叫 go-tools 呢?一来,可能是由于历史原因,作者保留了仓库名称没有变。 二来,其实 go-tools 下确实不只 staticcheck 这个 linter, 还有另一个小工具 keyifystructlayout 三兄弟

注意, golangci-lint 虽然 fork 并保留了 go-tools 早期的代码,但是现在最新发布的 golangci-lint 其实是直接引用了最新的 go-tools 包代码.

如果我们将配置设置为禁用所有linter(主要是默认启用的那些), 再执行 golangci-lint run -v

[linters]
disable-all = true
enable = [
    "megacheck",
]

会发现如下提示:

INFO [lintersdb] Active 3 linters: [gosimple staticcheck unused]

我们看看 golangci-lint 的代码就明白为什么启用 megacheck 后会提示我们启用了3个 linter:

// file: pkg/lint/lintersdb/manager.go

// https://github.com/golangci/golangci-lint/blob/fe0db3db215535884342b3b884ec0e637314921d/pkg/lint/lintersdb/manager.go#L144
const megacheckName = "megacheck"

// https://github.com/golangci/golangci-lint/blob/fe0db3db215535884342b3b884ec0e637314921d/pkg/lint/lintersdb/manager.go#L180

linter.NewConfig(golinters.NewStaticcheck(staticcheckCfg)).
	WithSince("v1.0.0").
	WithLoadForGoAnalysis().
	WithPresets(linter.PresetBugs, linter.PresetMetaLinter).
	WithAlternativeNames(megacheckName).
	WithURL("https://staticcheck.io/"),
linter.NewConfig(golinters.NewUnused(unusedCfg)).
	WithSince("v1.20.0").
	WithLoadForGoAnalysis().
	WithPresets(linter.PresetUnused).
	WithAlternativeNames(megacheckName).
	ConsiderSlow().
	WithChangeTypes().
	WithURL("https://github.com/dominikh/go-tools/tree/master/unused"),
linter.NewConfig(golinters.NewGosimple(gosimpleCfg)).
	WithSince("v1.20.0").
	WithLoadForGoAnalysis().
	WithPresets(linter.PresetStyle).
	WithAlternativeNames(megacheckName).
	WithURL("https://github.com/dominikh/go-tools/tree/master/simple"),

staticcheck

当然,如果你想安装原汁原味的 staticcheck, 也不是不可以:

go install honnef.co/go/tools/cmd/staticcheck@latest

staticcheck -list-checks 可以列出它当前支持的所有检测

当前最新稳定版为 v0.2.0

STATICCHECK 2021.1 RELEASE NOTES https://staticcheck.io/changes/2021.1

新增加了啥和改了啥,我这里就不说了,大家自己看文档, 我挑两个个人比较关注的说一下:

Better integration with gopls

Several behind the scenes changes prepare this release for better integration with gopls. This will include more accurate severities for diagnostics as well as numerous new refactorings. These improvements will be part of a future gopls release.

https://github.com/golang/tools/blob/master/gopls/doc/settings.md#staticcheck-bool

Deletion of rdeps

The rdeps tool has been deleted. This was a GOPATH-centric tool that allowed finding all reverse dependencies of a Go package. Both the move to Go modules as well as the emergence of much better tooling for inspecting dependencies (such as goda) has made rdeps redundant.

不得不说一下,作者这里推荐的 goda 也是个好东西。 配合 dot (来自Linux graphviz包) 可以画出漂亮的mod依赖图来:

git clone https://github.com/loov/goda && cd goda
goda graph github.com/loov/goda/... | dot -Tsvg -o graph.svg

我们看看 https://github.com/golangci/go-tools/blob/master/cmd/megacheck/megacheck.go 其导入的关键package为:

	"github.com/golangci/go-tools/simple"
	"github.com/golangci/go-tools/staticcheck"
	"github.com/golangci/go-tools/unused"

https://github.com/golangci/go-tools/blob/master/cmd/staticcheck/staticcheck.go 导入的

	"github.com/golangci/go-tools/simple"
	"github.com/golangci/go-tools/staticcheck"
	"github.com/golangci/go-tools/stylecheck"
	"github.com/golangci/go-tools/unused"

然后我们继续看 https://github.com/golangci/go-tools/tree/master/cmd 就发现, 早期的go-tools(也就是现在的staticcheck) 其实包含一系列的linter:

  1. errcheck-ng - errcheck-ng is the next generation of errcheck.
  2. gosimple
  3. keyify
  4. megacheck
  5. rdeps
  6. staticcheck
  7. structlayout-optimize
  8. structlayout-pretty
  9. structlayout
  10. unused

在 golangci-lint 当前版本 (v1.40.1)里, megacheck = [gosimple staticcheck unused]

staticcheck 就只有 staticcheck

我们看看新版本的: https://github.com/dominikh/go-tools/tree/master/cmd 只剩下

  1. keyify
  2. staticcheck
  3. structlayout-optimize
  4. structlayout-pretty
  5. structlayout

Keyify turns unkeyed struct literals (T{1, 2, 3}) into keyed ones (T{A: 1, B: 2, C: 3})

rdeps scans GOPATH for all reverse dependencies of a set of Go packages.

staticcheck offers extensive analysis of Go code, covering a myriad of categories. It will detect bugs, suggest code simplifications, point out dead code, and more.

由于 go module 的引入, 已经不存在全局唯一的 GOPATH 了, 因此 rdeps 也在最近被作者移除

cmd/rdeps: delete rdeps The move to Go modules has greatly diminished the usefulness of the rdeps tool, as there is no longer a universe of packages (GOPATH) to inspect, and because we've never updated the tool to work with modules in the first place.

On top of that, a much more powerful tool – goda – has emerged, which allows for far more advanced queries on dependency graphs in the scope of a module.

https://github.com/dominikh/go-tools/blob/master/cmd/structlayout/README.md

The structlayout utility prints the layout of a struct – that is the byte offset and size of each field, respecting alignment/padding.

The information is printed in human-readable form by default, but can be emitted as JSON with the -json flag. This makes it easy to consume this information in other tools.

A utility called structlayout-pretty takes this JSON and prints an ASCII graphic representing the memory layout.

structlayout-optimize is another tool. Inspired by maligned, it reads structlayout JSON on stdin and reorders fields to minimize the amount of padding. The tool can itself emit JSON and feed into e.g. structlayout-pretty.

structlayout-svg is a third-party tool that, similarly to structlayout-pretty, visualises struct layouts. It does so by generating a fancy-looking SVG graphic. You can install it via

go get github.com/ajstarks/svgo/structlayout-svg

那么,为什么我们默认没有启用 stylecheck 呢? 因为这个 linter 实在是太难过了,尤其是对于历史悠久的代码。 我这里随便跑一个开了 stylecheck 的,列举一些例子哈:

ST1003: var theSql should be theSQL (stylecheck)
ST1003: struct field UserId should be UserID (stylecheck)
ST1003: struct field HttpServer should be HTTPServer (stylecheck)
ST1003: struct field BaseUserApi should be BaseUserAPI (stylecheck)
ST1003: var searchDb should be searchDB (stylecheck)
ST1003: var infoJson should be infoJSON (stylecheck)
ST1003: method parameter userId should be userID (stylecheck)
ST1003: method parameter reqUid should be reqUID (stylecheck)
ST1003: should not use ALL_CAPS in Go names; use CamelCase instead (stylecheck) // const PROM_SUBSYSTEM = "user_search_updater"

但是,如果我们直接运行 staticcheck ./... 却不会有这个错误。这是为什么?

我们看看 golangci-lint 的代码:

// pkg/golinters/staticcheck_common.go


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
}

由于当前版本的 golangci-lint 将 staticcheck 一分为四了,因此 这个 staticCheckConfig 实际上是给四个 linter 共用的获取配置的方法.

在 golangci-lint 中,对于 staticcheck 4件套, 默认情况下,我们没有做任何配置, 因此是执行的:

	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,
		}
	}

我们看一下 HasConfiguration() 方法:


// pkg/config/linters_settings.go

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
}

// pkg/config/linters_settings.go
func (s *StaticCheckSettings) HasConfiguration() bool {
	return len(s.Initialisms) > 0 || len(s.HTTPStatusCodeWhitelist) > 0 || len(s.DotImportWhitelist) > 0 || len(s.Checks) > 0
}

除了注意 HasConfiguration(), 我们还需要注意这里的 only for stylecheck, 即 initialisms dot-import-whitelist 和 http-status-code-whitelist 这3个配置都是仅对 stylecheck 生效的。

通过查看 HasConfiguration() 的代码,我们清楚了,只要 checks initialisms dot-import-whitelist http-status-code-whitelist 四个配置里的任何一个存在,就会先应用用户的配置。

另外, func staticCheckConfig 里面还有逻辑,即 initialisms dot-import-whitelist http-status-code-whitelist 任何一个为空,都会取 "honnef.co/go/tools/config" 的默认配置, 也就是

// https://github.com/dominikh/go-tools/blob/e75722ce7c1c555645e84dc08b2b9e8d54624d98/config/config.go#L159
var DefaultConfig = Config{
	Checks: []string{"all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023"},
	Initialisms: []string{
		"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", "SIP", "RTP", "AMQP", "DB", "TS",
	},
	DotImportWhitelist:      []string{},
	HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"},
}

蛋疼的地方来了, golangci-lint 为了兼容性, 只要发现 checks 为空, 就会将 checks 设置成 * ,注意这个 *, 并 不是 新版本的 staticcheck (这里指的是cmd, 不是package) 的默认值。

// pkg/golinters/staticcheck_common.go func staticCheckConfig
	if len(cfg.Checks) == 0 {
		cfg.Checks = append(cfg.Checks, "*") // override for compatibility reason. Must drop in the next major version.
	}

没错, staticcheck 的默认配置是符合大多数人习惯的, 但是 golangci-lint 基于兼容性考虑,无法应用默认配置

override for compatibility reason. Must drop in the next major version.

所以,作为使用者,符合习惯的做法应该是, 按 staticcheck 的默认配置,根据自己的需求自行调整。 当然,如果不调整,最简单的就是取 staticcheck 的默认配置了。

[linters-settings.stylecheck]
checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023" ]
dot-import-whitelist = []
http-status-code-whitelist = []

但是,这个配置在最新稳定版的 1.40.1 golangci-lint 并不生效,原因是,我们看的代码是 master 分支的。

1.40.1 golangci-lint 引用的是 honnef.co/go/tools v0.1.4 实际上是已经支持 checks 选项的,但是 golangci-lint 的代码没有加。

最初 4 兄弟在 golangci-lint 只有一个 go 版本的配置项,还是这个 PR 引入的: https://github.com/golangci/golangci-lint/pull/1946/files

直到这个 PR https://github.com/golangci/golangci-lint/pull/2017

ldez committed 4 days ago (当前写作日期: 2021-06-01)

对应的提交 https://github.com/golangci/golangci-lint/commit/b916c9318bf31c6dca73e64c1751bd62a661e2bb

这个 PR 同时还用一句话讲清楚了 golangci-lint 官方一直不愿意讲的事情:

Staticcheck is split into 4 linters in golangci-lint staticcheck, gosimple, stylecheck and unused.

相关问题: https://github.com/golangci/golangci-lint/discussions/1989#discussioncomment-745004

If do so, why it is there 4 times?

staticcheck is a binary and a set of rules, those rules are grouped in "category": staticcheck, gosimple, stylecheck and unused.

megacheck was the first name of staticcheck, the "categories" were binaries, and as you can understand there are historical reasons behind those "linters" inside golangci-lint.

More information about the 4 linters: https://github.com/golangci/golangci-lint/issues/357


And why actually the staticcheck config file staticcheck.conf is ignored when running with golangci-lint?

The file is partially ignored: you can use staticcheck.conf to define initialisms, dot_import_whitelist, http_status_code_whitelist but checks are ignored (instead you have to use issues.excludes in the golanci-lint configuration).

It is mainly related to the API of staticcheck: we don't have access to the element to handle the staticcheck configuration.

About the configuration, you can follow https://github.com/golangci/golangci-lint/issues/1275

新版本的 golangci-lint 里面有这么个方法(实际上基本上是copy自dominikh/go-tools的):


// 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
}

filterAnalyzerNames() 实际上会被 setupStaticCheckAnalyzers() 调用, 其实只是在 filterAnalyzerNames 的基础上, 针对被启用的 Analyzer 设置了一下go version (setAnalyzerGoVersion) :

func setupStaticCheckAnalyzers(src []*lint.Analyzer, goVersion string, checks []string) []*analysis.Analyzer {
	var names []string
	for _, a := range src {
		names = append(names, a.Analyzer.Name)
	}

	filter := filterAnalyzerNames(names, checks)

	var ret []*analysis.Analyzer
	for _, a := range src {
		if filter[a.Analyzer.Name] {
			setAnalyzerGoVersion(a.Analyzer, goVersion)
			ret = append(ret, a.Analyzer)
		}
	}

	return ret
}

其后我们看下那几个 new 都差不多, 都会调用 setupStaticCheckAnalyzers():

func NewStaticcheck(settings *config.StaticCheckSettings) *goanalysis.Linter {
	cfg := staticCheckConfig(settings)

	analyzers := setupStaticCheckAnalyzers(staticcheck.Analyzers, getGoVersion(settings), cfg.Checks)

	return goanalysis.NewLinter(
		"staticcheck",
		"Staticcheck is a go vet on steroids, applying a ton of static analysis checks",
		analyzers,
		nil,
	).WithLoadMode(goanalysis.LoadModeTypesInfo)
}
func NewStylecheck(settings *config.StaticCheckSettings) *goanalysis.Linter {
	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",
		"Stylecheck is a replacement for golint",
		analyzers,
		nil,
	).WithLoadMode(goanalysis.LoadModeTypesInfo)
}

https://github.com/golangci/golangci-lint/commit/7705f82591bdc71500802ce1b0fee4ef6726bbcc

Update megacheck to the latest version Also do following improvements:

  • show proper sublinter name for megacheck sublinters
  • refactor and make more simple and robust megacheck merging/optimizing
  • improve handling of unknown linter names in //nolint directives
  • minimize diff of our megacheck version from the upstream, golang/go#29612 blocks usage of the upstream version
  • support the new stylecheck linter
  • improve tests coverage for megacheck and nolint related cases
  • update and use upstream versions of unparam and interfacer instead of forked ones
  • don't use golangci/tools repo anymore
  • fix newly found issues after updating linters

Also should be noted that megacheck works much faster and consumes less memory in the newest release, therefore golangci-lint works noticeably faster and consumes less memory for large repos.

Relates: #314 master (#352) v1.40.1 v1.40.0 v1.39.0 v1.38.0 v1.37.1 v1.37.0 v1.36.0 v1.35.2 v1.35.1 v1.35.0 v1.34.1 v1.34.0 v1.33.2 v1.33.1 v1.33.0 v1.32.2 v1.32.1 v1.32.0 v1.31.0 v1.30.0 v1.29.0 v1.28.3 v1.28.2 v1.28.1 v1.28.0 v1.27.0 v1.26.0 v1.25.1 v1.25.0 v1.24.0 v1.23.8 v1.23.7 v1.23.6 v1.23.5 v1.23.4 v1.23.3 v1.23.2 v1.23.1 v1.23.0 v1.22.2 v1.22.1 v1.22.0 v1.21.0 v1.20.1 v1.20.0 v1.19.1 v1.19.0 v1.18.0 v1.17.1 v1.17.0 v1.16.0 v1.15.0 v1.14.0 v1.13.2 v1.13.1 v1.13

总结

  1. 在 golangci-lint 当前版本 (v1.40.1)里, megacheck = [gosimple staticcheck unused]

  2. staticcheck = staticcheck package (而不是四合一的 当前真正的 staticcheck cmd的功能)

  3. stylecheck 目前我们还没办法启用,因为当前无法给它配置需要禁用的一些规则. 只能等下一个 golangci-lint 版本 (如果它把上面我说到的代码合并了)

如是 当前 master 分支的代码被合并了,那么是可以使用以下配置的:

linters-settings:
  stylecheck:
    checks:
      - all
      - '-ST1000'
      - '-ST1003'
      - '-ST1016'
      - '-ST1020'
      - '-ST1021'
      - '-ST1022'
      - '-ST1023'
    dot-import-whitelist: []
    http-status-code-whitelist: []
  1. 我们期待 golangci-lint 新的 2.0 版本里将真正的 四合一版本的 staticcheck 集成进来,这样就没有必要像现在这样,针对同一个 linter 的同样的配置要写四遍了。

Refs

https://staticcheck.io/

https://github.com/golangci/golangci-lint

https://golangci-lint.run/