Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ linters:
- exhaustruct
- wsl
- noinlineerr
- gomodguard
settings:
cyclop:
max-complexity: 20
funlen:
lines: -1
formatters:
enable:
- goimports
Expand Down
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ repos:
name: go test ./...
language: golang
types_or: [go]
entry: make test
pass_filenames: false
entry: mise run test
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 0 additions & 7 deletions Makefile

This file was deleted.

27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[env]
CGO_ENABLED = 1

[tasks.lint]
run = "pre-commit run --all-files"

[tasks.test]
run = "go test -race -v ./..."
31 changes: 25 additions & 6 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions pkg/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions pkg/analyzer/testdata/src/no_function_literals/example.go
Original file line number Diff line number Diff line change
@@ -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()
}
26 changes: 26 additions & 0 deletions pkg/analyzer/testdata/src/no_loops/example.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading