diff options
| author | dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> | 2024-03-04 17:40:11 +0000 |
|---|---|---|
| committer | Taras Madan <tarasmadan@google.com> | 2024-03-04 18:34:55 +0000 |
| commit | 5fc5366972c874b919f93165bb4ed4e2bcb7c350 (patch) | |
| tree | 287c3361a0dee0c72af80d9a1a66714a06e98a62 /vendor/github.com/jjti | |
| parent | 1be5ce38a9059c356eb193a8c34d60d61c9fc31f (diff) | |
mod: bump github.com/golangci/golangci-lint from 1.55.2 to 1.56.2
Bumps [github.com/golangci/golangci-lint](https://github.com/golangci/golangci-lint) from 1.55.2 to 1.56.2.
- [Release notes](https://github.com/golangci/golangci-lint/releases)
- [Changelog](https://github.com/golangci/golangci-lint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/golangci/golangci-lint/compare/v1.55.2...v1.56.2)
---
updated-dependencies:
- dependency-name: github.com/golangci/golangci-lint
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
Diffstat (limited to 'vendor/github.com/jjti')
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/.gitignore | 19 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/.golangci.yml | 103 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/CONTRIBUTING.md | 51 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/Makefile | 27 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/README.md | 210 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/config.go | 141 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/doc.go | 37 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/go.work | 8 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/go.work.sum | 3 | ||||
| -rw-r--r-- | vendor/github.com/jjti/go-spancheck/spancheck.go | 431 |
11 files changed, 1051 insertions, 0 deletions
diff --git a/vendor/github.com/jjti/go-spancheck/.gitignore b/vendor/github.com/jjti/go-spancheck/.gitignore new file mode 100644 index 000000000..1f83be414 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/.gitignore @@ -0,0 +1,19 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +src/ diff --git a/vendor/github.com/jjti/go-spancheck/.golangci.yml b/vendor/github.com/jjti/go-spancheck/.golangci.yml new file mode 100644 index 000000000..15d8513d6 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/.golangci.yml @@ -0,0 +1,103 @@ +## A good ref for this: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 + +run: + timeout: 5m + tests: true +linters: + enable: + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose + - containedctx + - decorder # checks declaration order and count of types, constants, variables and functions + - dogsled + - dupword # checks for duplicate words in the source code + - durationcheck # checks for two durations multiplied together + - errcheck + - errname + - errorlint + - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - gci + - gochecknoinits # checks that no init functions are present in Go code + - gocritic + - gomnd + - gosimple + - govet + - importas # enforces consistent import aliases + - ineffassign + - loggercheck + - makezero # finds slice declarations with non-zero initial length + - mirror + - misspell + - musttag # enforces field tags in (un)marshaled structs + - nakedret + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - noctx # finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - staticcheck + - stylecheck + - tenv + - thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - unused + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace +linters-settings: + gci: + skip-generated: true + custom-order: true + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/jjti) + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + gocritic: + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + govet: + enable-all: true + disable: + - fieldalignment # too strict + - shadow # bunch of false positive, doesn't realize when we return from a func + misspell: + locale: US + nakedret: + max-func-lines: 0 + nestif: + # Minimal complexity of if statements to report. + # Default: 5 + min-complexity: 4 + nolintlint: + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + stylecheck: + checks: ["all"] +issues: + include: + - EXC0001 # Error return value of x is not checked + - EXC0013 # package comment should be of the form "(.+)... + - EXC0014 # comment on exported (.+) should be of the form "(.+)..." + exclude: + - ifElseChain diff --git a/vendor/github.com/jjti/go-spancheck/CONTRIBUTING.md b/vendor/github.com/jjti/go-spancheck/CONTRIBUTING.md new file mode 100644 index 000000000..32932fae1 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing guideline + +Contributions are welcome + appreciated. + +## Open Requests + +These are a couple contributions I would especially appreciate: + +1. Add check for SetAttributes: https://github.com/jjti/go-spancheck/issues/1 +1. Add SuggestedFix(es): https://github.com/jjti/go-spancheck/issues/2 + +## Steps + +### 1. Create an Issue + +If one does not exist already, open a bug report or feature request in [https://github.com/jjti/go-spancheck/issues](https://github.com/jjti/go-spancheck/issues). + +### 2. Add a test case + +Test cases are in `/testdata`. + +If fixing a bug, you can add it to `testdata/enableall/enable_all.go` (for example): + +```go +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" +``` + +If adding a new feature with a new combination of flags, create a new module within `testdata`: + +1. Create a new module, eg `testdata/setattributes` +1. Copy/paste go.mod/sum into the new module directory and update the module definition, eg `module github.com/jjti/go-spancheck/testdata/setattributes` +1. Add the module to the workspace in [go.work](./go.work) +1. Add the module's directory to the `testvendor` Make target in [Makefile](./Makefile) + +### 3. Run tests + +```bash +make test +``` + +### 4. Open a PR + +Eg of a GitHub snippet for PRs: + +```bash +alias gpr='gh pr view --web 2>/dev/null || gh pr create --web --fill' +gpr +``` diff --git a/vendor/github.com/jjti/go-spancheck/LICENSE b/vendor/github.com/jjti/go-spancheck/LICENSE new file mode 100644 index 000000000..552ddf2dc --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Joshua Timmons + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/jjti/go-spancheck/Makefile b/vendor/github.com/jjti/go-spancheck/Makefile new file mode 100644 index 000000000..39d80f7c6 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/Makefile @@ -0,0 +1,27 @@ +.PHONY: fmt +fmt: + golangci-lint run --fix --config ./.golangci.yml + +.PHONY: test +test: testvendor + go test -v ./... + +# note: I'm copying https://github.com/ghostiam/protogetter/blob/main/testdata/Makefile +# +# x/tools/go/analysis/analysistest does not support go modules. To work around this issue +# we need to vendor any external modules to `./src`. +# +# Follow https://github.com/golang/go/issues/37054 for more details. +.PHONY: testvendor +testvendor: + @rm -rf base/src + @cd testdata/base && go mod vendor + @cp -r testdata/base/vendor testdata/base/src + @cp -r testdata/base/vendor testdata/disableerrorchecks/src + @cp -r testdata/base/vendor testdata/enableall/src + @rm -rf testdata/base/vendor + +.PHONY: install +install: + go install ./cmd/spancheck + @echo "Installed in $(shell which spancheck)"
\ No newline at end of file diff --git a/vendor/github.com/jjti/go-spancheck/README.md b/vendor/github.com/jjti/go-spancheck/README.md new file mode 100644 index 000000000..98aedec28 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/README.md @@ -0,0 +1,210 @@ +# go-spancheck + + +[](https://github.com/jjti/go-spancheck/actions/workflows/ci.yaml) +[](https://goreportcard.com/report/github.com/jjti/go-spancheck) +[](LICENSE) + +Checks usage of: + +- [OpenTelemetry spans](https://opentelemetry.io/docs/instrumentation/go/manual/) from [go.opentelemetry.io/otel/trace](go.opentelemetry.io/otel/trace) +- [OpenCensus spans](https://opencensus.io/quickstart/go/tracing/) from [go.opencensus.io/trace](https://pkg.go.dev/go.opencensus.io/trace#Span) + +## Installation & Usage + +```bash +go install github.com/jjti/go-spancheck/cmd/spancheck@latest +spancheck ./... +``` + +## Example + +```bash +spancheck -checks 'end,set-status,record-error' ./... +``` + +```go +func _() error { + // span.End is not called on all paths, possible memory leak + // span.SetStatus is not called on all paths + // span.RecordError is not called on all paths + _, span := otel.Tracer("foo").Start(context.Background(), "bar") + + if true { + // return can be reached without calling span.End + // return can be reached without calling span.SetStatus + // return can be reached without calling span.RecordError + return errors.New("err") + } + + return nil // return can be reached without calling span.End +} +``` + +## Configuration + +Only the `span.End()` check is enabled by default. The others can be enabled with `-checks 'end,set-status,record-error'`. + +```txt +$ spancheck -h +... +Flags: + -checks string + comma-separated list of checks to enable (options: end, set-status, record-error) (default "end") + -ignore-check-signatures string + comma-separated list of regex for function signatures that disable checks on errors +``` + +### Ignore check signatures + +The `span.SetStatus()` and `span.RecordError()` checks warn when there is: + +1. a path to return statement +1. that returns an error +1. without a call (to `SetStatus` or `RecordError`, respectively) + +But it's convenient to call `SetStatus` and `RecordError` from utility methods [[1](https://andydote.co.uk/2023/09/19/tracing-is-better/#step-2-wrap-the-errors)]. To support that, the `ignore-*-check-signatures` settings will suppress warnings if the configured function is present in the path. + +For example, by default, the code below would have warnings as shown: + +```go +func task(ctx context.Context) error { + ctx, span := otel.Tracer("foo").Start(ctx, "bar") // span.SetStatus is not called on all paths + defer span.End() + + if err := subTask(ctx); err != nil { + return recordErr(span, err) // return can be reached without calling span.SetStatus + } + + return nil +} + +func recordErr(span trace.Span, err error) error { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return err +} +``` + +The warnings are can be ignored by setting `-ignore-check-signatures` flag to `recordErr`: + +```bash +spancheck -checks 'end,set-status,record-error' -ignore-check-signatures 'recordErr' ./... +``` + +## Problem Statement + +Tracing is a celebrated [[1](https://andydote.co.uk/2023/09/19/tracing-is-better/),[2](https://charity.wtf/2022/08/15/live-your-best-life-with-structured-events/)] and well marketed [[3](https://docs.datadoghq.com/tracing/),[4](https://www.honeycomb.io/distributed-tracing)] pillar of observability. But self-instrumented tracing requires a lot of easy-to-forget boilerplate: + +```go +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" +) + +func task(ctx context.Context) error { + ctx, span := otel.Tracer("foo").Start(ctx, "bar") + defer span.End() // call `.End()` + + if err := subTask(ctx); err != nil { + span.SetStatus(codes.Error, err.Error()) // call SetStatus(codes.Error, msg) to set status:error + span.RecordError(err) // call RecordError(err) to record an error event + return err + } + + return nil +} +``` + +For spans to be _really_ useful, developers need to: + +1. call `span.End()` always +1. call `span.SetStatus(codes.Error, msg)` on error +1. call `span.RecordError(err)` on error +1. call `span.SetAttributes()` liberally + +- OpenTelemetry: [Creating spans](https://opentelemetry.io/docs/instrumentation/go/manual/#creating-spans) +- Uptrace: [OpenTelemetry Go Tracing API](https://uptrace.dev/opentelemetry/go-tracing.html#quickstart) + +This linter helps developers with steps 1-3. + +## Checks + +This linter supports three checks, each documented below. Only the check for `span.End()` is enabled by default. See [Configuration](#configuration) for instructions on enabling the others. + +### `span.End()` + +Enabled by default. + +Not calling `End` can cause memory leaks and prevents spans from being closed. + +> Any Span that is created MUST also be ended. This is the responsibility of the user. Implementations of this API may leak memory or other resources if Spans are not ended. + +[source: trace.go](https://github.com/open-telemetry/opentelemetry-go/blob/98b32a6c3a87fbee5d34c063b9096f416b250897/trace/trace.go#L523) + +```go +func task(ctx context.Context) error { + otel.Tracer("app").Start(ctx, "foo") // span is unassigned, probable memory leak + _, span := otel.Tracer().Start(ctx, "foo") // span.End is not called on all paths, possible memory leak + return nil // return can be reached without calling span.End +} +``` + +### `span.SetStatus(codes.Error, "msg")` + +Disabled by default. Enable with `-checks 'set-status'`. + +Developers should call `SetStatus` on spans. The status attribute is an important, first-class attribute: + +1. observability platforms and APMs differentiate "success" vs "failure" using [span's status codes](https://docs.datadoghq.com/tracing/metrics/). +1. telemetry collector agents, like the [Open Telemetry Collector's Tail Sampling Processor](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/tailsamplingprocessor/README.md#:~:text=Sampling%20Processor.-,status_code,-%3A%20Sample%20based%20upon), are configurable to sample `Error` spans at a higher rate than `OK` spans. +1. observability platforms, like [DataDog, have trace retention filters that use spans' status](https://docs.datadoghq.com/tracing/trace_pipeline/trace_retention/). In other words, `status:error` spans often receive special treatment with the assumption they are more useful for debugging. And forgetting to set the status can lead to spans, with useful debugging information, being dropped. + +```go +func _() error { + _, span := otel.Tracer("foo").Start(context.Background(), "bar") // span.SetStatus is not called on all paths + defer span.End() + + if err := subTask(); err != nil { + span.RecordError(err) + return errors.New(err) // return can be reached without calling span.SetStatus + } + + return nil +} +``` + +OpenTelemetry docs: [Set span status](https://opentelemetry.io/docs/instrumentation/go/manual/#set-span-status). + +### `span.RecordError(err)` + +Disabled by default. Enable with `-checks 'record-error'`. + +Calling `RecordError` creates a new exception-type [event (structured log message)](https://opentelemetry.io/docs/concepts/signals/traces/#span-events) on the span. This is recommended to capture the error's stack trace. + +```go +func _() error { + _, span := otel.Tracer("foo").Start(context.Background(), "bar") // span.RecordError is not called on all paths + defer span.End() + + if err := subTask(); err != nil { + span.SetStatus(codes.Error, err.Error()) + return errors.New(err) // return can be reached without calling span.RecordError + } + + return nil +} +``` + +OpenTelemetry docs: [Record errors](https://opentelemetry.io/docs/instrumentation/go/manual/#record-errors). + +Note: this check is not applied to [OpenCensus spans](https://pkg.go.dev/go.opencensus.io/trace#SpanInterface) because they have no `RecordError` method. + +## Attribution + +This linter is the product of liberal copying of: + +- [github.com/golang/tools/go/analysis/passes/lostcancel](https://github.com/golang/tools/tree/master/go/analysis/passes/lostcancel) (half the linter) +- [github.com/tomarrell/wrapcheck](https://github.com/tomarrell/wrapcheck) (error type checking and config) +- [github.com/Antonboom/testifylint](https://github.com/Antonboom/testifylint) (README) +- [github.com/ghostiam/protogetter](https://github.com/ghostiam/protogetter/blob/main/testdata/Makefile) (test setup) diff --git a/vendor/github.com/jjti/go-spancheck/config.go b/vendor/github.com/jjti/go-spancheck/config.go new file mode 100644 index 000000000..4005f49e0 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/config.go @@ -0,0 +1,141 @@ +package spancheck + +import ( + "flag" + "fmt" + "log" + "regexp" + "strings" +) + +// Check is a type of check that can be enabled or disabled. +type Check int + +const ( + // EndCheck if enabled, checks that span.End() is called after span creation and before the function returns. + EndCheck Check = iota + + // SetStatusCheck if enabled, checks that `span.SetStatus(codes.Error, msg)` is called when returning an error. + SetStatusCheck + + // RecordErrorCheck if enabled, checks that span.RecordError(err) is called when returning an error. + RecordErrorCheck +) + +func (c Check) String() string { + switch c { + case EndCheck: + return "end" + case SetStatusCheck: + return "set-status" + case RecordErrorCheck: + return "record-error" + default: + return "" + } +} + +var ( + // Checks is a list of all checks by name. + Checks = map[string]Check{ + EndCheck.String(): EndCheck, + SetStatusCheck.String(): SetStatusCheck, + RecordErrorCheck.String(): RecordErrorCheck, + } +) + +// Config is a configuration for the spancheck analyzer. +type Config struct { + fs flag.FlagSet + + // EnabledChecks is a list of checks to enable by name. + EnabledChecks []string + + // IgnoreChecksSignaturesSlice is a slice of strings that are turned into + // the IgnoreSetStatusCheckSignatures regex. + IgnoreChecksSignaturesSlice []string + + endCheckEnabled bool + setStatusEnabled bool + recordErrorEnabled bool + + // ignoreChecksSignatures is a regex that, if matched, disables the + // SetStatus and RecordError checks on error. + ignoreChecksSignatures *regexp.Regexp +} + +// NewDefaultConfig returns a new Config with default values. +func NewDefaultConfig() *Config { + return &Config{ + EnabledChecks: []string{EndCheck.String()}, + } +} + +// finalize parses checks and signatures from the public string slices of Config. +func (c *Config) finalize() { + c.parseSignatures() + + checks := parseChecks(c.EnabledChecks) + c.endCheckEnabled = contains(checks, EndCheck) + c.setStatusEnabled = contains(checks, SetStatusCheck) + c.recordErrorEnabled = contains(checks, RecordErrorCheck) +} + +// parseSignatures sets the Ignore*CheckSignatures regex from the string slices. +func (c *Config) parseSignatures() { + if c.ignoreChecksSignatures == nil && len(c.IgnoreChecksSignaturesSlice) > 0 { + if len(c.IgnoreChecksSignaturesSlice) == 1 && c.IgnoreChecksSignaturesSlice[0] == "" { + return + } + + c.ignoreChecksSignatures = createRegex(c.IgnoreChecksSignaturesSlice) + } +} + +func parseChecks(checksSlice []string) []Check { + if len(checksSlice) == 0 { + return nil + } + + checks := []Check{} + for _, check := range checksSlice { + checkName := strings.TrimSpace(check) + if checkName == "" { + continue + } + + check, ok := Checks[checkName] + if !ok { + continue + } + + checks = append(checks, check) + } + + return checks +} + +func createRegex(sigs []string) *regexp.Regexp { + if len(sigs) == 0 { + return nil + } + + regex := fmt.Sprintf("(%s)", strings.Join(sigs, "|")) + regexCompiled, err := regexp.Compile(regex) + if err != nil { + log.Default().Print("[WARN] failed to compile regex from signature flag", "regex", regex, "err", err) + return nil + } + + return regexCompiled +} + +func contains(s []Check, e Check) bool { + for _, a := range s { + if a == e { + return true + } + } + + return false +} diff --git a/vendor/github.com/jjti/go-spancheck/doc.go b/vendor/github.com/jjti/go-spancheck/doc.go new file mode 100644 index 000000000..f9dec043f --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/doc.go @@ -0,0 +1,37 @@ +// Package spancheck defines a linter that checks for mistakes with OTEL trace spans. +// +// # Analyzer spancheck +// +// spancheck: check for mistakes with OpenTelemetry trace spans. +// +// Common mistakes with OTEL trace spans include forgetting to call End: +// +// func(ctx context.Context) { +// ctx, span := otel.Tracer("app").Start(ctx, "span") +// // defer span.End() should be here +// +// // do stuff +// } +// +// Forgetting to set an Error status: +// +// ctx, span := otel.Tracer("app").Start(ctx, "span") +// defer span.End() +// +// if err := task(); err != nil { +// // span.SetStatus(codes.Error, err.Error()) should be here +// span.RecordError(err) +// return fmt.Errorf("failed to run task: %w", err) +// } +// +// Forgetting to record the Error: +// +// ctx, span := otel.Tracer("app").Start(ctx, "span") +// defer span.End() +// +// if err := task(); err != nil { +// span.SetStatus(codes.Error, err.Error()) +// // span.RecordError(err) should be here +// return fmt.Errorf("failed to run task: %w", err) +// } +package spancheck diff --git a/vendor/github.com/jjti/go-spancheck/go.work b/vendor/github.com/jjti/go-spancheck/go.work new file mode 100644 index 000000000..7d0a87b9e --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/go.work @@ -0,0 +1,8 @@ +go 1.20 + +use ( + . + ./testdata/base + ./testdata/disableerrorchecks + ./testdata/enableall +) diff --git a/vendor/github.com/jjti/go-spancheck/go.work.sum b/vendor/github.com/jjti/go-spancheck/go.work.sum new file mode 100644 index 000000000..f3cdef790 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/go.work.sum @@ -0,0 +1,3 @@ +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= diff --git a/vendor/github.com/jjti/go-spancheck/spancheck.go b/vendor/github.com/jjti/go-spancheck/spancheck.go new file mode 100644 index 000000000..ebfc1ac68 --- /dev/null +++ b/vendor/github.com/jjti/go-spancheck/spancheck.go @@ -0,0 +1,431 @@ +package spancheck + +import ( + "go/ast" + "go/types" + "log" + "regexp" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/ctrlflow" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/cfg" +) + +const stackLen = 32 + +// spanType differentiates span types. +type spanType int + +const ( + spanUnset spanType = iota // not a span + spanOpenTelemetry // from go.opentelemetry.io/otel + spanOpenCensus // from go.opencensus.io/trace +) + +var ( + // this approach stolen from errcheck + // https://github.com/kisielk/errcheck/blob/7f94c385d0116ccc421fbb4709e4a484d98325ee/errcheck/errcheck.go#L22 + errorType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) +) + +// NewAnalyzerWithConfig returns a new analyzer configured with the Config passed in. +// Its config can be set for testing. +func NewAnalyzerWithConfig(config *Config) *analysis.Analyzer { + return newAnalyzer(config) +} + +func newAnalyzer(config *Config) *analysis.Analyzer { + config.finalize() + + return &analysis.Analyzer{ + Name: "spancheck", + Doc: "Checks for mistakes with OpenTelemetry/Census spans.", + Flags: config.fs, + Run: run(config), + Requires: []*analysis.Analyzer{ + ctrlflow.Analyzer, + inspect.Analyzer, + }, + } +} + +func run(config *Config) func(*analysis.Pass) (interface{}, error) { + return func(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.FuncLit)(nil), // f := func() {} + (*ast.FuncDecl)(nil), // func foo() {} + } + inspect.Preorder(nodeFilter, func(n ast.Node) { + runFunc(pass, n, config) + }) + + return nil, nil + } +} + +type spanVar struct { + stmt ast.Node + id *ast.Ident + vr *types.Var + spanType spanType +} + +// runFunc checks if the node is a function, has a span, and the span never has SetStatus set. +func runFunc(pass *analysis.Pass, node ast.Node, config *Config) { + // copying https://cs.opensource.google/go/x/tools/+/master:go/analysis/passes/lostcancel/lostcancel.go + + // Find scope of function node + var funcScope *types.Scope + switch v := node.(type) { + case *ast.FuncLit: + funcScope = pass.TypesInfo.Scopes[v.Type] + case *ast.FuncDecl: + funcScope = pass.TypesInfo.Scopes[v.Type] + } + + // Maps each span variable to its defining ValueSpec/AssignStmt. + spanVars := make(map[*ast.Ident]spanVar) + + // Find the set of span vars to analyze. + stack := make([]ast.Node, 0, stackLen) + ast.Inspect(node, func(n ast.Node) bool { + switch n.(type) { + case *ast.FuncLit: + if len(stack) > 0 { + return false // don't stray into nested functions + } + case nil: + stack = stack[:len(stack)-1] // pop + return true + } + stack = append(stack, n) // push + + // Look for [{AssignStmt,ValueSpec} CallExpr SelectorExpr]: + // + // ctx, span := otel.Tracer("app").Start(...) + // ctx, span = otel.Tracer("app").Start(...) + // var ctx, span = otel.Tracer("app").Start(...) + sType, sStart := isSpanStart(pass.TypesInfo, n) + if !sStart || !isCall(stack[len(stack)-2]) { + return true + } + + stmt := stack[len(stack)-3] + id := getID(stmt) + if id == nil { + pass.ReportRangef(n, "span is unassigned, probable memory leak") + return true + } + + if id.Name == "_" { + pass.ReportRangef(id, "span is unassigned, probable memory leak") + } else if v, ok := pass.TypesInfo.Uses[id].(*types.Var); ok { + // If the span variable is defined outside function scope, + // do not analyze it. + if funcScope.Contains(v.Pos()) { + spanVars[id] = spanVar{ + vr: v, + stmt: stmt, + id: id, + spanType: sType, + } + } + } else if v, ok := pass.TypesInfo.Defs[id].(*types.Var); ok { + spanVars[id] = spanVar{ + vr: v, + stmt: stmt, + id: id, + spanType: sType, + } + } + + return true + }) + + if len(spanVars) == 0 { + return // no need to inspect CFG + } + + // Obtain the CFG. + cfgs := pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs) + var g *cfg.CFG + var sig *types.Signature + switch node := node.(type) { + case *ast.FuncDecl: + sig, _ = pass.TypesInfo.Defs[node.Name].Type().(*types.Signature) + g = cfgs.FuncDecl(node) + case *ast.FuncLit: + sig, _ = pass.TypesInfo.Types[node.Type].Type.(*types.Signature) + g = cfgs.FuncLit(node) + } + if sig == nil { + return // missing type information + } + + // Check for missing calls. + for _, sv := range spanVars { + if config.endCheckEnabled { + // Check if there's no End to the span. + if ret := missingSpanCalls(pass, g, sv, "End", func(pass *analysis.Pass, ret *ast.ReturnStmt) *ast.ReturnStmt { return ret }, nil); ret != nil { + pass.ReportRangef(sv.stmt, "%s.End is not called on all paths, possible memory leak", sv.vr.Name()) + pass.ReportRangef(ret, "return can be reached without calling %s.End", sv.vr.Name()) + } + } + + if config.setStatusEnabled { + // Check if there's no SetStatus to the span setting an error. + if ret := missingSpanCalls(pass, g, sv, "SetStatus", returnsErr, config.ignoreChecksSignatures); ret != nil { + pass.ReportRangef(sv.stmt, "%s.SetStatus is not called on all paths", sv.vr.Name()) + pass.ReportRangef(ret, "return can be reached without calling %s.SetStatus", sv.vr.Name()) + } + } + + if config.recordErrorEnabled && sv.spanType == spanOpenTelemetry { // RecordError only exists in OpenTelemetry + // Check if there's no RecordError to the span setting an error. + if ret := missingSpanCalls(pass, g, sv, "RecordError", returnsErr, config.ignoreChecksSignatures); ret != nil { + pass.ReportRangef(sv.stmt, "%s.RecordError is not called on all paths", sv.vr.Name()) + pass.ReportRangef(ret, "return can be reached without calling %s.RecordError", sv.vr.Name()) + } + } + } +} + +// isSpanStart reports whether n is tracer.Start() +func isSpanStart(info *types.Info, n ast.Node) (spanType, bool) { + sel, ok := n.(*ast.SelectorExpr) + if !ok { + return spanUnset, false + } + + switch sel.Sel.Name { + case "Start": // https://github.com/open-telemetry/opentelemetry-go/blob/98b32a6c3a87fbee5d34c063b9096f416b250897/trace/trace.go#L523 + obj, ok := info.Uses[sel.Sel] + return spanOpenTelemetry, ok && obj.Pkg().Path() == "go.opentelemetry.io/otel/trace" + case "StartSpan": // https://pkg.go.dev/go.opencensus.io/trace#StartSpan + obj, ok := info.Uses[sel.Sel] + return spanOpenCensus, ok && obj.Pkg().Path() == "go.opencensus.io/trace" + case "StartSpanWithRemoteParent": // https://github.com/census-instrumentation/opencensus-go/blob/v0.24.0/trace/trace_api.go#L66 + obj, ok := info.Uses[sel.Sel] + return spanOpenCensus, ok && obj.Pkg().Path() == "go.opencensus.io/trace" + default: + return spanUnset, false + } +} + +func isCall(n ast.Node) bool { + _, ok := n.(*ast.CallExpr) + return ok +} + +func getID(node ast.Node) *ast.Ident { + switch stmt := node.(type) { + case *ast.ValueSpec: + if len(stmt.Names) > 1 { + return stmt.Names[1] + } + case *ast.AssignStmt: + if len(stmt.Lhs) > 1 { + id, _ := stmt.Lhs[1].(*ast.Ident) + return id + } + } + return nil +} + +// missingSpanCalls finds a path through the CFG, from stmt (which defines +// the 'span' variable v) to a return statement, that doesn't call the passed selector on the span. +func missingSpanCalls( + pass *analysis.Pass, + g *cfg.CFG, + sv spanVar, + selName string, + checkErr func(pass *analysis.Pass, ret *ast.ReturnStmt) *ast.ReturnStmt, + ignoreCheckSig *regexp.Regexp, +) *ast.ReturnStmt { + // usesCall reports whether stmts contain a use of the selName call on variable v. + usesCall := func(pass *analysis.Pass, stmts []ast.Node) bool { + found, reAssigned := false, false + for _, subStmt := range stmts { + stack := []ast.Node{} + ast.Inspect(subStmt, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.FuncLit: + if len(stack) > 0 { + return false // don't stray into nested functions + } + case *ast.CallExpr: + if ident, ok := n.Fun.(*ast.Ident); ok { + fnSig := pass.TypesInfo.ObjectOf(ident).String() + if ignoreCheckSig != nil && ignoreCheckSig.MatchString(fnSig) { + found = true + return false + } + } + case nil: + stack = stack[:len(stack)-1] // pop + return true + } + stack = append(stack, n) // push + + // Check whether the span was assigned over top of its old value. + _, spanStart := isSpanStart(pass.TypesInfo, n) + if spanStart { + if id := getID(stack[len(stack)-3]); id != nil && id.Obj.Decl == sv.id.Obj.Decl { + reAssigned = true + return false + } + } + + if n, ok := n.(*ast.SelectorExpr); ok { + // Selector (End, SetStatus, RecordError) hit. + if n.Sel.Name == selName { + id, ok := n.X.(*ast.Ident) + found = ok && id.Obj.Decl == sv.id.Obj.Decl + } + + // Check if an ignore signature matches. + fnSig := pass.TypesInfo.ObjectOf(n.Sel).String() + if ignoreCheckSig != nil && ignoreCheckSig.MatchString(fnSig) { + found = true + } + } + + return !found + }) + } + return found && !reAssigned + } + + // blockUses computes "uses" for each block, caching the result. + memo := make(map[*cfg.Block]bool) + blockUses := func(pass *analysis.Pass, b *cfg.Block) bool { + res, ok := memo[b] + if !ok { + res = usesCall(pass, b.Nodes) + memo[b] = res + } + return res + } + + // Find the var's defining block in the CFG, + // plus the rest of the statements of that block. + var defBlock *cfg.Block + var rest []ast.Node +outer: + for _, b := range g.Blocks { + for i, n := range b.Nodes { + if n == sv.stmt { + defBlock = b + rest = b.Nodes[i+1:] + break outer + } + } + } + if defBlock == nil { + log.Default().Print("[ERROR] internal error: can't find defining block for span var") + } + + // Is the call "used" in the remainder of its defining block? + if usesCall(pass, rest) { + return nil + } + + // Does the defining block return without making the call? + if ret := defBlock.Return(); ret != nil { + return checkErr(pass, ret) + } + + // Search the CFG depth-first for a path, from defblock to a + // return block, in which v is never "used". + seen := make(map[*cfg.Block]bool) + var search func(blocks []*cfg.Block) *ast.ReturnStmt + search = func(blocks []*cfg.Block) *ast.ReturnStmt { + for _, b := range blocks { + if seen[b] { + continue + } + seen[b] = true + + // Prune the search if the block uses v. + if blockUses(pass, b) { + continue + } + + // Found path to return statement? + if ret := returnsErr(pass, b.Return()); ret != nil { + return ret // found + } + + // Recur + if ret := returnsErr(pass, search(b.Succs)); ret != nil { + return ret + } + } + return nil + } + + return search(defBlock.Succs) +} + +func returnsErr(pass *analysis.Pass, ret *ast.ReturnStmt) *ast.ReturnStmt { + if ret == nil { + return nil + } + + for _, r := range ret.Results { + if isErrorType(pass.TypesInfo.TypeOf(r)) { + return ret + } + + if r, ok := r.(*ast.CallExpr); ok { + for _, err := range errorsByArg(pass, r) { + if err { + return ret + } + } + } + } + + return nil +} + +// errorsByArg returns a slice s such that +// len(s) == number of return types of call +// s[i] == true iff return type at position i from left is an error type +// +// copied from https://github.com/kisielk/errcheck/blob/master/errcheck/errcheck.go +func errorsByArg(pass *analysis.Pass, call *ast.CallExpr) []bool { + switch t := pass.TypesInfo.Types[call].Type.(type) { + case *types.Named: + // Single return + return []bool{isErrorType(t)} + case *types.Pointer: + // Single return via pointer + return []bool{isErrorType(t)} + case *types.Tuple: + // Multiple returns + s := make([]bool, t.Len()) + for i := 0; i < t.Len(); i++ { + switch et := t.At(i).Type().(type) { + case *types.Named: + // Single return + s[i] = isErrorType(et) + case *types.Pointer: + // Single return via pointer + s[i] = isErrorType(et) + default: + s[i] = false + } + } + return s + } + return []bool{false} +} + +func isErrorType(t types.Type) bool { + return types.Implements(t, errorType) +} |
