package helpers import ( "go/ast" "go/parser" "go/token" "io/fs" "path/filepath" "strings" "testing" ) // Go never frees syscall.NewCallback (hard cap ~2000). Creating one per // screenshot frame is a fatal "too many callback functions". func TestNewCallbackOnlyAtPackageScope(t *testing.T) { root := filepath.Join("..", "..") fset := token.NewFileSet() err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { name := d.Name() if name == ".git" || name == "vendor" || name == "testdata" { return filepath.SkipDir } return nil } if !strings.HasSuffix(path, ".go") { return nil } file, err := parser.ParseFile(fset, path, nil, 0) if err != nil { t.Errorf("parse %s: %v", path, err) return nil } for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok { continue } ast.Inspect(fn, func(n ast.Node) bool { if !isNewCallbackCall(n) { return true } name := fn.Name.Name if fn.Recv != nil { name = "method " + name } t.Errorf("%s: syscall.NewCallback inside %s; Go never frees these (cap ~2000)", fset.Position(n.Pos()), name) return true }) } return nil }) if err != nil { t.Fatal(err) } } func isNewCallbackCall(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return false } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return false } switch sel.Sel.Name { case "NewCallback", "NewCallbackCDecl": return true default: return false } }