Skip to content

[linter-miner] feat(linters): add timesleepnocontext linter#38704

Merged
pelikhan merged 2 commits into
mainfrom
linter-miner/time-sleep-no-context-ba5025a546b9d721
Jun 11, 2026
Merged

[linter-miner] feat(linters): add timesleepnocontext linter#38704
pelikhan merged 2 commits into
mainfrom
linter-miner/time-sleep-no-context-ba5025a546b9d721

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new timesleepnocontext static-analysis linter that flags bare time.Sleep
calls inside any function, method, or closure that receives a non-blank
context.Context parameter. A pre-existing scan found 13 such violations across
9 non-test files — this linter closes the gap by enforcing the pattern
automatically in CI rather than relying on manual review.

The companion ADR-38704 documents the decision, alternatives considered, and
known consequences (including the 13 pre-existing violations that remain as
follow-up work).

Changes

pkg/linters/timesleepnocontext/timesleepnocontext.go (new)

Core analysis pass (golang.org/x/tools/go/analysis).

  • Traverses *ast.CallExpr nodes via astutil.Inspector; skips non-time.Sleep calls.
  • Skips test files via filecheck.IsTestFile.
  • For each matching call, walks enclosing *ast.FuncDecl / *ast.FuncLit nodes and resolves whether the function signature contains a context.Context param using type-identity comparison.
  • Ignores blank-identifier params (_ context.Context) — only named, usable contexts trigger the diagnostic.
  • Reports: use select with <ctx>.Done() instead of time.Sleep to allow context cancellation.
  • No automated fix (report-only).

pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go (new)

Golden-file fixtures covering all significant cases:

Case Expectation
Function with named context.Context param flagged
Method receiver with named context.Context param flagged
Goroutine closure inside a context-receiving function flagged
Function with no context param allowed
Function with blank _ context.Context allowed
Function using select/time.After/ctx.Done() allowed
Function literal with its own context param using select allowed

pkg/linters/timesleepnocontext/timesleepnocontext_test.go (new)

Single analysistest.Run unit test (build-tagged !integration) targeting the
testdata fixtures.

cmd/linters/main.go (modified)

Registers timesleepnocontext.Analyzer in the multichecker, making it active
under make golint-custom.

docs/adr/38704-enforce-context-aware-sleep-via-custom-linter.md (new)

Draft ADR documenting context, decision, alternatives, and consequences.
Must be finalized before merge.

Testing

  • analysistest.Run exercises all golden-file cases (bad × 3, good × 4).
  • The multichecker registration is exercised by the existing make golint-custom
    pipeline; no additional integration test was added for the wiring.

Notes / Follow-up

  • 13 pre-existing violations across 9 files (pkg/ + cmd/) must be
    fixed or individually suppressed before CI enforcement becomes blocking.
  • ADR status is Draft — the PR author must review and finalize
    docs/adr/38704-enforce-context-aware-sleep-via-custom-linter.md.
  • No automated fix (suggester) is provided; remediation is always a manual
    select { case <-time.After(d): ... case <-ctx.Done(): ... } rewrite.
  • Blank-identifier contexts (_ context.Context) are explicitly out of scope
    by design; callers that shadow _ with a real name in the body are not
    currently detected.

Generated by PR Description Updater for issue #38704 · 182.5 AIC · ⌖ 13.1 AIC · ⊞ 19.9K ·

Reports bare time.Sleep calls inside functions that already receive a
context.Context parameter. Such calls ignore cancellation signals, leaving
goroutines blocked for the full sleep duration even when the caller has
cancelled or timed out the operation.

The fix is to replace time.Sleep(d) with a context-aware select:

  select {
  case <-time.After(d):
  case <-ctx.Done():
      return ctx.Err()
  }

Evidence gathered from 14 days of issues and code scanning:
- 13 bare time.Sleep calls found in non-test code inside context-receiving
  functions (pkg/cli/logs_rate_limit.go, pkg/cli/run_workflow_tracking.go,
  pkg/workflow/docker_validation.go, pkg/cli/mcp_inspect.go, and others)
- Immediately caught a real violation in pkg/cli/run_workflow_execution.go:574

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 Jun 11, 2026
@pelikhan pelikhan marked this pull request as ready for review June 11, 2026 20:16
Copilot AI review requested due to automatic review settings June 11, 2026 20:16
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 custom Go analysis linter (timesleepnocontext) to the gh-aw linter suite to detect time.Sleep(...) calls inside functions that already accept a context.Context, encouraging context-cancellable waiting patterns.

Changes:

  • Introduces pkg/linters/timesleepnocontext analyzer that detects time.Sleep calls and reports diagnostics when a context parameter exists in an enclosing function.
  • Adds analysistest-based unit tests and fixtures covering bad/good cases (including closures closing over a context).
  • Registers the new analyzer in cmd/linters multichecker.
Show a summary per file
File Description
pkg/linters/timesleepnocontext/timesleepnocontext.go Implements the analyzer logic to detect time.Sleep under context-receiving functions.
pkg/linters/timesleepnocontext/timesleepnocontext_test.go Adds analyzer test harness using analysistest.
pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go Provides fixture cases with // want expectations for diagnostics.
cmd/linters/main.go Registers timesleepnocontext.Analyzer in 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: 1

