go/analysis: add runtime.Cleanup pass
This pass checks for various pitfalls when using runtime cleanups.
Mostly, it makes sure that the pointer isn't closed over by the function
or the argument to the function, effectively disabling the mechanism.
diff --git a/go/analysis/passes/cleanup/cleanup.go b/go/analysis/passes/cleanup/cleanup.go
new file mode 100644
index 0000000..9144949
--- /dev/null
+++ b/go/analysis/passes/cleanup/cleanup.go
@@ -0,0 +1,110 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package cleanup defines an Analyzer that detects common mistakes
+// involving runtime cleanups.
+package cleanup
+
+import (
+ "go/ast"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
+)
+
+const Doc = "check for common mistakes involving runtime cleanups"
+
+var Analyzer = &analysis.Analyzer{
+ Name: "cleanup",
+ Doc: Doc,
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/cleanup",
+ Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer},
+ Run: run,
+}
+
+func run(pass *analysis.Pass) (any, error) {
+ index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+
+ cleanup := index.Object("runtime", "AddCleanup")
+ if !index.Used(cleanup) {
+ return nil, nil
+ }
+
+ for c := range index.Calls(cleanup) {
+ call := c.Node().(*ast.CallExpr)
+ ptr := ast.Unparen(call.Args[0])
+ // Calls and receives yield a value that is unrelated to its identifier
+ // construction, so ignore their identifiers
+ // TODO: should we disallow these rather than just ignore them?
+ switch node := ptr.(type) {
+ case *ast.CallExpr:
+ continue
+ case *ast.UnaryExpr:
+ if node.Op == token.ARROW {
+ continue
+ }
+ default:
+ }
+ ptrVariables := make(map[types.Object]bool)
+ for id := range c.Child(call.Args[0]).Preorder((*ast.Ident)(nil)) {
+ // walk every identifier in the ptr expression and build a list of variables
+ // that shouldn't be closed over by any function literal.
+ // We're walking over an expression with no block statements, so any object found will
+ // be to variables in this scope.
+ obj := pass.TypesInfo.Uses[id.Node().(*ast.Ident)]
+ if _, ok := obj.(*types.PkgName); ok {
+ continue
+ }
+ // field or method selection
+ scope := obj.Parent()
+ if scope == nil {
+ continue
+ }
+ ptrVariables[obj] = true
+ }
+ cur := c.Child(call.Args[2])
+ cursor:
+ for id := range cur.Preorder((*ast.Ident)(nil)) {
+ ident := id.Node().(*ast.Ident)
+ obj := pass.TypesInfo.Uses[ident]
+ if obj == nil {
+ continue
+ }
+ if ptrVariables[obj] {
+ for id := range cur.Enclosing((*ast.SelectorExpr)(nil)) {
+ sel := id.Node().(*ast.SelectorExpr)
+ if pass.TypesInfo.Selections[sel].Indirect() {
+ // we referenced a identifier from ptr, but it is a selection
+ // that dereferences, so the variable isn't closed over
+ continue cursor
+ }
+ // we only care about the innermost selection
+ break
+ }
+ pass.ReportRangef(ident, "argument closes over %s, cleanup will never run or might panic", ident.Name)
+ break
+ }
+ }
+ cur = c.Child(call.Args[1])
+ if fn, ok := ast.Unparen(call.Args[1]).(*ast.FuncLit); ok {
+ // don't look at the parameters for function literals
+ cur, _ = cur.FindNode(fn.Body)
+ }
+ for id := range cur.Preorder((*ast.Ident)(nil)) {
+ ident := id.Node().(*ast.Ident)
+ obj := pass.TypesInfo.Uses[ident]
+ if obj == nil {
+ continue
+ }
+ if ptrVariables[obj] {
+ pass.ReportRangef(ident, "function closes over %s, cleanup will never run", ident.Name)
+ break
+ }
+ }
+ }
+ return nil, nil
+}
diff --git a/go/analysis/passes/cleanup/cleanup_test.go b/go/analysis/passes/cleanup/cleanup_test.go
new file mode 100644
index 0000000..e40f63f
--- /dev/null
+++ b/go/analysis/passes/cleanup/cleanup_test.go
@@ -0,0 +1,17 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package cleanup_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/go/analysis/passes/cleanup"
+)
+
+func Test(t *testing.T) {
+ testdata := analysistest.TestData()
+ analysistest.Run(t, testdata, cleanup.Analyzer, "a")
+}
diff --git a/go/analysis/passes/cleanup/testdata/src/a/a.go b/go/analysis/passes/cleanup/testdata/src/a/a.go
new file mode 100644
index 0000000..b74aae9
--- /dev/null
+++ b/go/analysis/passes/cleanup/testdata/src/a/a.go
@@ -0,0 +1,47 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file contains tests for the cleanup checker.
+
+package a
+
+import "runtime"
+
+func PointerAsArg(i *int) {
+ runtime.AddCleanup(i, func(*int) {}, i) // want `argument closes over i`
+}
+
+type netFD struct {
+ fd int
+}
+
+func nfdclose(fd int) {
+}
+
+func (nfd *netFD) close(fd int) {
+}
+
+func NormalUse(nfd *netFD) {
+ runtime.AddCleanup(nfd, func(fd int) { // No error, this is the intended usage
+ }, nfd.fd)
+}
+
+func NormalUseFunc(nfd *netFD) {
+ runtime.AddCleanup(nfd, nfdclose, nfd.fd) // Using a function defined elsewhere is fine
+}
+
+func Address(nfd *netFD) {
+ runtime.AddCleanup(nfd, func(*int) {
+ }, &nfd.fd) // want `argument closes over nfd`
+}
+
+func ClosureMethod(nfd *netFD) {
+ runtime.AddCleanup(nfd, nfd.close, 0) // want `function closes over nfd, cleanup will never run`
+}
+
+func Closure(nfd *netFD) {
+ runtime.AddCleanup(nfd, func(fd int) {
+ nfd.close(nfd.fd) // want `function closes over nfd, cleanup will never run`
+ }, 0)
+}
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Couple of notes on this:
This would have been trivial to implement with SSA, where I could look the free variables up when the closure is made, but I couldn't justify building an entire program for every function on the off-chance that it would use `runtime.AddCleanup`. Is there a way to selectively build SSA for a package under analysis?
I'm unsure what needs to happen to get this to run in vet. Where exactly does this need importing?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
go/analysis: add runtime.Cleanup pass
This pass checks for various pitfalls when using runtime cleanups.
Mostly, it makes sure that the pointer isn't closed over by the function
or the argument to the function, effectively disabling the mechanism.If you plan for this analyzer to be incorporated into cmd/vet, then it will require an approved proposal to create the new public API of its package. If this is just for gopls, it can be moved into a subdirectory of x/tools/gopls/internal/analysis without a proposal. However, even in that case, a tracking issue is preferred since it allows for public discussion and prioritization and ties together all the related code changes.
const Doc = "check for common mistakes involving runtime cleanups"The documentation should go into more detail. See adjacent analyzers for examples.
I'd rather not review the complex logic below until I know exactly what it is supposed to do.
if !index.Used(cleanup) {
return nil, nil
}This isn't needed: the index.Calls sequence will be empty in this case.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
go/analysis: add runtime.Cleanup pass
This pass checks for various pitfalls when using runtime cleanups.
Mostly, it makes sure that the pointer isn't closed over by the function
or the argument to the function, effectively disabling the mechanism.If you plan for this analyzer to be incorporated into cmd/vet, then it will require an approved proposal to create the new public API of its package. If this is just for gopls, it can be moved into a subdirectory of x/tools/gopls/internal/analysis without a proposal. However, even in that case, a tracking issue is preferred since it allows for public discussion and prioritization and ties together all the related code changes.
I've moved the logic into gopls for now and will create an issue for vet inclusion.
const Doc = "check for common mistakes involving runtime cleanups"The documentation should go into more detail. See adjacent analyzers for examples.
I'd rather not review the complex logic below until I know exactly what it is supposed to do.
Have added a doc.go file, hopefully that explains the intent here a bit.
This isn't needed: the index.Calls sequence will be empty in this case.
Done
for c := range index.Calls(cleanup) {Daniel MorsingcurCall
Done
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Put a proposal up at https://go.dev/issues/75066
Thanks. I'll wait for the discussion to conclude the question of whether the vet check is warranted or the dynamic check is sufficient. Proposal issues FTW!
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |