mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add setboolcheck linter: flag map[T]bool used as sets (#42631)
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)) ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Added a new code analyzer to detect maps used as boolean sets and recommend more efficient alternatives for better performance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Scott Gress <scottmgress@gmail.com> Co-authored-by: Scott Gress <scott@fleetdm.com>
This commit is contained in:
parent
19aa7af3e6
commit
aedf366fc0
9 changed files with 595 additions and 0 deletions
|
|
@ -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{}`
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
27
tools/ci/setboolcheck/cmd/gclplugin/plugin.go
Normal file
27
tools/ci/setboolcheck/cmd/gclplugin/plugin.go
Normal file
|
|
@ -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
|
||||
}
|
||||
13
tools/ci/setboolcheck/go.mod
Normal file
13
tools/ci/setboolcheck/go.mod
Normal file
|
|
@ -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
|
||||
)
|
||||
10
tools/ci/setboolcheck/go.sum
Normal file
10
tools/ci/setboolcheck/go.sum
Normal file
|
|
@ -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=
|
||||
376
tools/ci/setboolcheck/setboolcheck.go
Normal file
376
tools/ci/setboolcheck/setboolcheck.go
Normal file
|
|
@ -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
|
||||
}
|
||||
13
tools/ci/setboolcheck/setboolcheck_test.go
Normal file
13
tools/ci/setboolcheck/setboolcheck_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
139
tools/ci/setboolcheck/testdata/src/example/example.go
vendored
Normal file
139
tools/ci/setboolcheck/testdata/src/example/example.go
vendored
Normal file
|
|
@ -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 }
|
||||
Loading…
Reference in a new issue