Skip to content

lint: add SlogContextual cop and fix remaining bare slog calls#2674

Merged
dgageot merged 6 commits intodocker:mainfrom
dgageot:chore/logger_ctx
May 6, 2026
Merged

lint: add SlogContextual cop and fix remaining bare slog calls#2674
dgageot merged 6 commits intodocker:mainfrom
dgageot:chore/logger_ctx

Conversation

@dgageot
Copy link
Copy Markdown
Member

@dgageot dgageot commented May 6, 2026

Follow-up to #2669. That PR converted slog.{Debug,Info,Warn,Error} to their *Context variants where a context.Context was passed in as a parameter, but missed every site where the context was locally derivedctx := cmd.Context(), ctx, cancel := context.WithTimeout(...), closures capturing an outer ctx, etc. The OpenTelemetry handler we register on slog reads trace/span IDs from the active context, so any record emitted via the bare helper drops out of the trace.

This PR adds a custom linter cop that catches those cases automatically, and fixes the ones that were still hiding in the codebase.

What's in the PR

Lint/SlogContextual cop (lint/slog_contextual.go)

A custom rubocop-go cop that flags slog.{Debug,Info,Warn,Error}(...) whenever a context.Context is reachable in the enclosing function. It recognises:

  • parameters and named results of type context.Context (named or anonymous);
  • locals derived from ctx := cmd.Context(), ctx := context.Background()/TODO(), and the first return of context.With*(...);
  • var ctx context.Context and var ctx = context.Background() forms;
  • closures capturing an outer ctx (defer func() { slog.Error(...) }()).

Helpers without an in-scope context are intentionally not flagged — rewriting them would force callers to thread a context through APIs that don't otherwise need one. Per-line opt-out via //rubocop:disable Lint/SlogContextual.

I tried a type-aware implementation using types.Scope.Innermost(pos), which would have collapsed the cop to ~60 lines, but the rubocop-go runner type-checks each package without an Importer, so cross-package types like context.Context resolve to "invalid type" and the cop never matches. Stuck with the syntactic version.

Conversions surfaced by the cop

The cop found 24 bare slog.X calls in functions where a context was already in scope. All converted in this PR:

  • cmd/root/api.go (4) — defer closures and the top-level "Starting server" log
  • cmd/root/debug.go (2) — toolset listing errors
  • cmd/root/eval.go (2) — session save errors
  • cmd/root/pull.go, cmd/root/push.go — "Starting pull/push" debug logs
  • cmd/root/run.go::stopToolSets — error log inside the cancel-bound scope
  • pkg/fake/proxy.go (3) — recording-proxy cleanup and VCR request error
  • pkg/tui/handlers.go (2) and pkg/tui/tui.go (7) — tab/session persistence warnings
  • pkg/acp/agent.go (5) — ACP method handlers; their context.Context parameter was anonymous, so the linter fix that picks up anonymous params surfaced these too. Renamed each anonymous param to ctx and switched to slog.DebugContext.

cmd/root/flags.go also moves ctx := cmd.Context() above the loadUserConfig() call so the now-context-aware slog.WarnContext has a context in scope. cmd.Context() is just a closure accessor on the captured cmd, so calling it earlier is safe.

Validation

  • task lint — clean (golangci-lint + 12 custom cops on 948 files)
  • task test — all packages pass
  • task build — binary built
  • Verified the cop on a 12-case fixture (params, named results, anonymous params, :=, multi-return, cmd.Context(), var forms, closures inheriting outer ctx, closures with their own ctx, _ discard, no-ctx, already-using-Context-variant) — all expected offenses fired and all non-cases stayed silent.

Commit history

Each commit is focused so the rationale is easy to follow:

  1. lint: add slog contextual cop and fix remaining bare calls — the cop and the bulk of the conversions.
  2. simplify slog cop, drop positional scope tracking — moved ctx := cmd.Context() to the top of flags.go::PersistentPreRunE so the cop no longer needs per-position scope analysis (1-bit-per-scope is enough).
  3. shorten slog cop using cop.MatchSelector and unifying helpers — removed strings and context.With*-specific logic; any call into the context package counts.
  4. split slog cop helpers along ast node boundaries — extracted reportIfBareSlog and valueSpecDeclaresContext; renamed for symmetry (signatureDeclaresContext / bodyDeclaresContext).
  5. fix linter: handle anonymous context params and multi-return contexts — handles func(context.Context) and surfaces 5 cases in pkg/acp/agent.go.
  6. revert misguided multi-return change in slog cop — kept the anonymous-param fix from the previous commit; reverted the LHS-position widening, which was a latent false positive for _, cancel := context.WithCancel(...) and didn't address any case the cop could actually detect.

dgageot added 6 commits May 6, 2026 16:30
The slog contextual cop had two bugs:

1. Anonymous context parameters (e.g., func(context.Context)) were not
   detected because the code only checked f.Names, which is empty for
   anonymous params.

2. Multi-return assignments with context not at position 0 (e.g.,
   err, ctx := fn()) were missed because the code only checked lhs[0].

Fixed by:
- Checking len(f.Names) == 0 as a special case for anonymous params
- Using slices.ContainsFunc to check all LHS positions for named idents

Also fixed newly-detected issues in pkg/acp/agent.go where anonymous
context parameters were not being used in slog calls.
The 'err, ctx := fn()' case the previous commit aimed at can't be

detected anyway: contextProducer only matches calls into the

context package or zero-arg .Context() methods, none of which return

(error, Context). Meanwhile, the broader check turned

'_, cancel := context.WithCancel(...)' into a false positive — that

form discards the context, so no usable name is in scope.
@dgageot dgageot requested a review from a team as a code owner May 6, 2026 15:37
Copy link
Copy Markdown

@docker-agent docker-agent Bot left a comment

Choose a reason for hiding this comment

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

Assessment: 🟢 APPROVE

This PR correctly adds the SlogContextual cop and converts 24 bare slog.X call sites to their *Context variants. The cop logic is sound for the syntactic approach chosen, all context bindings in the converted files are valid and in-scope, and the defer-closure captures (cmd/root/api.go) are safe.

Minor observation (lint/slog_contextual.go, contextProducer): The condition sel == "Context" && len(call.Args) == 0 is a heuristic that over-matches any zero-arg .Context() call regardless of return type — myWidget.Context() returning a custom non-context.Context type would set hasContext = true in the enclosing function, potentially causing a false positive lint offense. In practice this is safe given the strong Go convention that .Context() returns context.Context, but it's worth a brief note in the doc comment should a false positive appear in future code.

Copy link
Copy Markdown

@docker-agent docker-agent Bot left a comment

Choose a reason for hiding this comment

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

Assessment: 🟢 APPROVE

The changes look correct. The new Lint/SlogContextual cop is well-structured, its AST-walking logic appropriately handles parameters, named results, anonymous params, locals derived from cmd.Context()/context.With*, var forms, and closures. All 24 bare slog.X conversions to their *Context variants use the correct in-scope ctx variable. The early ctx := cmd.Context() move in flags.go is safe (it's a pure accessor with no side effects). Anonymous parameter renames in pkg/acp/agent.go introduce no shadowing or naming conflicts. No bugs found in the added code.

@dgageot dgageot merged commit ddd8831 into docker:main May 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants