Skip to content

[linter-miner] feat(linters): add ctxbackground linter — flag context.Background() when a ctx param exists#32865

Merged
pelikhan merged 1 commit into
mainfrom
linter-miner/ctxbackground-0764ab81807aa378
May 17, 2026
Merged

[linter-miner] feat(linters): add ctxbackground linter — flag context.Background() when a ctx param exists#32865
pelikhan merged 1 commit into
mainfrom
linter-miner/ctxbackground-0764ab81807aa378

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

Summary

Adds a new custom Go analysis linter ctxbackground that reports calls to context.Background() inside functions that already receive a context.Context parameter.

Calling context.Background() discards the caller's cancellation signals and deadline, which can cause goroutine leaks and unresponsive behaviour when the parent context is cancelled.

Evidence

The code-pattern scanner found this pattern in 10+ files under pkg/, including:

  • pkg/cli/resources.go
  • pkg/cli/includes.go
  • pkg/workflow/safe_outputs_actions.go

What the linter catches

// ❌ flagged — discards parent cancellation
func DoWork(ctx context.Context) {
    child := context.Background() // want: use the context.Context parameter instead of context.Background()
    doSomething(child)
}

// ✅ ok — no context parameter
func DoWorkNoCtx() {
    _ = context.Background()
}

// ✅ ok — blank identifier param
func DoWorkBlank(_ context.Context) {
    _ = context.Background()
}

Files changed

  • pkg/linters/ctxbackground/ctxbackground.go — analyzer implementation
  • pkg/linters/ctxbackground/ctxbackground_test.go — analysistest-based tests
  • pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go — test fixtures
  • cmd/linters/main.go — registered in multichecker

Validation

  • go build ./cmd/linters
  • go test ./pkg/linters/ctxbackground/...
  • go test ./pkg/linters/... ✅ (all linter packages pass)

Generated by Linter Miner · ● 11.9M ·

  • expires on May 24, 2026, 5:51 PM UTC

Reports calls to context.Background() inside functions that already
receive a context.Context parameter. Such calls discard the caller's
cancellation and deadline signals, which can cause goroutine leaks and
unresponsive behaviour.

Evidence: found in ~10+ files under pkg/ including
pkg/cli/resources.go, pkg/cli/includes.go,
pkg/workflow/safe_outputs_actions.go, and others.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! go-linters labels May 17, 2026
@pelikhan pelikhan marked this pull request as ready for review May 17, 2026 17:57
Copilot AI review requested due to automatic review settings May 17, 2026 17:57
@pelikhan pelikhan merged commit 0933ef8 into main May 17, 2026
9 checks passed
@pelikhan pelikhan deleted the linter-miner/ctxbackground-0764ab81807aa378 branch May 17, 2026 17:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new ctxbackground Go analyzer that flags context.Background() calls inside functions with a context.Context parameter and registers it with the custom multichecker.

Changes:

  • Implements the new analyzer and helper logic for detecting context parameters.
  • Adds analysistest coverage and fixtures.
  • Registers the analyzer in cmd/linters.
Show a summary per file
File Description
pkg/linters/ctxbackground/ctxbackground.go Defines the new analyzer implementation.
pkg/linters/ctxbackground/ctxbackground_test.go Adds the analysistest entry point.
pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go Adds fixture cases for expected diagnostics.
cmd/linters/main.go Registers ctxbackground with the multichecker.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 4

Comment on lines +27 to +29
nodeFilter := []ast.Node{
(*ast.FuncDecl)(nil),
}
Comment on lines +56 to +57
if ident.Name == "context" && sel.Sel.Name == "Background" {
pass.Reportf(call.Pos(), "use the context.Context parameter instead of context.Background()")
Comment on lines +15 to +16
// Analyzer is the ctx-background analysis pass.
var Analyzer = &analysis.Analyzer{
Comment on lines +7 to +9
// flagged: function receives ctx context.Context but calls context.Background()
func DoWork(ctx context.Context) {
_ = context.Background() // want `use the context.Context parameter instead of context.Background\(\)`
@github-actions
Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100

Excellent test quality

Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected No (test: 16 lines, production: 106 lines, ratio: 0.15)
🚨 Coding-guideline violations None

Test Classification Details

Test File Classification Issues Detected
TestCtxBackground pkg/linters/ctxbackground/ctxbackground_test.go:12 ✅ Design None

Analysis Notes

TestCtxBackground uses the standard analysistest.Run pattern — the idiomatic approach for Go static analysis tools. Instead of inline assertion calls, behavioral expectations are embedded as // want "..." comments directly in the testdata source file. The analysistest framework enforces both positive (flagged lines) and negative (non-flagged lines) constraints simultaneously.

The testdata covers four distinct scenarios:

Scenario Expected Rationale
Function with ctx context.Context param calling context.Background() Flagged Core behavioral contract
Function with no context param Not flagged Edge case — linter only fires when a ctx param exists
Function with blank identifier _ context.Context Not flagged Edge case — unused ctx should not trigger
init() function Not flagged Edge case — init has no meaningful ctx to propagate
Method receiver with ctx context.Context Flagged Verifies methods are handled, not just top-level funcs

This is strong behavioral coverage for a new linter: the testdata encodes the linter's exact contract (what it flags and what it intentionally ignores).


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The single test uses analysistest.Run with rich testdata covering both positive and negative cases, fully encoding the linter's behavioral contract.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · ● 5.8M ·

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%).

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd — this is a new feature (linter) and the main risk is false positives and missing test coverage for boundary cases.

Key Themes

  1. False-positive bug (ast.Inspect + FuncLit): The ast.Inspect walk on the function body descends into nested anonymous functions. A context.Background() call inside a closure that captures ctx from the outer scope will be incorrectly flagged, even when the detachment is intentional (e.g. background goroutines). The fix is to return false when ast.Inspect encounters a *ast.FuncLit node.

  2. Missing test fixture for closures: The testdata has no case exercising the closure/anonymous-function scenario. A test fixture would both document the intended behaviour and catch regressions after the FuncLit guard is added.

  3. FuncLit scope not analysed: Named-function analysis is done via Preorder but FuncLit nodes are never independently checked. If an anonymous function has its own ctx context.Context parameter, it won't be analysed. Worth deciding intentionally (and documenting) rather than leaving it as an accidental gap.

  4. context.TODO() scope undocumented: The linter doesn't flag context.TODO(). That may be the right call, but adding a single not-flagged fixture makes the decision explicit and reviewable.

Positive Highlights

  • ✅ Clean, idiomatic use of golang.org/x/tools/go/analysis — inspector pre-order + type checking is the right approach
  • ✅ Correctly excludes blank-identifier params (_ context.Context) — this is a subtle edge case that's easy to miss
  • analysistest-based tests are the gold standard for Go linters; good choice
  • ✅ Build tag on the test file follows the project convention

Verdict

Requesting changes primarily for the FuncLit false-positive bug (item 1) before merge. The test coverage gaps (items 2–4) should be addressed alongside the fix.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 5.2M


// Walk the function body for context.Background() calls.
ast.Inspect(fn.Body, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] ast.Inspect recursively descends into nested *ast.FuncLit nodes. This means context.Background() inside a closure that captures ctx from the outer scope will be flagged — even when the closure deliberately detaches from the parent context (e.g. a background goroutine that must outlive the request). You should skip nested func literals:

ast.Inspect(fn.Body, func(node ast.Node) bool {
    // Do not descend into nested function literals.
    if _, ok := node.(*ast.FuncLit); ok {
        return false
    }
    call, ok := node.(*ast.CallExpr)
    ...

Without this guard, patterns like go func() { ctx := context.Background(); ... }() inside a function that has a ctx param produce false positives.

// not flagged: init function
func init() {
_ = context.Background()
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The testdata doesn't cover the anonymous-function/closure scenario, which is where the ast.Inspect descent issue above would surface. Consider adding:

// not flagged: context.Background() inside a func literal that has no ctx param
// (outer function has ctx, but the inner func literal is a distinct scope)
func DoWorkWithClosure(ctx context.Context) {
    go func() {
        _ = context.Background() // want? depends on the FuncLit guard decision
    }()
}

Having an explicit fixture for this case makes the intended behaviour clear and prevents regressions when the linter is later modified.

// not flagged: init function
func init() {
_ = context.Background()
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] context.TODO() is a closely related anti-pattern ("I'll wire up a real context later") but isn't flagged by this linter. Either add a test fixture that explicitly marks it as not flagged (to document the intentional scope boundary), or extend the linter to also catch context.TODO(). Leaving it undocumented means the next person to maintain this linter won't know whether the omission was deliberate.


nodeFilter := []ast.Node{
(*ast.FuncDecl)(nil),
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The nodeFilter only includes *ast.FuncDecl, so anonymous functions (*ast.FuncLit) that themselves receive a context.Context parameter are never checked. For example:

func Register(mux *http.ServeMux) {
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        ctx := context.Background() // NOT flagged, but should be
        doWork(ctx)
    })
}

If this is an intentional limitation (keeping the linter simple), add a comment to nodeFilter documenting that FuncLit is excluded on purpose. If the intent is to catch all cases, extend nodeFilter to also include (*ast.FuncLit)(nil) and apply hasContextParam to FuncLit.Type as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! go-linters

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants