feat: add spancheck linter (#4290)

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
Joshua Timmons 2024-01-02 22:24:30 -05:00 committed by GitHub
parent 0fdf33aaaa
commit d23c35470b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 402 additions and 1 deletions

View File

@ -1912,6 +1912,24 @@ linters-settings:
# Default: false # Default: false
args-on-sep-lines: true args-on-sep-lines: true
spancheck:
# Checks to enable.
# Options include:
# - `end`: check that `span.End()` is called
# - `record-error`: check that `span.RecordError(err)` is called when an error is returned
# - `set-status`: check that `span.SetStatus(codes.Error, msg)` is called when an error is returned
# Default: ["end"]
checks:
- end
- record-error
- set-status
# A list of regexes for function signatures that silence `record-error` and `set-status` reports
# if found in the call path to a returned error.
# https://github.com/jjti/go-spancheck#ignore-check-signatures
# Default: []
ignore-check-signatures:
- "telemetry.RecordError"
staticcheck: staticcheck:
# Deprecated: use the global `run.go` instead. # Deprecated: use the global `run.go` instead.
go: "1.15" go: "1.15"
@ -2442,6 +2460,7 @@ linters:
- rowserrcheck - rowserrcheck
- scopelint - scopelint
- sloglint - sloglint
- spancheck
- sqlclosecheck - sqlclosecheck
- staticcheck - staticcheck
- structcheck - structcheck
@ -2562,6 +2581,7 @@ linters:
- rowserrcheck - rowserrcheck
- scopelint - scopelint
- sloglint - sloglint
- spancheck
- sqlclosecheck - sqlclosecheck
- staticcheck - staticcheck
- structcheck - structcheck

1
go.mod
View File

@ -57,6 +57,7 @@ require (
github.com/jgautheron/goconst v1.7.0 github.com/jgautheron/goconst v1.7.0
github.com/jingyugao/rowserrcheck v1.1.1 github.com/jingyugao/rowserrcheck v1.1.1
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af
github.com/jjti/go-spancheck v0.4.2
github.com/julz/importas v0.1.0 github.com/julz/importas v0.1.0
github.com/kisielk/errcheck v1.6.3 github.com/kisielk/errcheck v1.6.3
github.com/kkHAIKE/contextcheck v1.1.4 github.com/kkHAIKE/contextcheck v1.1.4

2
go.sum generated
View File

@ -310,6 +310,8 @@ github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjz
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48=
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
github.com/jjti/go-spancheck v0.4.2 h1:4MvJOTKRi9ClsPNTxVhHvcSyuInILLSKmstTCJ/oIZI=
github.com/jjti/go-spancheck v0.4.2/go.mod h1:TBZ1nIcHTtCnBChHcjd+5agCxHBaW0tzw9quzCCNAts=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=

View File

@ -245,13 +245,14 @@ type LintersSettings struct {
Revive ReviveSettings Revive ReviveSettings
RowsErrCheck RowsErrCheckSettings RowsErrCheck RowsErrCheckSettings
SlogLint SlogLintSettings SlogLint SlogLintSettings
Spancheck SpancheckSettings
Staticcheck StaticCheckSettings Staticcheck StaticCheckSettings
Structcheck StructCheckSettings Structcheck StructCheckSettings
Stylecheck StaticCheckSettings Stylecheck StaticCheckSettings
TagAlign TagAlignSettings TagAlign TagAlignSettings
Tagliatelle TagliatelleSettings Tagliatelle TagliatelleSettings
Testifylint TestifylintSettings
Tenv TenvSettings Tenv TenvSettings
Testifylint TestifylintSettings
Testpackage TestpackageSettings Testpackage TestpackageSettings
Thelper ThelperSettings Thelper ThelperSettings
Unparam UnparamSettings Unparam UnparamSettings
@ -773,6 +774,11 @@ type SlogLintSettings struct {
ArgsOnSepLines bool `mapstructure:"args-on-sep-lines"` ArgsOnSepLines bool `mapstructure:"args-on-sep-lines"`
} }
type SpancheckSettings struct {
Checks []string `mapstructure:"checks"`
IgnoreCheckSignatures []string `mapstructure:"ignore-check-signatures"`
}
type StaticCheckSettings struct { type StaticCheckSettings struct {
// Deprecated: use the global `run.go` instead. // Deprecated: use the global `run.go` instead.
GoVersion string `mapstructure:"go"` GoVersion string `mapstructure:"go"`

View File

@ -0,0 +1,29 @@
package golinters
import (
"github.com/jjti/go-spancheck"
"golang.org/x/tools/go/analysis"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
)
func NewSpancheck(settings *config.SpancheckSettings) *goanalysis.Linter {
cfg := spancheck.NewDefaultConfig()
if settings != nil {
if settings.Checks != nil {
cfg.EnabledChecks = settings.Checks
}
if settings.IgnoreCheckSignatures != nil {
cfg.IgnoreChecksSignaturesSlice = settings.IgnoreCheckSignatures
}
}
a := spancheck.NewAnalyzerWithConfig(cfg)
return goanalysis.
NewLinter(a.Name, a.Doc, []*analysis.Analyzer{a}, nil).
WithLoadMode(goanalysis.LoadModeTypesInfo)
}

View File

@ -131,6 +131,7 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
reviveCfg *config.ReviveSettings reviveCfg *config.ReviveSettings
rowserrcheckCfg *config.RowsErrCheckSettings rowserrcheckCfg *config.RowsErrCheckSettings
sloglintCfg *config.SlogLintSettings sloglintCfg *config.SlogLintSettings
spancheckCfg *config.SpancheckSettings
staticcheckCfg *config.StaticCheckSettings staticcheckCfg *config.StaticCheckSettings
structcheckCfg *config.StructCheckSettings structcheckCfg *config.StructCheckSettings
stylecheckCfg *config.StaticCheckSettings stylecheckCfg *config.StaticCheckSettings
@ -216,6 +217,7 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
reviveCfg = &m.cfg.LintersSettings.Revive reviveCfg = &m.cfg.LintersSettings.Revive
rowserrcheckCfg = &m.cfg.LintersSettings.RowsErrCheck rowserrcheckCfg = &m.cfg.LintersSettings.RowsErrCheck
sloglintCfg = &m.cfg.LintersSettings.SlogLint sloglintCfg = &m.cfg.LintersSettings.SlogLint
spancheckCfg = &m.cfg.LintersSettings.Spancheck
staticcheckCfg = &m.cfg.LintersSettings.Staticcheck staticcheckCfg = &m.cfg.LintersSettings.Staticcheck
structcheckCfg = &m.cfg.LintersSettings.Structcheck structcheckCfg = &m.cfg.LintersSettings.Structcheck
stylecheckCfg = &m.cfg.LintersSettings.Stylecheck stylecheckCfg = &m.cfg.LintersSettings.Stylecheck
@ -782,6 +784,12 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
WithLoadForGoAnalysis(). WithLoadForGoAnalysis().
WithURL("https://github.com/ryanrolds/sqlclosecheck"), WithURL("https://github.com/ryanrolds/sqlclosecheck"),
linter.NewConfig(golinters.NewSpancheck(spancheckCfg)).
WithSince("v1.56.0").
WithLoadForGoAnalysis().
WithPresets(linter.PresetBugs).
WithURL("https://github.com/jjti/go-spancheck"),
linter.NewConfig(golinters.NewStaticcheck(staticcheckCfg)). linter.NewConfig(golinters.NewStaticcheck(staticcheckCfg)).
WithEnabledByDefault(). WithEnabledByDefault().
WithSince("v1.0.0"). WithSince("v1.0.0").

View File

@ -33,6 +33,7 @@ func TestSourcesFromTestdataSubDir(t *testing.T) {
"ginkgolinter", "ginkgolinter",
"zerologlint", "zerologlint",
"protogetter", "protogetter",
"spancheck",
} }
for _, dir := range subDirs { for _, dir := range subDirs {

View File

@ -0,0 +1,8 @@
linters-settings:
spancheck:
checks:
- "end"
- "record-error"
- "set-status"
ignore-check-signatures:
- "recordErr"

14
test/testdata/spancheck/go.mod vendored Normal file
View File

@ -0,0 +1,14 @@
module spancheck
go 1.20
require (
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel/trace v1.21.0
)
require (
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
)

16
test/testdata/spancheck/go.sum generated vendored Normal file
View File

@ -0,0 +1,16 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -0,0 +1,90 @@
//golangcitest:args -Espancheck
package spancheck
import (
"context"
"errors"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
)
type testDefaultError struct{}
func (e *testDefaultError) Error() string {
return "foo"
}
// incorrect
func _() {
otel.Tracer("foo").Start(context.Background(), "bar") // want "span is unassigned, probable memory leak"
ctx, _ := otel.Tracer("foo").Start(context.Background(), "bar") // want "span is unassigned, probable memory leak"
fmt.Print(ctx)
}
func _() {
ctx, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.End is not called on all paths, possible memory leak"
print(ctx.Done(), span.IsRecording())
} // want "return can be reached without calling span.End"
func _() {
var ctx, span = otel.Tracer("foo").Start(context.Background(), "bar") // want "span.End is not called on all paths, possible memory leak"
print(ctx.Done(), span.IsRecording())
} // want "return can be reached without calling span.End"
func _() {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.End is not called on all paths, possible memory leak"
_, span = otel.Tracer("foo").Start(context.Background(), "bar")
fmt.Print(span)
defer span.End()
} // want "return can be reached without calling span.End"
// correct
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
return nil
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
if true {
return nil
}
return nil
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
if false {
err := errors.New("foo")
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
return err
}
if true {
span.SetStatus(codes.Error, "foo")
span.RecordError(errors.New("foo"))
return errors.New("bar")
}
return nil
}
func _() {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
_, span = otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
}

View File

@ -0,0 +1,206 @@
//golangcitest:config_path configs/enable_all.yml
//golangcitest:args -Espancheck
package spancheck
import (
"context"
"errors"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
type testError struct{}
func (e *testError) Error() string {
return "foo"
}
// incorrect
func _() {
otel.Tracer("foo").Start(context.Background(), "bar") // want "span is unassigned, probable memory leak"
ctx, _ := otel.Tracer("foo").Start(context.Background(), "bar") // want "span is unassigned, probable memory leak"
fmt.Print(ctx)
}
func _() {
ctx, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.End is not called on all paths, possible memory leak"
print(ctx.Done(), span.IsRecording())
} // want "return can be reached without calling span.End"
func _() {
var ctx, span = otel.Tracer("foo").Start(context.Background(), "bar") // want "span.End is not called on all paths, possible memory leak"
print(ctx.Done(), span.IsRecording())
} // want "return can be reached without calling span.End"
func _() {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.End is not called on all paths, possible memory leak"
_, span = otel.Tracer("foo").Start(context.Background(), "bar")
fmt.Print(span)
defer span.End()
} // want "return can be reached without calling span.End"
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.SetStatus is not called on all paths"
defer span.End()
if true {
err := errors.New("foo")
span.RecordError(err)
return err // want "return can be reached without calling span.SetStatus"
}
return nil
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.SetStatus is not called on all paths"
defer span.End()
if true {
span.RecordError(errors.New("foo"))
return errors.New("foo") // want "return can be reached without calling span.SetStatus"
}
return nil
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.SetStatus is not called on all paths"
defer span.End()
if true {
span.RecordError(errors.New("foo"))
return &testError{} // want "return can be reached without calling span.SetStatus"
}
return nil
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.RecordError is not called on all paths"
defer span.End()
if true {
span.SetStatus(codes.Error, "foo")
return &testError{} // want "return can be reached without calling span.RecordError"
}
return nil
}
func _() (string, error) {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.SetStatus is not called on all paths"
defer span.End()
if true {
span.RecordError(errors.New("foo"))
return "", &testError{} // want "return can be reached without calling span.SetStatus"
}
return "", nil
}
func _() (string, error) {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.SetStatus is not called on all paths"
defer span.End()
if true {
span.RecordError(errors.New("foo"))
return "", errors.New("foo") // want "return can be reached without calling span.SetStatus"
}
return "", nil
}
func _() {
f := func() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.SetStatus is not called on all paths"
defer span.End()
if true {
span.RecordError(errors.New("foo"))
return errors.New("foo") // want "return can be reached without calling span.SetStatus"
}
return nil
}
fmt.Println(f)
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar") // want "span.SetStatus is not called on all paths"
defer span.End()
{
if true {
span.RecordError(errors.New("foo"))
return errors.New("foo") // want "return can be reached without calling span.SetStatus"
}
}
return nil
}
// correct
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
return nil
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
if true {
return nil
}
return nil
}
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
if false {
err := errors.New("foo")
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
return err
}
if true {
span.SetStatus(codes.Error, "foo")
span.RecordError(errors.New("foo"))
return errors.New("bar")
}
return nil
}
func _() {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
_, span = otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
}
// ignore error because of matching func sig
func _() error {
_, span := otel.Tracer("foo").Start(context.Background(), "bar")
defer span.End()
err := errors.New("foo")
recordError(span, err)
return err
}
func recordError(span trace.Span, err error) {}