From 29b9a74cb89326915cdb361409a9c8021d049866 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:03:17 +0000 Subject: [PATCH 1/4] Initial plan From 243c1d2d2a17e0ec774ad320c534735d5639c995 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:15:35 +0000 Subject: [PATCH 2/4] fix: stop timesleepnocontext at callback FuncLit boundaries Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../timesleepnocontext/timesleepnocontext.go | 32 +++++++++++++++++++ .../timesleepnocontext/timesleepnocontext.go | 22 ++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go index e6be802d097..0bb43a8c091 100644 --- a/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go +++ b/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go @@ -2,6 +2,7 @@ package timesleepnocontext import ( "context" + "net/http" "time" ) @@ -58,6 +59,37 @@ func doWork(fn func(context.Context, time.Duration)) { fn(context.Background(), time.Second) } +// Good: callback context source is request-scoped, not the outer registration context. +func GoodHTTPHandleFuncCallbackInCtxFunc(ctx context.Context, d time.Duration) { + mux := http.NewServeMux() + mux.HandleFunc("/wait", func(w http.ResponseWriter, r *http.Request) { + _ = w + _ = r + time.Sleep(d) + }) + _ = ctx +} + +// Good: ordinary synchronous callback closure should not be attributed to outer ctx. +func GoodSyncCallbackInCtxFunc(ctx context.Context, d time.Duration) { + register(func() { + time.Sleep(d) + }) + _ = ctx +} + +// Bad: deferred closure shares outer context lifetime. +func BadDeferWithCtx(ctx context.Context, d time.Duration) { + defer func() { + time.Sleep(d) // want `use select with ctx\.Done\(\) instead of time\.Sleep to allow context cancellation` + }() + _ = ctx +} + +func register(fn func()) { + fn() +} + // Good: inline nolint suppresses intentional sleep. func GoodNoLint(ctx context.Context, d time.Duration) { _ = ctx diff --git a/pkg/linters/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/timesleepnocontext.go index d098895a3fa..45675acc9c8 100644 --- a/pkg/linters/timesleepnocontext/timesleepnocontext.go +++ b/pkg/linters/timesleepnocontext/timesleepnocontext.go @@ -11,6 +11,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" "github.com/github/gh-aw/pkg/linters/internal/astutil" "github.com/github/gh-aw/pkg/linters/internal/filecheck" @@ -51,12 +52,16 @@ func run(pass *analysis.Pass) (any, error) { } for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { - funcType := enclosingFuncType(encl.Node()) + funcNode := encl.Node() + funcType := enclosingFuncType(funcNode) if funcType == nil { continue } ctxParamName, hasCtx := contextParamName(pass, funcType) if !hasCtx { + if _, isFuncLit := funcNode.(*ast.FuncLit); isFuncLit && !isGoOrDeferClosure(encl) { + break + } continue } pass.Report(analysis.Diagnostic{ @@ -71,6 +76,21 @@ func run(pass *analysis.Pass) (any, error) { return nil, nil } +func isGoOrDeferClosure(funcLitCur inspector.Cursor) bool { + parent := funcLitCur.Parent() + call, ok := parent.Node().(*ast.CallExpr) + if !ok || call.Fun != funcLitCur.Node() { + return false + } + + switch parent.Parent().Node().(type) { + case *ast.GoStmt, *ast.DeferStmt: + return true + default: + return false + } +} + // isTimeSleepCall reports whether call is a call to time.Sleep. func isTimeSleepCall(pass *analysis.Pass, call *ast.CallExpr) bool { sel, ok := call.Fun.(*ast.SelectorExpr) From 10ba74265e3881fd3cb5a42f033742aaccbafde0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:18:29 +0000 Subject: [PATCH 3/4] fix: guard timesleepnocontext closure parent walk Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/linters/timesleepnocontext/timesleepnocontext.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/linters/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/timesleepnocontext.go index 45675acc9c8..b0804b44f85 100644 --- a/pkg/linters/timesleepnocontext/timesleepnocontext.go +++ b/pkg/linters/timesleepnocontext/timesleepnocontext.go @@ -83,7 +83,12 @@ func isGoOrDeferClosure(funcLitCur inspector.Cursor) bool { return false } - switch parent.Parent().Node().(type) { + grandparent := parent.Parent().Node() + if grandparent == nil { + return false + } + + switch grandparent.(type) { case *ast.GoStmt, *ast.DeferStmt: return true default: From 1262c0e9d0bdb638607eeaf3c072c0b34fec3577 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:51:04 +0000 Subject: [PATCH 4/4] fix: unwrap ParenExpr in isGoOrDeferClosure; add parenthesized go/defer fixtures Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../timesleepnocontext/timesleepnocontext.go | 16 +++++++++ .../timesleepnocontext/timesleepnocontext.go | 33 ++++++++++++++++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go index 0bb43a8c091..184f1f97e0d 100644 --- a/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go +++ b/pkg/linters/timesleepnocontext/testdata/src/timesleepnocontext/timesleepnocontext.go @@ -86,6 +86,22 @@ func BadDeferWithCtx(ctx context.Context, d time.Duration) { _ = ctx } +// Bad: parenthesized deferred closure shares outer context lifetime. +func BadDeferParenWithCtx(ctx context.Context, d time.Duration) { + defer (func() { + time.Sleep(d) // want `use select with ctx\.Done\(\) instead of time\.Sleep to allow context cancellation` + })() + _ = ctx +} + +// Bad: parenthesized goroutine closure shares outer context lifetime. +func BadGoParenWithCtx(ctx context.Context, d time.Duration) { + go (func() { + time.Sleep(d) // want `use select with ctx\.Done\(\) instead of time\.Sleep to allow context cancellation` + })() + _ = ctx +} + func register(fn func()) { fn() } diff --git a/pkg/linters/timesleepnocontext/timesleepnocontext.go b/pkg/linters/timesleepnocontext/timesleepnocontext.go index b0804b44f85..aca6183807d 100644 --- a/pkg/linters/timesleepnocontext/timesleepnocontext.go +++ b/pkg/linters/timesleepnocontext/timesleepnocontext.go @@ -77,13 +77,38 @@ func run(pass *analysis.Pass) (any, error) { } func isGoOrDeferClosure(funcLitCur inspector.Cursor) bool { - parent := funcLitCur.Parent() - call, ok := parent.Node().(*ast.CallExpr) - if !ok || call.Fun != funcLitCur.Node() { + // Walk up from the FuncLit, unwrapping any ParenExpr wrappers, to find the + // enclosing CallExpr. This handles parenthesized forms like defer (func(){})(). + cur := funcLitCur.Parent() + for { + if cur.Node() == nil { + return false + } + if _, ok := cur.Node().(*ast.ParenExpr); ok { + cur = cur.Parent() + continue + } + break + } + + call, ok := cur.Node().(*ast.CallExpr) + if !ok { + return false + } + // Unwrap ParenExpr from call.Fun and verify it resolves to our FuncLit. + callee := call.Fun + for { + if paren, ok := callee.(*ast.ParenExpr); ok { + callee = paren.X + } else { + break + } + } + if callee != funcLitCur.Node() { return false } - grandparent := parent.Parent().Node() + grandparent := cur.Parent().Node() if grandparent == nil { return false }