Skip to content

feat(linters): add nilctxpassed analyzer#45799

Merged
pelikhan merged 5 commits into
mainfrom
copilot/add-nilctxpassed-linter
Jul 15, 2026
Merged

feat(linters): add nilctxpassed analyzer#45799
pelikhan merged 5 commits into
mainfrom
copilot/add-nilctxpassed-linter

Conversation

Copilot AI commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Passing nil as a context.Context compiles cleanly but panics at runtime when the callee calls any method on the context. Adds a new nilctxpassed analyzer to catch this at analysis time.

What changed

  • pkg/linters/nilctxpassed/nilctxpassed.go — New analyzer. For each call expression, resolves the callee's *types.Signature, checks each parameter that accepts context.Context, and flags arguments that are the predeclared nil (confirmed via *types.Nil). Handles variadic positions; respects //nolint:nilctxpassed and skips generated files.
  • pkg/linters/nilctxpassed/nilctxpassed_test.go + testdata/analysistest-based coverage: nil in first/second/variadic positions, proper context.Background()/context.TODO() (no flag), nil for non-context interfaces (no flag), nolint suppression.
  • cmd/linters/main.go — Registers the analyzer in the multichecker binary.
  • pkg/linters/README.md, doc.go, spec_test.go — Documentation and spec updated (count 44 → 45).

Example

// flagged: nil passed as context.Context
http.NewRequestWithContext(nil, "GET", url, nil) // want `nil passed as context.Context...`

// correct
http.NewRequestWithContext(context.Background(), "GET", url, nil)

Copilot AI and others added 2 commits July 15, 2026 18:52
Adds a new nilctxpassed analyzer that detects function calls where nil
is passed as a context.Context argument. The correct idioms are
context.Background() or context.TODO() instead.

- pkg/linters/nilctxpassed/nilctxpassed.go: analyzer implementation
- pkg/linters/nilctxpassed/nilctxpassed_test.go: analysistest-based test
- pkg/linters/nilctxpassed/testdata/src/nilctxpassed/nilctxpassed.go: fixture
- cmd/linters/main.go: register in multichecker binary
- pkg/linters/README.md: documentation entry
- pkg/linters/doc.go: update count (44 → 45) and list
- pkg/linters/spec_test.go: register in documented analyzers

Closes #45795

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add nilctxpassed linter to detect nil context usage feat(linters): add nilctxpassed analyzer Jul 15, 2026
Copilot AI requested a review from pelikhan July 15, 2026 18:54
@pelikhan pelikhan marked this pull request as ready for review July 15, 2026 18:58
Copilot AI review requested due to automatic review settings July 15, 2026 18:58

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 Go go/analysis analyzer (nilctxpassed) to detect calls that pass the predeclared nil to parameters typed as context.Context, and wires it into the linter binary and public documentation/spec.

Changes:

  • Introduces pkg/linters/nilctxpassed analyzer and an analysistest suite with fixture code.
  • Registers the analyzer in cmd/linters multichecker.
  • Updates linter package documentation and spec assertions to include the new analyzer.
Show a summary per file
File Description
pkg/linters/nilctxpassed/nilctxpassed.go Implements the nilctxpassed analyzer logic and diagnostics
pkg/linters/nilctxpassed/nilctxpassed_test.go Adds analysistest harness for the new analyzer
pkg/linters/nilctxpassed/testdata/src/nilctxpassed/nilctxpassed.go Provides fixture cases for diagnostics and suppression
cmd/linters/main.go Registers nilctxpassed in the multichecker driver
pkg/linters/README.md Documents the new analyzer in the public subpackages list/table
pkg/linters/doc.go Updates overall analyzer list/count and includes nilctxpassed
pkg/linters/spec_test.go Updates spec expectations to include the new documented analyzer

Review details

Tip

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

  • Files reviewed: 7/8 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines +40 to +43
