diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f72b023..233b996 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -25,10 +25,11 @@ jobs: - uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} + - uses: jdx/mise-action@v4 - name: Build run: go build -v ./... - name: Test - run: make test + run: mise run test coverage: name: coverage permissions: diff --git a/.golangci.yml b/.golangci.yml index 50db9b9..98b56b4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,9 +8,12 @@ linters: - exhaustruct - wsl - noinlineerr + - gomodguard settings: cyclop: max-complexity: 20 + funlen: + lines: -1 formatters: enable: - goimports diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4b9212..49dd070 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,4 +18,5 @@ repos: name: go test ./... language: golang types_or: [go] - entry: make test + pass_filenames: false + entry: mise run test diff --git a/CLAUDE.md b/CLAUDE.md index 92e3eae..519d7cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,9 +11,9 @@ also vendored into `golangci-lint` (since v1.58.0). ## Commands -- Run tests: `make test` (`go test -race -v ./...`) +- Run tests: `mise run test` (`go test -race -v ./...`) - Run a single test: `go test -race -run TestSuggestedFixes ./pkg/analyzer/` -- Lint (runs pre-commit, incl. golangci-lint + tests): `make lint` +- Lint (runs pre-commit, incl. golangci-lint + tests): `mise run lint` - Build the CLI: `go build ./cmd/fatcontext` - Release: push a git tag (`git tag -a vX.Y.Z -m "vX.Y.Z" && git push --follow-tags`), which triggers GoReleaser via GitHub Actions. diff --git a/Makefile b/Makefile deleted file mode 100644 index efe0bd3..0000000 --- a/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -.PHONY: lint -lint: - pre-commit run --all-files - -.PHONY: test -test: - go test -race -v ./... diff --git a/README.md b/README.md index e1b8fde..fdc5e59 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,30 @@ func notOk() { } ``` +## Configuration + +The linter exposes the following options, all available on the command line: + +| Option | Default | Description | +|----------------------------|------------|--------------------------------------------------------------------------| +| `check-loops` | `true` | Detect fat contexts created inside loops. | +| `check-function-literals` | `true` | Detect fat contexts created inside function literals. | +| `check-struct-pointers` | `false` | Detect potential fat contexts created through struct pointers. | + +```bash +# Disable loop detection (enabled by default) +fatcontext -check-loops=false ./... + +# Disable function literal detection (enabled by default) +fatcontext -check-function-literals=false ./... + +# Enable struct pointer detection (disabled by default) +fatcontext -check-struct-pointers ./... +``` + +When used through `golangci-lint`, these options are configured in the linter's +settings rather than on the command line. + ## Development Setup pre-commit locally: @@ -51,7 +75,8 @@ pre-commit install Run tests & linter: ```bash -make lint test +mise run lint +mise run test ``` To release, just publish a git tag: diff --git a/mise.toml b/mise.toml index fb3ff12..0ed72a8 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,8 @@ [env] CGO_ENABLED = 1 +[tasks.lint] +run = "pre-commit run --all-files" + +[tasks.test] +run = "go test -race -v ./..." diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 88c75bd..82972e4 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -15,9 +15,13 @@ import ( "golang.org/x/tools/go/ast/inspector" ) -// FlagCheckStructPointers is a possible flag for the analyzer. -// Exported to make it usable in golangci-lint. -const FlagCheckStructPointers = "check-struct-pointers" +// Flags for the analyzer. +// Exported to make them usable in golangci-lint. +const ( + FlagCheckStructPointers = "check-struct-pointers" + FlagCheckLoops = "check-loops" + FlagCheckFunctionLiterals = "check-function-literals" +) // NewAnalyzer returns a fatcontext analyzer. func NewAnalyzer() *analysis.Analyzer { @@ -26,6 +30,10 @@ func NewAnalyzer() *analysis.Analyzer { flags := flag.NewFlagSet("fatcontext", flag.ExitOnError) flags.BoolVar(&rnnr.DetectInStructPointers, FlagCheckStructPointers, false, "set to true to detect potential fat contexts in struct pointers") + flags.BoolVar(&rnnr.CheckLoops, FlagCheckLoops, true, + "set to false to disable detection of fat contexts in loops") + flags.BoolVar(&rnnr.CheckFunctionLiterals, FlagCheckFunctionLiterals, true, + "set to false to disable detection of fat contexts in function literals") return &analysis.Analyzer{ Name: "fatcontext", @@ -50,9 +58,11 @@ const ( type runner struct { DetectInStructPointers bool + CheckLoops bool + CheckFunctionLiterals bool } -func (r *runner) run(pass *analysis.Pass) (interface{}, error) { +func (r *runner) run(pass *analysis.Pass) (any, error) { inspctr, typeValid := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) if !typeValid { return nil, errInvalidAnalysis @@ -99,7 +109,16 @@ func (r *runner) run(pass *analysis.Pass) (interface{}, error) { } func (r *runner) shouldIgnoreReport(category string) bool { - return category == categoryInStructPointer && !r.DetectInStructPointers + switch category { + case categoryInLoop: + return !r.CheckLoops + case categoryInFuncLit: + return !r.CheckFunctionLiterals + case categoryInStructPointer: + return !r.DetectInStructPointers + } + + return false } func (r *runner) getSuggestedFixes( @@ -268,7 +287,7 @@ func getStmtList(stmt ast.Stmt) []ast.Stmt { } // render returns the pretty-print of the given node. -func render(fset *token.FileSet, x interface{}) ([]byte, error) { +func render(fset *token.FileSet, x any) ([]byte, error) { var buf bytes.Buffer err := printer.Fprint(&buf, fset, x) diff --git a/pkg/analyzer/analyzer_test.go b/pkg/analyzer/analyzer_test.go index 1d978f8..3e84c1f 100644 --- a/pkg/analyzer/analyzer_test.go +++ b/pkg/analyzer/analyzer_test.go @@ -38,6 +38,20 @@ func TestAnalyzer(t *testing.T) { analyzer.FlagCheckStructPointers: "true", }, }, + { + desc: "loops disabled", + dir: "no_loops", + options: map[string]string{ + analyzer.FlagCheckLoops: "false", + }, + }, + { + desc: "function literals disabled", + dir: "no_function_literals", + options: map[string]string{ + analyzer.FlagCheckFunctionLiterals: "false", + }, + }, } for _, test := range testCases { diff --git a/pkg/analyzer/testdata/src/no_function_literals/example.go b/pkg/analyzer/testdata/src/no_function_literals/example.go new file mode 100644 index 0000000..a53360c --- /dev/null +++ b/pkg/analyzer/testdata/src/no_function_literals/example.go @@ -0,0 +1,26 @@ +package no_function_literals + +import ( + "context" +) + +// Loop detection stays enabled: this MUST be reported. +func inLoop() { + ctx := context.Background() + + for i := 0; i < 10; i++ { + ctx = context.WithValue(ctx, "key", i) // want "nested context in loop" + _ = ctx + } +} + +// Function literal detection is disabled: this must NOT be reported. +func inFuncLit() { + ctx := context.Background() + + f := func() { + ctx = context.WithValue(ctx, "key", "val") + _ = ctx + } + f() +} diff --git a/pkg/analyzer/testdata/src/no_loops/example.go b/pkg/analyzer/testdata/src/no_loops/example.go new file mode 100644 index 0000000..af756ff --- /dev/null +++ b/pkg/analyzer/testdata/src/no_loops/example.go @@ -0,0 +1,26 @@ +package no_loops + +import ( + "context" +) + +// Loop detection is disabled: this must NOT be reported. +func inLoop() { + ctx := context.Background() + + for i := 0; i < 10; i++ { + ctx = context.WithValue(ctx, "key", i) + _ = ctx + } +} + +// Function literal detection stays enabled: this MUST be reported. +func inFuncLit() { + ctx := context.Background() + + f := func() { + ctx = context.WithValue(ctx, "key", "val") // want "nested context in function literal" + _ = ctx + } + f() +}