Comment thread cmd/linters/main.go
Comment on lines 66 to 71
sortslice.Analyzer,
strconvparseignorederror.Analyzer,
jsonmarshalignoredeerror.Analyzer,
lenstringzero.Analyzer,
timesleepnocontext.Analyzer,
tolowerequalfold.Analyzer,
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (212 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/38704-enforce-context-aware-sleep-via-custom-linter.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff.
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and confirm the alternatives you considered.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-38704: Enforce Context-Aware Sleeps via a Custom Static-Analysis Linter

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you for capturing the decision, the alternatives, and the trade-offs while the context is still fresh.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 38704-...md for PR #38704).

🔒 This gate remains blocking until an ADR is linked in the PR body.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 83.7 AIC · ⌖ 9.7 AIC ·

@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test (TestAnalyzer): 1 design test, 0 implementation tests, 0 guideline violations. Testdata covers 7 distinct behavioral scenarios (3 bad cases, 4 good cases).

📊 Metrics & Test Classification (1 test analyzed)
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
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
TestAnalyzer pkg/linters/timesleepnocontext/timesleepnocontext_test.go:13 ✅ Design None — covers 3 bad cases + 4 good/edge cases via analysistest

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%).

TestAnalyzer uses analysistest.Run — the canonical framework for Go analyzer tests. The // want comments in the testdata fixture (testdata/src/timesleepnocontext/timesleepnocontext.go) serve as the behavioral assertions, covering all key scenarios:

  • Bad: named-context function calls time.Sleep → diagnostic expected (×2: function + method)
  • Bad: goroutine closure captures context → diagnostic expected
  • Good: no context param → no diagnostic
  • Good: blank context param (_) → no diagnostic
  • Good: uses select { case <-ctx.Done() } → no diagnostic
  • Good: function literal carries its own context param → no diagnostic

Test inflation: 16 test lines vs. 137 production lines — ratio 0.12 ✅

📖 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.

References: §27374710223

🧪 Test quality analysis by Test Quality Sentinel · 122.1 AIC · ⌖ 27.7 AIC ·

@github-actions github-actions Bot left a comment

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.

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). TestAnalyzer uses analysistest.Run with comprehensive testdata covering 3 flagged cases and 4 good/edge cases.

@github-actions github-actions Bot left a comment

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.

REQUEST_CHANGES — one false-positive bug and one undocumented behaviour gap.

### Blocking issues found

1. False positive — blank-context closure inside outer context-receiving function (line 55)

The continue in the enclosing-function loop escalates past a func(_ context.Context) closure to the outer function and emits an incorrect diagnostic. The ctxbackground linter (the direct analogue) uses break in the equivalent position to stop the search when the innermost scoped context is blank/unusable. Test data covers GoodBlankContext as a top-level function only; the nested case is untested and broken.

2. Unnamed context.Context parameters silently skipped, no test coverage (lines 116–120)

Parameters written as func Foo(context.Context) (no name, not even _) produce an empty field.Names slice; the inner loop never runs and contextParamName returns false. Whether this should be treated identically to _ (skip) or flagged differently is a design decision that needs a test and a doc comment. Currently neither exists.

🔎 Code quality review by PR Code Quality Reviewer · ⌖ 13.5 AIC

}
ctxParamName, hasCtx := contextParamName(pass, funcType)
if !hasCtx {
continue

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.

False positive: blank-context closure nested inside a context-receiving function is incorrectly flagged.

When the innermost enclosing function has _ context.Context, contextParamName returns false and the loop continues — escalating to the outer function. A time.Sleep inside func(_ context.Context) will be reported with the outer function's ctx, even though the blank _ is an explicit signal that this scope intentionally ignores context.

Compare the analogous ctxbackground linter (line 50): it uses break when no usable context name is found, stopping the search rather than climbing up.

�� Failing scenario (not in test data)
func Outer(ctx context.Context) {
    doWork(func(_ context.Context, d time.Duration) {
        time.Sleep(d) // incorrectly flagged: "use select with ctx.Done()"
                      // — but the blank _ signals deliberate context ignorance
    })
}

The fix is to distinguish no context param (keep climbing) from blank/unnamed context param (stop climbing). One approach:

ctxParamName, hasCtx := contextParamName(pass, funcType)
if !hasCtx {
    // If this scope has a context.Context param but it is blank/unnamed,
    // don't escalate — the blank signals deliberate non-use.
    if hasAnyContextParam(pass, funcType) {
        break
    }
    continue
}

A companion hasAnyContextParam (checks type only, ignores names) can be extracted from contextParamName.

Comment on lines +116 to +120
for _, name := range field.Names {
if name.Name != "_" {
return name.Name, true
}
}

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.

Unnamed context.Context parameters are silently skipped with no test coverage — behaviour is undocumented.

When a context.Context field has no names at all (field.Names is empty, which happens for unnamed parameters like func Foo(context.Context) {}), the inner for _, name := range field.Names loop never executes and the function falls through to return "", false. This is a false negative — time.Sleep in func Foo(context.Context) { time.Sleep(...) } is not flagged.

Unnamed context parameters are idiomatic in interface implementations and stubs. Whether the intent is to treat them the same as _ context.Context (skip, no flag) or to flag them differently, neither the code comment nor the test data addresses this case.

💡 Missing test cases to add
// Unnamed context.Context — decide: Good or Bad?
func UnnamedContext(context.Context, d time.Duration) {
    time.Sleep(d)
}

If the intent is not to flag (same as _): add the test case with no // want annotation and update the doc comment to say "blank or unnamed context parameters are exempt."

If the intent is to flag (the parameter could be re-named and made cancellation-aware): add a // want annotation and handle the case by substituting a placeholder name like ctx in the diagnostic message.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan pelikhan merged commit de83a95 into main Jun 11, 2026
29 of 30 checks passed
@pelikhan pelikhan deleted the linter-miner/time-sleep-no-context-ba5025a546b9d721 branch June 11, 2026 20:49
Copilot stopped work on behalf of pelikhan due to an error June 11, 2026 20:50
Copilot AI requested a review from pelikhan June 11, 2026 20:50
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