ctxType := astutil.ContextContextType(pass)
if ctxType == nil {
return nil, nil
}
Comment on lines +1 to +5
package nilctxpassed

import (
"context"
)
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 85/100 — Excellent

Analyzed 2 test(s): 2 design, 0 implementation, 0 violation(s).

📊 Metrics (2 tests)
Metric Value
Analyzed 2 (Go: 2, JS: 0)
✅ Design 2 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 1 (50%)
Duplicate clusters 0
Inflation No
🚨 Violations 0
Test File Classification Issues
TestNilCtxPassed nilctxpassed_test.go:11 design_test / high_value none
TestSpec_PublicAPI_SubpackageAnalyzers[nilctxpassed] spec_test.go (table row) design_test / high_value none

Verdict

Passed. 0% implementation tests (threshold: 30%). No violations.

Coverage highlights:

  • TestNilCtxPassed uses the idiomatic analysistest.Run pattern against annotated testdata covering 4 bad cases (nil ctx — simple, multi-param, positional, variadic) and 5 good cases (context.Background, context.TODO, non-ctx nil, local var, nolint suppression). Both positive and negative paths verified.
  • spec_test.go adds nilctxpassed to the documented-analyzer registry test, enforcing the public API contract.
  • Build tags correct (//go:build !integration on line 1). No forbidden mock libraries. No inflation (test:prod ratio ~0.13:1).

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 27.9 AIC · ⌖ 8.68 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot 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.

✅ Test Quality Sentinel: 85/100. 0% implementation tests (threshold: 30%). No violations.

Documents the decision to add a static analyzer that catches nil passed
as context.Context at analysis time rather than runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (213 new lines in pkg/ — exceeds the 100-line ADR enforcement threshold) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/45799-add-nilctxpassed-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 at docs/adr/45799-add-nilctxpassed-linter.md — it was generated from the PR diff

  2. Complete any missing sections — add context the AI could not infer, refine the decision rationale, and verify the alternatives listed reflect what you actually 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-45799: Add nilctxpassed Static Analyzer

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.

📋 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., 45799-add-nilctxpassed-linter.md for PR #45799).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 54 AIC · ⌖ 13.1 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot 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.

Skills-Based Review 🧠

Applied /tdd — requesting changes on two correctness/coverage gaps.

📋 Key Themes & Highlights

Key Issues

  • False-negative blind spot: astutil.ContextContextType returns nil when the analyzed package doesn't import context itself, causing the analyzer to silently skip all calls in that package. This is a real correctness bug — packages that call e.g. http.NewRequestWithContext(nil,...) without a direct context import won't be analyzed.
  • Missing test coverage: No test for calling via a function variable (f := takesCtx; f(nil)), leaving the calleeSignature code path without regression protection.

Positive Highlights

  • ✅ Clean structure following established analyzer patterns (astutil, nolint, filecheck)
  • ✅ Correct variadic parameter handling
  • isBuiltinNil using types.Nil is precise — avoids false positives on typed nils
  • ✅ Comprehensive happy-path test cases

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 32.8 AIC · ⌖ 4.55 AIC · ⊞ 6.7K
Comment /matt to run again

ctxType := astutil.ContextContextType(pass)
if ctxType == nil {
return nil, nil
}

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.

[/tdd] The early-exit on ctxType == nil produces false negatives when the analyzed package doesn't itself import context — a package can call http.NewRequestWithContext(nil, ...) without importing context and the analyzer silently skips it.

💡 Suggested fix

Look up the context.Context type by walking the type-checker's universe or via a direct package lookup rather than relying on astutil.ContextContextType which depends on the current package's imports:

// Instead of bailing when the local package doesn't import context,
// resolve the type from the type-checker's scope:
ctxIface := lookupContextType(pass.Pkg)
// where lookupContextType searches imported packages transitively

Alternatively, check every parameter against the named type context.Context using its package path ("context") so the check is import-independent.

@copilot please address this.

} else {
continue
}

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.

[/tdd] There is no test case for calling through a function variable or method value — e.g. f := takesCtx; f(nil). The calleeSignature path handles this correctly via TypesInfo.TypeOf, but without a test this regression surface is uncovered.

💡 Suggested test case

Add to testdata:

func BadNilFuncVar() {
    f := takesCtx
    f(nil) // want `nil passed as context\.Context...`
}

@copilot please address this.

@github-actions github-actions Bot 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.

Review: nilctxpassed analyzer

The implementation is well-structured and follows the established linter patterns. Two issues warrant attention (already flagged inline):

Blocking concernContextContextType returns nil when the analyzed package does not directly import context, causing an early exit at line 43. Packages that pass nil to context.Context parameters from external packages (e.g. net/http.NewRequestWithContext) without importing context themselves will produce no diagnostics — a common real-world bug pattern the analyzer should catch.

Suggested fix: rather than relying on the current package's imports, resolve the context.Context type by iterating all transitively-imported packages, or compare by interface identity (method set) rather than types.Identical.

Test gap — The testdata only exercises packages that import context directly. A test using net/http.NewRequestWithContext(nil, ...) without a context import would expose the false-negative above.

No other issues found in registration, documentation, or the nolint/variadic handling.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 26 AIC · ⌖ 4.34 AIC · ⊞ 5K

@github-actions github-actions Bot 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.

REQUEST_CHANGES — two correctness bugs and a crash path must be fixed before merge.

Blocking issues

  1. ctxType == nil early exit silently disables the linter for the most common real-world scenario (flagged by prior review): ContextContextType only walks direct imports; any package calling an external function that accepts context.Context without itself importing "context" will produce zero diagnostics. This covers the majority of real call sites (e.g. http.NewRequestWithContext(nil, ...)).

  2. nolint suppression checked at call.Pos() instead of arg.Pos() (new): In multi-line calls, //nolint:nilctxpassed on the nil argument line is silently ignored. The linter emits a false-positive and developers cannot suppress it. Inverse is also true: a nolint on the call-expression opening line blanket-suppresses all argument diagnostics. Fix: move HasDirectiveForLinter inside the argument loop using arg.Pos().

  3. params.At(params.Len()-1) with no guard against params.Len() == 0 (new): When sig.Variadic() is true on a zero-parameter signature (cgo stubs, synthetic types), this panics with an index-out-of-range. A simple if params.Len() == 0 { continue } guard is all that is needed.

🔎 Code quality review by PR Code Quality Reviewer · 58.9 AIC · ⌖ 4.79 AIC · ⊞ 5.6K
Comment /review to run again

