From aedf366fc0af8cce6c8b58b3b5a1f299b7dbc491 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky <2685025+getvictor@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:26:24 -0500 Subject: [PATCH] Add setboolcheck linter: flag map[T]bool used as sets (#42631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: add a check for a common issue I see humans and AI agents making, so that we don't have to waste time on it in code reviews. Resolves #42635 Note: This lint check has been mostly AI generated. I don't think it needs a thorough review because it is not production code and not even test code. Any issues will be obvious from usage by contributors. Add a custom go/analysis analyzer that detects map[T]bool variables used as sets (where only the literal `true` is ever assigned) and suggests using map[T]struct{} instead, which is the idiomatic Go approach for sets — zero memory for values and unambiguous semantics. The analyzer minimizes false positives by: - Only flagging when ALL indexed assignments use the literal `true` - Skipping variables initialized from function calls (unknown source) - Skipping variables reassigned from unknown sources - Skipping function parameters and exported package-level variables - Skipping range loop variables Integrated as an incremental linter (new/changed code only) to avoid breaking existing code. Running this check on our whole codebase flags valid cases: ``` cmd/fleet/serve.go:306:2: map[string]bool used as a set; consider map[string]struct{} instead (setboolcheck) allowedHostIdentifiers := map[string]bool{ ^ cmd/fleetctl/fleetctl/generate_gitops.go:189:3: map[string]bool used as a set; consider map[string]struct{} instead (setboolcheck) handled := make(map[string]bool, len(renames)*2) ^ cmd/fleetctl/fleetctl/generate_gitops.go:1593:2: map[uint]bool used as a set; consider map[uint]struct{} instead (setboolcheck) m := make(map[uint]bool, len(ids)) ``` ## Summary by CodeRabbit * **Chores** * Added a new code analyzer to detect maps used as boolean sets and recommend more efficient alternatives for better performance. --------- Co-authored-by: Claude Co-authored-by: Scott Gress Co-authored-by: Scott Gress --- .claude/CLAUDE.md | 10 + .custom-gcl.yml | 3 + .golangci-incremental.yml | 4 + tools/ci/setboolcheck/cmd/gclplugin/plugin.go | 27 ++ tools/ci/setboolcheck/go.mod | 13 + tools/ci/setboolcheck/go.sum | 10 + tools/ci/setboolcheck/setboolcheck.go | 376 ++++++++++++++++++ tools/ci/setboolcheck/setboolcheck_test.go | 13 + .../testdata/src/example/example.go | 139 +++++++ 9 files changed, 595 insertions(+) create mode 100644 tools/ci/setboolcheck/cmd/gclplugin/plugin.go create mode 100644 tools/ci/setboolcheck/go.mod create mode 100644 tools/ci/setboolcheck/go.sum create mode 100644 tools/ci/setboolcheck/setboolcheck.go create mode 100644 tools/ci/setboolcheck/setboolcheck_test.go create mode 100644 tools/ci/setboolcheck/testdata/src/example/example.go diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7c6e2d5bfc..b519e2f6ea 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -13,3 +13,13 @@ MYSQL_TEST=1 go test -run TestFunctionName ./server/datastore/mysql/... # Generate boilerplate for a new frontend component, including associated stylesheet, tests, and storybook ./frontend/components/generate -n RequiredPascalCaseNameOfTheComponent -p optional/path/to/desired/parent/directory +``` + +## Go code style + +- Prefer `map[T]struct{}` over `map[T]bool` when the map represents a set. +- Convert a map's keys to a slice with `slices.Collect(maps.Keys(m))` instead of manually appending in a loop. +- Avoid `time.Sleep` in tests. Prefer `testing/synctest` to run code in a fake-clock bubble, or use polling helpers, channels, or `require.Eventually`. +- Use `require` and `assert` from `github.com/stretchr/testify` in tests. +- Use `t.Context()` in tests instead of `context.Background()`. +- Use `any` instead of `interface{}` diff --git a/.custom-gcl.yml b/.custom-gcl.yml index d6676782c2..9036aca5da 100644 --- a/.custom-gcl.yml +++ b/.custom-gcl.yml @@ -6,3 +6,6 @@ plugins: - module: "go.uber.org/nilaway" import: "go.uber.org/nilaway/cmd/gclplugin" version: v0.0.0-20260126174828-99d94caaf043 # fixed version for reproducible builds - latest as of 2026-01-29 + - module: "github.com/fleetdm/fleet/v4/tools/ci/setboolcheck" + import: "github.com/fleetdm/fleet/v4/tools/ci/setboolcheck/cmd/gclplugin" + path: "tools/ci/setboolcheck" diff --git a/.golangci-incremental.yml b/.golangci-incremental.yml index 6c01f41445..55392dadaf 100644 --- a/.golangci-incremental.yml +++ b/.golangci-incremental.yml @@ -14,6 +14,7 @@ linters: - modernize - testifylint - nilaway + - setboolcheck settings: gosec: # Only enable rules that are too noisy on existing code but valuable for new code. @@ -38,6 +39,9 @@ linters: # Settings must be a "map from string to string" to mimic command line flags: the keys are # flag names and the values are the values to the particular flags. include-pkgs: "github.com/fleetdm/fleet/v4" + setboolcheck: + type: module + description: Flags map[T]bool used as sets; suggests map[T]struct{} instead. exclusions: generated: strict rules: diff --git a/tools/ci/setboolcheck/cmd/gclplugin/plugin.go b/tools/ci/setboolcheck/cmd/gclplugin/plugin.go new file mode 100644 index 0000000000..86f5423d74 --- /dev/null +++ b/tools/ci/setboolcheck/cmd/gclplugin/plugin.go @@ -0,0 +1,27 @@ +// Package gclplugin provides the golangci-lint module plugin entry point. +package gclplugin + +import ( + "github.com/fleetdm/fleet/v4/tools/ci/setboolcheck" + "github.com/golangci/plugin-module-register/register" + "golang.org/x/tools/go/analysis" +) + +func init() { + register.Plugin("setboolcheck", New) +} + +// New returns the golangci-lint plugin for the setboolcheck analyzer. +func New(_ any) (register.LinterPlugin, error) { + return &plugin{}, nil +} + +type plugin struct{} + +func (p *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { + return []*analysis.Analyzer{setboolcheck.Analyzer}, nil +} + +func (p *plugin) GetLoadMode() string { + return register.LoadModeTypesInfo +} diff --git a/tools/ci/setboolcheck/go.mod b/tools/ci/setboolcheck/go.mod new file mode 100644 index 0000000000..a20e7dd8bc --- /dev/null +++ b/tools/ci/setboolcheck/go.mod @@ -0,0 +1,13 @@ +module github.com/fleetdm/fleet/v4/tools/ci/setboolcheck + +go 1.26.1 + +require ( + github.com/golangci/plugin-module-register v0.1.2 + golang.org/x/tools v0.42.0 +) + +require ( + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sync v0.19.0 // indirect +) diff --git a/tools/ci/setboolcheck/go.sum b/tools/ci/setboolcheck/go.sum new file mode 100644 index 0000000000..3bbf9d7108 --- /dev/null +++ b/tools/ci/setboolcheck/go.sum @@ -0,0 +1,10 @@ +github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= +github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= diff --git a/tools/ci/setboolcheck/setboolcheck.go b/tools/ci/setboolcheck/setboolcheck.go new file mode 100644 index 0000000000..f2a518df31 --- /dev/null +++ b/tools/ci/setboolcheck/setboolcheck.go @@ -0,0 +1,376 @@ +// Package setboolcheck defines an analyzer that flags map[T]bool variables +// used as sets, suggesting map[T]struct{} instead. +// +// The heuristic: a map[T]bool variable (local or unexported package-level) +// is flagged when, in the enclosing package, every observed write via +// indexed assignments or composite literal elements uses the literal true +// as the value. This avoids false positives on maps that genuinely store +// bool values (e.g. map[string]bool{"a": true, "b": false}). +package setboolcheck + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// Analyzer checks for map[T]bool that should be map[T]struct{}. +var Analyzer = &analysis.Analyzer{ + Name: "setboolcheck", + Doc: "checks for map[T]bool that should be map[T]struct{} (set pattern)", + URL: "https://github.com/fleetdm/fleet", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +// varInfo tracks analysis state for a single map[T]bool variable. +type varInfo struct { + pos token.Pos // declaration position (for diagnostics) + hasAssign bool // at least one indexed assignment or composite literal element + allTrue bool // every assigned value is the literal true + tainted bool // variable was assigned from an unknown source + skip bool // function parameter or exported package-level var +} + +func run(pass *analysis.Pass) (any, error) { + insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + vars := make(map[*types.Var]*varInfo) + + // Phase 1: collect map[T]bool variable declarations. + declFilter := []ast.Node{ + (*ast.FuncDecl)(nil), + (*ast.GenDecl)(nil), + (*ast.AssignStmt)(nil), + (*ast.RangeStmt)(nil), + } + + insp.Preorder(declFilter, func(n ast.Node) { + switch node := n.(type) { + case *ast.FuncDecl: + registerParams(pass, vars, node) + case *ast.GenDecl: + if node.Tok == token.VAR { + registerVarDecl(pass, vars, node) + } + case *ast.AssignStmt: + if node.Tok == token.DEFINE { + registerShortVarDecl(pass, vars, node) + } + case *ast.RangeStmt: + if node.Tok == token.DEFINE { + registerRangeVarDecl(pass, vars, node) + } + } + }) + + // Phase 2: scan assignments for indexed writes, full reassignments, and escapes. + escapeFilter := []ast.Node{ + (*ast.AssignStmt)(nil), + (*ast.CallExpr)(nil), + } + + insp.Preorder(escapeFilter, func(n ast.Node) { + switch node := n.(type) { + case *ast.AssignStmt: + checkAssignment(pass, vars, node) + // Taint tracked maps that appear on the RHS of assignments (aliasing). + taintEscapedInAssign(pass, vars, node) + case *ast.CallExpr: + // Taint tracked maps passed as function arguments. + taintEscapedInCall(pass, vars, node) + } + }) + + // Phase 3: report. + for obj, info := range vars { + if info.skip || info.tainted { + continue + } + if info.hasAssign && info.allTrue { + keyStr := obj.Type().Underlying().(*types.Map).Key().String() + pass.Reportf(info.pos, "map[%s]bool used as a set; consider map[%s]struct{} instead", keyStr, keyStr) + } + } + + return nil, nil +} + +// registerParams marks function parameters so they are skipped. +func registerParams(pass *analysis.Pass, vars map[*types.Var]*varInfo, fn *ast.FuncDecl) { + if fn.Type.Params == nil { + return + } + for _, field := range fn.Type.Params.List { + for _, name := range field.Names { + if obj := asVar(pass, name); obj != nil && isBoolMap(obj.Type()) { + vars[obj] = &varInfo{pos: name.Pos(), allTrue: true, skip: true} + } + } + } +} + +// registerVarDecl handles "var x map[T]bool" and "var x = make(map[T]bool)". +func registerVarDecl(pass *analysis.Pass, vars map[*types.Var]*varInfo, decl *ast.GenDecl) { + for _, spec := range decl.Specs { + vs, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for i, name := range vs.Names { + obj := asVar(pass, name) + if obj == nil || !isBoolMap(obj.Type()) { + continue + } + info := &varInfo{pos: name.Pos(), allTrue: true} + + if obj.Parent() == pass.Pkg.Scope() && obj.Exported() { + info.skip = true + } + + if len(vs.Names) == len(vs.Values) && i < len(vs.Values) { + classifyInit(pass, info, vs.Values[i]) + } else if len(vs.Values) > 0 { + info.tainted = true // multi-return or mismatched initializers + } + vars[obj] = info + } + } +} + +// registerShortVarDecl handles "x := make(map[T]bool)" and similar. +func registerShortVarDecl(pass *analysis.Pass, vars map[*types.Var]*varInfo, stmt *ast.AssignStmt) { + for i, lhs := range stmt.Lhs { + ident, ok := lhs.(*ast.Ident) + if !ok { + continue + } + obj := asVar(pass, ident) + if obj == nil || !isBoolMap(obj.Type()) { + continue + } + info := &varInfo{pos: ident.Pos(), allTrue: true} + if i < len(stmt.Rhs) { + classifyInit(pass, info, stmt.Rhs[i]) + } else { + info.tainted = true // multi-return + } + vars[obj] = info + } +} + +// registerRangeVarDecl marks range loop variables as tainted (unknown source). +func registerRangeVarDecl(pass *analysis.Pass, vars map[*types.Var]*varInfo, stmt *ast.RangeStmt) { + for _, expr := range []ast.Expr{stmt.Key, stmt.Value} { + if expr == nil { + continue + } + ident, ok := expr.(*ast.Ident) + if !ok { + continue + } + if obj := asVar(pass, ident); obj != nil && isBoolMap(obj.Type()) { + vars[obj] = &varInfo{pos: ident.Pos(), allTrue: true, tainted: true} + } + } +} + +// classifyInit determines whether an initializer is safe (make, composite literal) +// or unknown (function call, variable copy, etc.). +func classifyInit(pass *analysis.Pass, info *varInfo, expr ast.Expr) { + switch e := expr.(type) { + case *ast.CallExpr: + if isBuiltinMake(pass, e) { + return // make(map[T]bool) - safe, no values yet + } + info.tainted = true // some other function call + + case *ast.CompositeLit: + checkCompositeLitValues(pass, info, e) + + default: + info.tainted = true // variable copy, field access, etc. + } +} + +// checkCompositeLitValues checks that all values in a map literal are the literal true. +func checkCompositeLitValues(pass *analysis.Pass, info *varInfo, lit *ast.CompositeLit) { + for _, elt := range lit.Elts { + kv, ok := elt.(*ast.KeyValueExpr) + if !ok { + info.allTrue = false + break + } + info.hasAssign = true + if !isBuiltinTrue(pass, kv.Value) { + info.allTrue = false + break + } + } +} + +// checkAssignment examines assignments for indexed map writes and full reassignments. +func checkAssignment(pass *analysis.Pass, vars map[*types.Var]*varInfo, stmt *ast.AssignStmt) { + // Check indexed assignments: m[k] = value + for i, lhs := range stmt.Lhs { + indexExpr, ok := lhs.(*ast.IndexExpr) + if !ok { + continue + } + obj := identVar(pass, indexExpr.X) + if obj == nil { + continue + } + info, ok := vars[obj] + if !ok { + continue + } + info.hasAssign = true + if rhs := correspondingRHS(stmt, i); rhs == nil || !isBuiltinTrue(pass, rhs) { + info.allTrue = false + } + } + + // Detect full reassignment of a tracked variable: m = ... + if stmt.Tok != token.ASSIGN { + return + } + for i, lhs := range stmt.Lhs { + // Skip index expressions - already handled above. + if _, isIndex := lhs.(*ast.IndexExpr); isIndex { + continue + } + obj := identVar(pass, lhs) + if obj == nil { + continue + } + info, ok := vars[obj] + if !ok { + continue + } + rhs := correspondingRHS(stmt, i) + if rhs == nil { + info.tainted = true + continue + } + switch e := rhs.(type) { + case *ast.CallExpr: + if isBuiltinMake(pass, e) { + continue // re-init with make - safe + } + info.tainted = true + case *ast.CompositeLit: + checkCompositeLitValues(pass, info, e) + default: + info.tainted = true + } + } +} + +// taintEscapedInAssign taints tracked maps that appear on the RHS of an assignment +// to another named variable, since the alias could be used to write non-true values. +// Assignments to the blank identifier (_ = m) are ignored since they are no-ops. +func taintEscapedInAssign(pass *analysis.Pass, vars map[*types.Var]*varInfo, stmt *ast.AssignStmt) { + for i, rhs := range stmt.Rhs { + obj := identVar(pass, rhs) + if obj == nil { + continue + } + if _, ok := vars[obj]; !ok { + continue + } + // Only taint if the LHS is a real named variable (not blank identifier). + if i < len(stmt.Lhs) { + if lhsIdent, ok := stmt.Lhs[i].(*ast.Ident); ok && lhsIdent.Name == "_" { + continue + } + } + vars[obj].tainted = true + } +} + +// taintEscapedInCall taints tracked maps passed as function arguments, +// since the callee could write non-true values through the reference. +func taintEscapedInCall(pass *analysis.Pass, vars map[*types.Var]*varInfo, call *ast.CallExpr) { + // Skip the builtin make - its arguments are types, not map values. + if isBuiltinMake(pass, call) { + return + } + for _, arg := range call.Args { + obj := identVar(pass, arg) + if obj == nil { + continue + } + if info, ok := vars[obj]; ok { + info.tainted = true + } + } +} + +// --- helpers --- + +// asVar resolves an ast.Ident to a *types.Var, or returns nil. +func asVar(pass *analysis.Pass, ident *ast.Ident) *types.Var { + obj := pass.TypesInfo.ObjectOf(ident) + if obj == nil { + return nil + } + v, ok := obj.(*types.Var) + if !ok { + return nil + } + return v +} + +// identVar extracts the *types.Var from an expression if it is a simple identifier. +func identVar(pass *analysis.Pass, expr ast.Expr) *types.Var { + ident, ok := expr.(*ast.Ident) + if !ok { + return nil + } + return asVar(pass, ident) +} + +// isBoolMap reports whether t is (or has underlying type) map[T]bool. +func isBoolMap(t types.Type) bool { + m, ok := t.Underlying().(*types.Map) + if !ok { + return false + } + b, ok := m.Elem().(*types.Basic) + return ok && b.Kind() == types.Bool +} + +// isBuiltinTrue reports whether expr is the predeclared identifier "true" +// (not a shadowed local variable named "true"). +func isBuiltinTrue(pass *analysis.Pass, expr ast.Expr) bool { + ident, ok := expr.(*ast.Ident) + if !ok { + return false + } + obj := pass.TypesInfo.ObjectOf(ident) + return obj == types.Universe.Lookup("true") +} + +// isBuiltinMake reports whether call is a call to the predeclared "make" function. +func isBuiltinMake(pass *analysis.Pass, call *ast.CallExpr) bool { + ident, ok := call.Fun.(*ast.Ident) + if !ok { + return false + } + obj := pass.TypesInfo.ObjectOf(ident) + return obj == types.Universe.Lookup("make") +} + +// correspondingRHS returns the RHS expression matching LHS index i, +// or nil for multi-value returns. +func correspondingRHS(stmt *ast.AssignStmt, i int) ast.Expr { + if len(stmt.Lhs) == len(stmt.Rhs) { + return stmt.Rhs[i] + } + return nil +} diff --git a/tools/ci/setboolcheck/setboolcheck_test.go b/tools/ci/setboolcheck/setboolcheck_test.go new file mode 100644 index 0000000000..4ecad6c4be --- /dev/null +++ b/tools/ci/setboolcheck/setboolcheck_test.go @@ -0,0 +1,13 @@ +package setboolcheck_test + +import ( + "testing" + + "github.com/fleetdm/fleet/v4/tools/ci/setboolcheck" + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, setboolcheck.Analyzer, "example") +} diff --git a/tools/ci/setboolcheck/testdata/src/example/example.go b/tools/ci/setboolcheck/testdata/src/example/example.go new file mode 100644 index 0000000000..a31e3920b0 --- /dev/null +++ b/tools/ci/setboolcheck/testdata/src/example/example.go @@ -0,0 +1,139 @@ +package example + +// --- Should be flagged --- + +func setWithMake() { + seen := make(map[string]bool) // want `map\[string\]bool used as a set` + seen["a"] = true + seen["b"] = true + _ = seen +} + +func setWithCompositeLiteral() { + allowed := map[string]bool{"x": true, "y": true} // want `map\[string\]bool used as a set` + _ = allowed +} + +func setWithIntKey() { + ids := make(map[int]bool) // want `map\[int\]bool used as a set` + ids[1] = true + ids[2] = true + _ = ids +} + +func setReInitWithMake() { + m := make(map[string]bool) // want `map\[string\]bool used as a set` + m["a"] = true + m = make(map[string]bool) + m["b"] = true + _ = m +} + +func setUsedInClosure() { + seen := make(map[string]bool) // want `map\[string\]bool used as a set` + items := []string{"a", "b"} + for _, item := range items { + seen[item] = true + } + fn := func() { + seen["c"] = true + } + fn() + _ = seen +} + +func setVarDecl() { + var seen map[string]bool // want `map\[string\]bool used as a set` + seen = make(map[string]bool) + seen["a"] = true + _ = seen +} + +var packageLevelSet = map[string]bool{"a": true, "b": true} // want `map\[string\]bool used as a set` + +// --- Should NOT be flagged --- + +func genuineBoolMap() { + m := make(map[string]bool) + m["connected"] = true + m["disconnected"] = false + _ = m +} + +func boolMapFromCompositeLiteral() { + m := map[string]bool{"a": true, "b": false} + _ = m +} + +func boolMapFromCompositeLiteralWithChange() { + m := map[string]bool{"a": true, "b": true} + m["b"] = false + _ = m +} + +func boolMapFromFunctionCall() { + m := getBoolMap() + m["x"] = true + _ = m +} + +func boolMapReassignedFromFunc() { + m := make(map[string]bool) + m["a"] = true + m = getBoolMap() + _ = m +} + +func boolMapParameter(m map[string]bool) { + m["x"] = true +} + +func boolMapCopied() { + m := make(map[string]bool) // m escapes via alias, so not flagged + m["a"] = true + other := m // other is tainted (assigned from variable), so not flagged + _ = other +} + +func noIndexedAssignments() { + m := make(map[string]bool) + _ = m["a"] + _ = m +} + +func boolMapFromRangeValue() { + source := map[string]map[string]bool{ + "x": {"a": true}, + } + for _, v := range source { + v["b"] = true + } +} + +func boolMapAssignedVariable() { + m := make(map[string]bool) + val := true + m["a"] = val // not a literal true + _ = m +} + +var ExportedMap = map[string]bool{"a": true} // exported package-level - skip + +func boolMapPassedToFunc() { + m := make(map[string]bool) // m escapes via function call, so not flagged + m["a"] = true + consumeMap(m) +} + +func consumeMap(_ map[string]bool) {} + +func multiReturnVar() { + var a, b = getMultiMap() // multi-return: both should be tainted + a["x"] = true + b["y"] = true + _, _ = a, b +} + +func getMultiMap() (map[string]bool, map[string]bool) { return nil, nil } + +func getBoolMap() map[string]bool { return nil }