From ad758d6c4fb3fe192dcb0be384a1af71c24b3f96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 05:37:09 +0000 Subject: [PATCH 1/2] Initial plan From 74ba546363f3bb6d0f8016fd3ad98e967cb831ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 05:46:39 +0000 Subject: [PATCH 2/2] Add nolint support to context linters Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../contextcancelnotdeferred.go | 14 ++++++++------ .../contextcancelnotdeferred.go | 7 +++++++ pkg/linters/ctxbackground/ctxbackground.go | 5 +++++ .../testdata/src/ctxbackground/ctxbackground.go | 6 ++++++ .../src/ctxbackground/ctxbackground.go.golden | 6 ++++++ .../execcommandwithoutcontext.go | 5 +++++ .../execcommandwithoutcontext.go | 6 ++++++ .../execcommandwithoutcontext.go.golden | 6 ++++++ .../src/timesleepnocontext/timesleepnocontext.go | 6 ++++++ .../timesleepnocontext/timesleepnocontext.go | 5 +++++ 10 files changed, 60 insertions(+), 6 deletions(-) diff --git a/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go b/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go index 08adc8b846d..34b69cfde9a 100644 --- a/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go +++ b/pkg/linters/contextcancelnotdeferred/contextcancelnotdeferred.go @@ -12,6 +12,7 @@ import ( "github.com/github/gh-aw/pkg/linters/internal/astutil" "github.com/github/gh-aw/pkg/linters/internal/filecheck" + "github.com/github/gh-aw/pkg/linters/internal/nolint" ) // Analyzer is the context-cancel-not-deferred analysis pass. @@ -28,19 +29,20 @@ func run(pass *analysis.Pass) (any, error) { if err != nil { return nil, err } + noLintLinesByFile := nolint.BuildLineIndex(pass, "contextcancelnotdeferred") nodeFilter := []ast.Node{ (*ast.FuncDecl)(nil), } insp.Preorder(nodeFilter, func(n ast.Node) { - inspectCancelFuncDecl(pass, n) + inspectCancelFuncDecl(pass, n, noLintLinesByFile) }) return nil, nil } -func inspectCancelFuncDecl(pass *analysis.Pass, n ast.Node) { +func inspectCancelFuncDecl(pass *analysis.Pass, n ast.Node, noLintLinesByFile map[string]map[int]struct{}) { fn, ok := n.(*ast.FuncDecl) if !ok || fn.Body == nil { return @@ -54,11 +56,11 @@ func inspectCancelFuncDecl(pass *analysis.Pass, n ast.Node) { cancelVars := make(map[types.Object]*cancelVarState) ast.Inspect(fn.Body, func(node ast.Node) bool { - return inspectCancelNode(pass, cancelVars, node) + return inspectCancelNode(pass, cancelVars, node, noLintLinesByFile) }) for _, state := range cancelVars { - if state.hasDirectCancel && !state.hasDeferCancel { + if state.hasDirectCancel && !state.hasDeferCancel && !nolint.HasDirective(pass.Fset.PositionFor(state.createPos, false), noLintLinesByFile) { pass.Report(analysis.Diagnostic{ Pos: state.createPos, Message: "context cancel function should be deferred immediately after context.WithCancel/WithTimeout/WithDeadline", @@ -67,7 +69,7 @@ func inspectCancelFuncDecl(pass *analysis.Pass, n ast.Node) { } } -func inspectCancelNode(pass *analysis.Pass, cancelVars map[types.Object]*cancelVarState, node ast.Node) bool { +func inspectCancelNode(pass *analysis.Pass, cancelVars map[types.Object]*cancelVarState, node ast.Node, noLintLinesByFile map[string]map[int]struct{}) bool { if node == nil { return false } @@ -91,7 +93,7 @@ func inspectCancelNode(pass *analysis.Pass, cancelVars map[types.Object]*cancelV if obj == nil { continue } - if prev, exists := cancelVars[obj]; exists && prev.hasDirectCancel && !prev.hasDeferCancel { + if prev, exists := cancelVars[obj]; exists && prev.hasDirectCancel && !prev.hasDeferCancel && !nolint.HasDirective(pass.Fset.PositionFor(prev.createPos, false), noLintLinesByFile) { pass.Report(analysis.Diagnostic{ Pos: prev.createPos, Message: "context cancel function should be deferred immediately after context.WithCancel/WithTimeout/WithDeadline", diff --git a/pkg/linters/contextcancelnotdeferred/testdata/src/contextcancelnotdeferred/contextcancelnotdeferred.go b/pkg/linters/contextcancelnotdeferred/testdata/src/contextcancelnotdeferred/contextcancelnotdeferred.go index 621aceae7c9..dc07752d6e6 100644 --- a/pkg/linters/contextcancelnotdeferred/testdata/src/contextcancelnotdeferred/contextcancelnotdeferred.go +++ b/pkg/linters/contextcancelnotdeferred/testdata/src/contextcancelnotdeferred/contextcancelnotdeferred.go @@ -54,3 +54,10 @@ func BadReassignThenGood(parent context.Context) error { func BlankIdentifier(parent context.Context) { _, _ = context.WithTimeout(parent, time.Second) } + +func GoodNoLint(parent context.Context) error { + ctx, cancel := context.WithTimeout(parent, time.Second) //nolint:contextcancelnotdeferred + _ = ctx + cancel() + return nil +} diff --git a/pkg/linters/ctxbackground/ctxbackground.go b/pkg/linters/ctxbackground/ctxbackground.go index 547c4ca51ed..792c998abeb 100644 --- a/pkg/linters/ctxbackground/ctxbackground.go +++ b/pkg/linters/ctxbackground/ctxbackground.go @@ -12,6 +12,7 @@ import ( "github.com/github/gh-aw/pkg/linters/internal/astutil" "github.com/github/gh-aw/pkg/linters/internal/filecheck" + "github.com/github/gh-aw/pkg/linters/internal/nolint" ) // Analyzer is the ctx-background analysis pass. @@ -28,6 +29,7 @@ func run(pass *analysis.Pass) (any, error) { if err != nil { return nil, err } + noLintLinesByFile := nolint.BuildLineIndex(pass, "ctxbackground") for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) { call, ok := cur.Node().(*ast.CallExpr) @@ -39,6 +41,9 @@ func run(pass *analysis.Pass) (any, error) { if filecheck.IsTestFile(pos.Filename) { continue } + if nolint.HasDirective(pos, noLintLinesByFile) { + continue + } for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { var ftype *ast.FuncType diff --git a/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go b/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go index ba8a15b9a12..dfc7b92ae0e 100644 --- a/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go +++ b/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go @@ -58,4 +58,10 @@ func DoWorkWithDetachedClosure(ctx context.Context) { _ = ctx } +// not flagged: inline nolint suppresses intentional detachment. +func DoWorkNoLint(ctx context.Context) { + _ = context.Background() //nolint:ctxbackground + _ = ctx +} + func handle(f func(context.Context)) { f(nil) } diff --git a/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go.golden b/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go.golden index 8e57fcf0faf..d88e4cb9bc9 100644 --- a/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go.golden +++ b/pkg/linters/ctxbackground/testdata/src/ctxbackground/ctxbackground.go.golden @@ -58,4 +58,10 @@ func DoWorkWithDetachedClosure(ctx context.Context) { _ = ctx } +// not flagged: inline nolint suppresses intentional detachment. +func DoWorkNoLint(ctx context.Context) { + _ = context.Background() //nolint:ctxbackground + _ = ctx +} + func handle(f func(context.Context)) { f(nil) } diff --git a/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go b/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go index faa4428d565..6299120a37a 100644 --- a/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go +++ b/pkg/linters/execcommandwithoutcontext/execcommandwithoutcontext.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/linters/internal/astutil" "github.com/github/gh-aw/pkg/linters/internal/filecheck" + "github.com/github/gh-aw/pkg/linters/internal/nolint" ) // Analyzer is the exec-command-without-context analysis pass. @@ -30,6 +31,7 @@ func run(pass *analysis.Pass) (any, error) { if err != nil { return nil, err } + noLintLinesByFile := nolint.BuildLineIndex(pass, "execcommandwithoutcontext") for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) { call, ok := cur.Node().(*ast.CallExpr) @@ -45,6 +47,9 @@ func run(pass *analysis.Pass) (any, error) { if filecheck.IsTestFile(pos.Filename) { continue } + if nolint.HasDirective(pos, noLintLinesByFile) { + continue + } for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { funcType := enclosingFuncType(encl.Node()) diff --git a/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go b/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go index e96bda2244e..28043e8d6f7 100644 --- a/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go +++ b/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go @@ -67,3 +67,9 @@ func OuterCtxInnerClosure(ctx context.Context) { _ = exec.Command("ls") // want `use exec\.CommandContext\(ctx, \.\.\.\) instead of exec\.Command to propagate context cancellation` }() } + +// Good: inline nolint suppresses an intentional detached command. +func GoodNoLint(ctx context.Context, name string) { + _ = ctx + _ = exec.Command(name) //nolint:execcommandwithoutcontext +} diff --git a/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go.golden b/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go.golden index 99b1f03cd5d..3220dd21893 100644 --- a/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go.golden +++ b/pkg/linters/execcommandwithoutcontext/testdata/src/execcommandwithoutcontext/execcommandwithoutcontext.go.golden @@ -67,3 +67,9 @@ func OuterCtxInnerClosure(ctx context.Context) { _ = exec.CommandContext(ctx, "ls") // want `use exec\.CommandContext\(ctx, \.\.\.\) instead of exec\.Command to propagate context cancellation` }() } + +// Good: inline nolint suppresses an intentional detached command. +func GoodNoLint(ctx context.Context, name string) { + _ = ctx + _ = exec.Command(name) //nolint:execcommandwithoutcontext +} diff --git a/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go index 2642d45e362..e6be802d097 100644 --- a/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go +++ b/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go @@ -57,3 +57,9 @@ func GoodFuncLitWithOwnCtx() { func doWork(fn func(context.Context, time.Duration)) { fn(context.Background(), time.Second) } + +// Good: inline nolint suppresses intentional sleep. +func GoodNoLint(ctx context.Context, d time.Duration) { + _ = ctx + time.Sleep(d) //nolint:timesleepnocontext +} diff --git a/pkg/linters/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/timesleepnocontext.go index ad9f9303983..d098895a3fa 100644 --- a/pkg/linters/timesleepnocontext/timesleepnocontext.go +++ b/pkg/linters/timesleepnocontext/timesleepnocontext.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/linters/internal/astutil" "github.com/github/gh-aw/pkg/linters/internal/filecheck" + "github.com/github/gh-aw/pkg/linters/internal/nolint" ) // Analyzer is the time-sleep-no-context analysis pass. @@ -30,6 +31,7 @@ func run(pass *analysis.Pass) (any, error) { if err != nil { return nil, err } + noLintLinesByFile := nolint.BuildLineIndex(pass, "timesleepnocontext") for cur := range insp.Root().Preorder((*ast.CallExpr)(nil)) { call, ok := cur.Node().(*ast.CallExpr) @@ -44,6 +46,9 @@ func run(pass *analysis.Pass) (any, error) { if filecheck.IsTestFile(pos.Filename) { continue } + if nolint.HasDirective(pos, noLintLinesByFile) { + continue + } for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { funcType := enclosingFuncType(encl.Node())