if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
continue
}
if nolint.HasDirectiveForLinter(pos, noLintIndex, "nilctxpassed") {

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.

nolint suppression is checked at call.Pos() but the diagnostic is reported at arg.Pos() — in a multi-line call, //nolint:nilctxpassed on the nil argument line is silently ignored.

💡 Details and fix

Current code checks position at the call expression start:

pos := pass.Fset.PositionFor(call.Pos(), false)
if nolint.HasDirectiveForLinter(pos, noLintIndex, "nilctxpassed") {
    continue
}

For a multi-line call like:

foo(
    nil, (nolint/redacted):nilctxpassed
)

call.Pos() is on the foo( line — the directive on the nil line is never found, so the diagnostic fires despite suppression. Worse, a //nolint:nilctxpassed on the foo( line will blanket-suppress diagnostics for all args on later lines.

Fix: move the nolint check inside the argument loop using arg.Pos():

for i, arg := range call.Args {
    // ...(type checks)...
    argPos := pass.Fset.PositionFor(arg.Pos(), false)
    if nolint.HasDirectiveForLinter(argPos, noLintIndex, "nilctxpassed") {
        continue
    }
    pass.Report(...)
}

The file-skip check can remain at call.Pos() since file membership cannot change within a call.

params := sig.Params()
for i, arg := range call.Args {
var paramType types.Type
if sig.Variadic() && i >= params.Len()-1 {

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.

Potential panic: params.At(params.Len()-1) is called without guarding against params.Len() == 0 when sig.Variadic() is true on a malformed or cgo-generated signature.

💡 Details and fix

sig.Variadic() == true means the function type is variadic, but nothing in the Go type-checker guarantees params.Len() >= 1 in all corner cases (e.g. cgo-generated stubs, intentionally malformed AST input during testing). Computing params.Len()-1 on an empty tuple yields -1, and params.At(-1) panics.

Fix: add a guard before the slice-type lookup:

if sig.Variadic() && i >= params.Len()-1 {
    if params.Len() == 0 {
        continue
    }
    sliceType, ok := params.At(params.Len() - 1).Type().(*types.Slice)
    ...
}

takesCtx(ctx)
}

// not flagged: nolint suppresses the diagnostic

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.

The nolint test only covers a single-line, single-argument call — the broken multi-line case is entirely untested, meaning the position-mismatch bug described in the main file goes undetected by this test suite.

💡 What to add

Add a test case where the //nolint:nilctxpassed directive is on the same line as the nil argument in a multi-line call:

// not flagged: nolint on the nil arg line in a multiline call
func NolintMultilineSuppressed() {
    takesCtxAndOther(
        nil, (nolint/redacted):nilctxpassed
        42,
    )
}

Without this test, the nolint-at-call-pos vs nolint-at-arg-pos bug passes CI silently.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

pr-sous-chef
@copilot please run the pr-finisher skill, address the blocking review feedback on this PR (including the nilctxpassed false-negative when the package does not directly import context, the nolint suppression position issue, the variadic zero-parameter panic guard, and the missing regression coverage), then rerun checks once complete.

Run: https://github.com/github/gh-aw/actions/runs/29444107527

Generated by 👨‍🍳 PR Sous Chef · 3.02 AIC · ⌖ 8.21 AIC · ⊞ 7.5K ·
Comment /souschef to run again

…int position, guard variadic zero-params

- Replace ContextContextType early-return with isContextContext() that
  walks each parameter's named type directly (Pkg().Path()=="context" &&
  Name()=="Context"), eliminating the false-negative when the analyzed
  package does not directly import "context".
- Add params.Len()>0 guard before params.Len()-1 to prevent a panic on
  a theoretically zero-param variadic signature.
- Move nolint suppression check to arg-level (arg.Pos()) so that a
  //nolint:nilctxpassed comment on the nil argument's line correctly
  suppresses the diagnostic in multiline calls.
- Add testdata/src/ctxhelper helper package and testdata/src/nilctxpassed_noctx
  fixture that calls ctxhelper.TakesCtx(nil) without importing "context",
  with a // want annotation that exercises the regression path.
- Add NolintSuppressedMultiline fixture case to verify per-arg nolint
  suppression in multiline calls.
- Update TestNilCtxPassed to run analysis on both packages.

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

pr-sous-chef
@copilot please run the pr-finisher skill, address the blocking review feedback on this PR (including the nilctxpassed false-negative when the package does not directly import `contex...

All four issues addressed in commit fix(nilctxpassed):

  1. False-negative (no direct context import) — Removed the ContextContextType early-return. Replaced types.Identical(paramType, ctxType) with a new isContextContext(t) helper that inspects each parameter's named type directly (obj.Pkg().Path() == "context" && obj.Name() == "Context"), so the analyzer catches nil passed to context.Context parameters regardless of whether the calling package imports context.

  2. Variadic zero-parameter panic guard — Added params.Len() > 0 before the params.Len()-1 subscript in the variadic branch.

  3. Nolint suppression position — Moved the nolint.HasDirectiveForLinter check from call-level (call.Pos()) to arg-level (arg.Pos()), inside the per-argument loop. This ensures a //nolint:nilctxpassed comment placed on the nil argument line correctly suppresses the diagnostic in multiline calls.

  4. Regression coverage — Added testdata/src/ctxhelper/ctxhelper.go (helper package providing TakesCtx(context.Context) and TakesOtherPointer(*int)), testdata/src/nilctxpassed_noctx/nilctxpassed_noctx.go (analyzed package that imports ctxhelper but NOT context, with a // want annotation on the nil call and a non-flagged nil-to-non-context call), and a NolintSuppressedMultiline case in the existing fixture to exercise per-arg nolint suppression. TestNilCtxPassed now runs analysis on both nilctxpassed and nilctxpassed_noctx.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

pr-sous-chef
@copilot please run the pr-finisher skill, confirm the dismissed blocking review is fully addressed in code/tests and that the ADR / PR state is ready for maintainer verification, then rerun checks once complete.

Run: https://github.com/github/gh-aw/actions/runs/29447716606

Generated by 👨‍🍳 PR Sous Chef · 4.78 AIC · ⌖ 6.15 AIC · ⊞ 7.5K ·
Comment /souschef to run again

Copilot AI commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

pr-sous-chef
@copilot please run the pr-finisher skill, confirm the dismissed blocking review is fully addressed in code/tests and that the ADR / PR state is ready for maintainer verification, the...

All blocking review issues are confirmed fully addressed in the pushed commits. Summary:

Blocking review (all addressed in commit 037a4f6):

  • ✅ False-negative when package doesn't import contextisContextContext() now identifies context.Context by inspecting Obj().Pkg().Path() and Obj().Name() on each parameter's named type, independently of the current package's imports.
  • ✅ Missing regression coverage — testdata/src/nilctxpassed_noctx/ fixture (imports ctxhelper, not context) with // want annotation exercises the no-direct-import path; TestNilCtxPassed runs analysis on both packages.
  • ✅ Variadic zero-param panic guard — params.Len() > 0 check added before params.Len()-1.
  • ✅ Nolint suppression position — check moved from call-level to arg-level (arg.Pos()) so //nolint:nilctxpassed on the nil argument's line suppresses correctly in multiline calls; NolintSuppressedMultiline test case added.

ADRdocs/adr/45799-add-nilctxpassed-linter.md is present and covers context, decision, alternatives, and consequences. Status remains "Draft" — ready for a maintainer to advance to "Accepted."

Local validationmake fmt ✅, make lint ✅, all pkg/linters/... tests ✅ (including spec test with count 45), no uncommitted changes.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill and ensure this PR is ready for maintainer review.

Generated by 👨‍🍳 PR Sous Chef · 5.48 AIC · ⌖ 7.22 AIC · ⊞ 7.5K ·
Comment /souschef to run again

Copilot AI commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill and ensure this PR is ready for maintainer review.

...

PR is ready for maintainer review. Local validation confirms:

  • make fmt
  • make lint
  • All pkg/linters/... tests ✅ (54 packages, including nilctxpassed and spec test at count 45)
  • Working tree clean, no uncommitted changes

All blocking review issues were addressed in commit 037a4f6 (false-negative fix via isContextContext, variadic guard, arg-level nolint, and nilctxpassed_noctx regression coverage). ADR is at docs/adr/45799-add-nilctxpassed-linter.md.

Hand-off: CI must be re-triggered by a maintainer (close/reopen PR or workflow_dispatch) before merge. The only known pre-existing failures in pkg/cli are DNS-blocked network calls in this sandbox, unrelated to this PR.

@pelikhan pelikhan merged commit 5ce6372 into main Jul 15, 2026
31 checks passed
@pelikhan pelikhan deleted the copilot/add-nilctxpassed-linter branch July 15, 2026 21:08
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.10

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.

[linter-miner] feat(linters): add nilctxpassed linter

4 participants