Skip to content

Commit 613657f

Browse files
committed
chbatchclose: add support for closure inside defer
1 parent 8f76930 commit 613657f

2 files changed

Lines changed: 127 additions & 20 deletions

File tree

passes/chbatchclose/chbatchclose.go

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,28 +146,76 @@ func (a *analyzer) handleAssign(pass *analysis.Pass, assign *ast.AssignStmt, usa
146146
}
147147
}
148148

149-
// handleDefer checks if a defer statement calls Close() or Abort() on a tracked Batch variable.
149+
// handleDefer checks if a defer statement calls Close() on a tracked Batch variable.
150+
// Two shapes are supported:
151+
// - direct selector call: defer batch.Close()
152+
// - immediately-invoked closure: defer func() { ... batch.Close() ... }()
153+
// (no params; nested FuncLits inside the closure body are not descended into)
150154
func handleDefer(deferStmt *ast.DeferStmt, usages map[string]*batchUsage) {
151155
call := deferStmt.Call
152-
sel, ok := call.Fun.(*ast.SelectorExpr)
153-
if !ok {
154-
return
155-
}
156-
varName := util.IdentName(sel.X)
157-
if varName == "" {
158-
return
159-
}
160-
u, exists := usages[varName]
161-
if !exists {
162-
return
163-
}
164156

165-
switch sel.Sel.Name {
166-
case "Close":
167-
u.deferredClose = true
157+
switch fun := call.Fun.(type) {
158+
case *ast.SelectorExpr:
159+
varName := util.IdentName(fun.X)
160+
if varName == "" {
161+
return
162+
}
163+
u, exists := usages[varName]
164+
if !exists {
165+
return
166+
}
167+
if fun.Sel.Name == "Close" {
168+
u.deferredClose = true
169+
}
170+
case *ast.FuncLit:
171+
// only no-arg closures are supported for the moment
172+
if fun.Type.Params != nil && len(fun.Type.Params.List) > 0 {
173+
return
174+
}
175+
if fun.Body == nil {
176+
return
177+
}
178+
handleDeferredClosure(fun.Body, usages)
168179
}
169180
}
170181

182+
// handleDeferredClosure walks the body of a deferred closure looking for
183+
// `<tracked>.Close()` calls. It does not descend into nested FuncLits, so
184+
// `defer func() { go func() { batch.Close() }() }()` and similar are not
185+
// treated as a valid defensive close.
186+
func handleDeferredClosure(body *ast.BlockStmt, usages map[string]*batchUsage) {
187+
ast.Inspect(body, func(n ast.Node) bool {
188+
if n == nil {
189+
return false
190+
}
191+
// don't descend into nested closures (goroutines, callbacks, etc.)
192+
if n != body {
193+
if _, ok := n.(*ast.FuncLit); ok {
194+
return false
195+
}
196+
}
197+
call, ok := n.(*ast.CallExpr)
198+
if !ok {
199+
return true
200+
}
201+
sel, ok := call.Fun.(*ast.SelectorExpr)
202+
if !ok {
203+
return true
204+
}
205+
if sel.Sel.Name != "Close" {
206+
return true
207+
}
208+
varName := util.IdentName(sel.X)
209+
if varName == "" {
210+
return true
211+
}
212+
if u, exists := usages[varName]; exists {
213+
u.deferredClose = true
214+
}
215+
return true
216+
})
217+
}
218+
171219
// handleReturn checks if any return value is a tracked Batch variable.
172220
func handleReturn(ret *ast.ReturnStmt, usages map[string]*batchUsage) {
173221
for _, result := range ret.Results {

passes/chbatchclose/testdata/src/testcases/testcases.go

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,16 +176,75 @@ func reassignValidValid() {
176176
_ = batch.Send()
177177
}
178178

179-
// closures are not supported
180-
// while the code below is in theory correct, it is very likely to be a bad pattern and should be flagged by the linter
181-
func deferCloseIsInClosure() {
182-
batch, err := conn.PrepareBatch(ctx, "INSERT INTO t") //want `clickhouse Batch batch must be closed defensively with defer batch\.Close\(\) after successful instantiation`
179+
// valid: defer with a closure that calls batch.Close() directly.
180+
// This is the IDE/errcheck-friendly pattern (allows wrapping the Close error, see test below).
181+
func validDeferCloseInClosure() {
182+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO t")
183183
if err != nil {
184184
return
185185
}
186186
defer func() { batch.Close() }()
187187
}
188188

189+
// valid: defer with closure handling the Close() error (real-world pattern of above).
190+
func validDeferCloseInClosureWithErrCheck() {
191+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO t")
192+
if err != nil {
193+
return
194+
}
195+
defer func() {
196+
if err = batch.Close(); err != nil {
197+
_ = err // log error
198+
}
199+
}()
200+
}
201+
202+
// invalid: defer with a closure that takes the batch as an argument.
203+
// this is correct in theory, but not supported for the moment (false positive)
204+
// Supporting this would require mapping closure params back to outer args. Assuming this pattern is not used for the moment.
205+
func invalidDeferCloseInClosureWithArg() {
206+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO t") //want `clickhouse Batch batch must be closed defensively with defer batch\.Close\(\) after successful instantiation`
207+
if err != nil {
208+
return
209+
}
210+
defer func(b driver.Batch) { _ = b.Close() }(batch)
211+
}
212+
213+
// invalid: Close is called from a goroutine nested inside the deferred closure.
214+
// The defer itself does not synchronously close the batch.
215+
// in theory correct-ish but for the moment I think it's not a good pattern
216+
func invalidDeferCloseInNestedGoroutine() {
217+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO t") //want `clickhouse Batch batch must be closed defensively with defer batch\.Close\(\) after successful instantiation`
218+
if err != nil {
219+
return
220+
}
221+
defer func() {
222+
go func() { _ = batch.Close() }()
223+
}()
224+
}
225+
226+
// invalid: 2 level of closure
227+
// correct in theory but not tracked for the moment - false positive
228+
func invalidDeferCloseInNestedCallback() {
229+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO t") //want `clickhouse Batch batch must be closed defensively with defer batch\.Close\(\) after successful instantiation`
230+
if err != nil {
231+
return
232+
}
233+
cb := func(fn func()) { fn() }
234+
defer func() {
235+
cb(func() { _ = batch.Close() })
236+
}()
237+
}
238+
239+
// invalid: deferred closure calls only Abort() (or other methods), not Close().
240+
func invalidDeferAbortInClosure() {
241+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO t") //want `clickhouse Batch batch must be closed defensively with defer batch\.Close\(\) after successful instantiation`
242+
if err != nil {
243+
return
244+
}
245+
defer func() { _ = batch.Abort() }()
246+
}
247+
189248
// the code below is in theory correct as all error cases are handled and result in a batch.Abort().
190249
// we still mark this as an error as it's not defensive. A defer batch.Close() would not change the code correctness and is easy to add.
191250
func invalidNotDefensive() error {

0 commit comments

Comments
 (0)