diff --git a/docs/adr/52920-add-packagelevelmutableslicemap-linter.md b/docs/adr/52920-add-packagelevelmutableslicemap-linter.md new file mode 100644 index 00000000000..931265b53cb --- /dev/null +++ b/docs/adr/52920-add-packagelevelmutableslicemap-linter.md @@ -0,0 +1,48 @@ +# ADR-52920: Add packagelevelmutableslicemap Static Analysis Linter + +**Date**: 2026-08-15 +**Status**: Draft +**Deciders**: pelikhan + +--- + +### Context + +Package-level `var` declarations of slices and maps in Go are shared across every goroutine and every call into the package for the lifetime of the process. When a function body mutates such a variable (via `append()` re-assignment, index assignment, or `delete()`), it risks data races under concurrent access and can leak state across otherwise-unrelated calls (e.g. between test runs or concurrent request handlers). + +Issue #52683 identified a concrete instance of this pattern in the codebase — a module-level mutable array (`_allResults`) reset and appended to across `main()` invocations, which is a latent cross-run data-corruption risk if the calling architecture ever changes to parallel dispatch. A broader codebase scan found additional package-level slice/map `var` declarations in `pkg/` that exhibit the same structural risk. The project's existing `go/analysis`-based linter suite in `pkg/linters/` already enforces similar structural constraints (e.g. `manualmutexunlock`, `goroutinemissingrecover`) and provides shared infrastructure (`analyzerutil`, `filecheck`, `nolint`) that makes adding a new checker straightforward. + +No built-in Go tool or existing third-party linter in the project's toolchain specifically targets this class of mutation at static analysis time. + +### Decision + +We will add a new custom `go/analysis` linter, `packagelevelmutableslicemap`, under `pkg/linters/packagelevelmutableslicemap/`. The analyzer scans package-scope `var` declarations with slice or map underlying types — including named wrapper types such as `type registry map[string]int` — and flags any mutation of those variables from inside a function body via `append()` re-assignment (including parallel assignments and appends whose source is a different slice), index assignment (`m[k] = v`, including nested `m[a][b] = v`), or `delete(m, k)`. Object identity via `types.Object` is used to correctly exempt local variables that shadow a package-level name. Mutations inside a top-level `init()` function are exempt, since `init` runs exactly once before any other code and is the idiomatic place to populate package-level state. A `//nolint:packagelevelmutableslicemap` directive on the mutating line suppresses the diagnostic, consistent with sibling linters. The analyzer is registered in `pkg/linters/registry.go` alongside the existing 64 analyzers. + +### Alternatives Considered + +#### Alternative 1: Rely on Go's built-in race detector (`-race`) + +The race detector (`go test -race` / `go run -race`) detects data races dynamically at runtime. It is thorough but requires actual concurrent execution of the conflicting code paths during the test run. Mutations of package-level slices/maps that happen sequentially — or that are only exercised under production load — will not trigger it. It also does not catch the cross-call state-leak pattern (where sequential mutations corrupt state for a later call), which is a distinct risk from a data race. Static analysis at lint time catches the structural smell unconditionally, regardless of how tests are structured. + +#### Alternative 2: Code review convention and documentation + +The team could document a convention prohibiting mutable package-level slice/map state and rely on human reviewers to enforce it. This has zero tooling cost but scales poorly: conventions drift, reviewers miss cases under time pressure, and new contributors are unaware of the rule until they encounter a review comment. Given that the project already invests in automated linting for analogous structural constraints, automation is the consistent choice. + +### Consequences + +#### Positive +- Package-level mutable slice/map bugs are caught at lint time — before they manifest as intermittent data races or cross-test contamination in production or CI. +- The implementation follows established patterns in `pkg/linters/`, reusing `analyzerutil`, `filecheck`, and `nolint` infrastructure; the incremental cost per new analyzer is low, and the approach is familiar to contributors. +- The `//nolint:packagelevelmutableslicemap` escape hatch allows intentionally synchronized global state (e.g., a mutex-protected registry) to opt out without refactoring. + +#### Negative +- The analyzer will produce false positives for package-level state that is intentionally and correctly synchronized (e.g., a `sync.Mutex`-protected global cache). Each such site requires a `//nolint` directive or a refactor; if there are many such sites in the codebase, this creates short-term maintenance work. +- The linter cannot detect all mutable-shared-state hazards — it only covers `var` declarations with `slice` or `map` underlying types mutated directly. Aliased mutations (e.g., passing the global slice to a helper that appends internally) are not flagged. + +#### Neutral +- The analyzer count in `pkg/linters/doc.go` and `README.md` increases from 64 to 65; the `spec_test.go` count assertion is updated accordingly. This is a minor bookkeeping change with no behavioral impact. +- Existing code in the repository that triggers the new linter will begin failing lint checks once the analyzer is active; a sweep of existing violations may be needed before enabling the linter in CI. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* diff --git a/pkg/linters/README.md b/pkg/linters/README.md index 93320b81104..08d35f89122 100644 --- a/pkg/linters/README.md +++ b/pkg/linters/README.md @@ -41,6 +41,7 @@ This package currently provides custom Go analyzers in the following subpackages - `osgetenvlibrary` — reports `os.Getenv` calls in library packages (`pkg/*`) where environment access should be injected. - `osexitinlibrary` — reports `os.Exit` calls in library packages (`pkg/*`) where process termination should be delegated to `cmd/*` entry points. - `ossetenvlibrary` — reports `os.Setenv` calls in library packages (`pkg/*`) where side effects should be isolated. +- `packagelevelmutableslicemap` — reports package-level (file/package-scope) `var` slice/map declarations mutated from inside a function body via `append()` re-assignment, index assignment, or `delete()`. Mutations inside a top-level `init()` are exempt. - `panic-in-library-code` — reports `panic()` calls in library packages (`pkg/*`) where errors should be returned instead. - `rawloginlib` — reports direct usage of the standard `log` package in library packages, where `pkg/logger` should be used. - `regexpcompileinfunction` — reports `regexp.MustCompile` / `regexp.Compile` calls inside functions that should be package-level. @@ -133,6 +134,7 @@ environment variable and gates findings on the recorded execution hit count for | `osgetenvlibrary` | Custom `go/analysis` analyzer that flags `os.Getenv` usage in library packages | | `osexitinlibrary` | Custom `go/analysis` analyzer that flags `os.Exit` usage in library packages | | `ossetenvlibrary` | Custom `go/analysis` analyzer that flags `os.Setenv` usage in library packages | +| `packagelevelmutableslicemap` | Custom `go/analysis` analyzer that flags package-level slice/map `var` declarations mutated from inside a function body via `append()` re-assignment, index assignment, or `delete()` | | `panic-in-library-code` | Custom `go/analysis` analyzer that flags `panic()` usage in library packages | | `rawloginlib` | Custom `go/analysis` analyzer that flags standard-library `log` package calls in library packages | | `regexpcompileinfunction` | Custom `go/analysis` analyzer that flags regexp compilation inside function bodies | @@ -270,6 +272,7 @@ _ = trimleftright.Analyzer - `github.com/github/gh-aw/pkg/linters/osgetenvlibrary` — os-getenv-library analyzer subpackage - `github.com/github/gh-aw/pkg/linters/osexitinlibrary` — os-exit-in-library analyzer subpackage - `github.com/github/gh-aw/pkg/linters/ossetenvlibrary` — os-setenv-library analyzer subpackage +- `github.com/github/gh-aw/pkg/linters/packagelevelmutableslicemap` — package-level-mutable-slice-map analyzer subpackage - `github.com/github/gh-aw/pkg/linters/panic-in-library-code` — panic-in-library-code analyzer subpackage - `github.com/github/gh-aw/pkg/linters/rawloginlib` — raw-log-in-lib analyzer subpackage - `github.com/github/gh-aw/pkg/linters/regexpcompileinfunction` — regexp-compile-in-function analyzer subpackage diff --git a/pkg/linters/doc.go b/pkg/linters/doc.go index 34d83c11327..f1b1e9ec857 100644 --- a/pkg/linters/doc.go +++ b/pkg/linters/doc.go @@ -1,6 +1,6 @@ // Package linters is a namespace for gh-aw's custom Go analysis linters. // -// All 64 active analyzers: +// All 65 active analyzers: // // - appendbytestring — flags append(b, []byte(s)...) calls where s is a string that can be simplified to append(b, s...) // - appendoneelement — flags append(s, []T{x}...) calls where a single-element slice literal is spread and can be simplified to append(s, x) @@ -37,6 +37,7 @@ // - osexitinlibrary — flags os.Exit calls in library packages // - osgetenvlibrary — flags os.Getenv calls in library packages // - ossetenvlibrary — flags os.Setenv calls in library packages +// - packagelevelmutableslicemap — reports mutation of package-level slice/map variables from inside function bodies, which risks data races and cross-call state leaks // - panic-in-library-code — flags panic() calls in library packages // - rawloginlib — flags direct usage of the standard log package in library packages // - regexpcompileinfunction — flags regexp.MustCompile/Compile calls inside functions diff --git a/pkg/linters/packagelevelmutableslicemap/packagelevelmutableslicemap.go b/pkg/linters/packagelevelmutableslicemap/packagelevelmutableslicemap.go new file mode 100644 index 00000000000..3c63121979b --- /dev/null +++ b/pkg/linters/packagelevelmutableslicemap/packagelevelmutableslicemap.go @@ -0,0 +1,244 @@ +// Package packagelevelmutableslicemap implements a Go analysis linter that +// flags package-level (file/package-scope) var declarations of slices or maps +// that are mutated from inside a function body via append re-assignment, +// index assignment, or delete(). +// +// Package-level mutable slices/maps are shared across every goroutine and +// every call into the package for the lifetime of the process. Mutating one +// from inside a function — rather than storing the state on a struct or +// returning fresh values — risks data races under concurrent access and can +// leak state between unrelated calls. +// +// Mutations inside a top-level init() function are not reported: init runs +// exactly once before any other code, so it is the idiomatic place to +// populate package-level state. +package packagelevelmutableslicemap + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" + + "github.com/github/gh-aw/pkg/linters/internal/analyzerutil" + "github.com/github/gh-aw/pkg/linters/internal/astutil" + "github.com/github/gh-aw/pkg/linters/internal/filecheck" + "github.com/github/gh-aw/pkg/linters/internal/nolint" +) + +// Analyzer is the package-level-mutable-slice-map analysis pass. +var Analyzer = analyzerutil.New("packagelevelmutableslicemap", "reports mutation of package-level slice/map variables from inside function bodies, which risks data races and cross-call state leaks", run) + +func run(pass *analysis.Pass) (any, error) { + insp, err := astutil.Inspector(pass) + if err != nil { + return nil, err + } + noLintIndex, err := nolint.Index(pass) + if err != nil { + return nil, err + } + generatedFiles, err := filecheck.Index(pass) + if err != nil { + return nil, err + } + + targets := collectPackageLevelSliceMapVars(pass) + if len(targets) == 0 { + return nil, nil + } + + for cur := range insp.Root().Preorder((*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)) { + if isInInitFunction(cur) { + continue + } + analyzeNode(pass, cur.Node(), targets, generatedFiles, noLintIndex) + } + return nil, nil +} + +// isInInitFunction reports whether cur is inside a top-level init() function. +// Only top-level (no receiver) init functions are recognized; methods named +// init are ordinary methods and are not exempt. +func isInInitFunction(cur inspector.Cursor) bool { + for encl := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { + decl, ok := encl.Node().(*ast.FuncDecl) + if !ok { + // Innermost enclosing function is a literal (e.g. a goroutine + // started from init), which is not exempt. + return false + } + return decl.Recv == nil && decl.Name != nil && decl.Name.Name == "init" + } + return false +} + +// collectPackageLevelSliceMapVars scans the top-level declarations of every +// file in the package and returns the set of package-scope var objects whose +// underlying type is a slice or a map (including named wrapper types such as +// `type registry map[string]int`), keyed by their declared name. +func collectPackageLevelSliceMapVars(pass *analysis.Pass) map[types.Object]string { + targets := make(map[types.Object]string) + for _, file := range pass.Files { + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.VAR { + continue + } + for _, spec := range genDecl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for _, name := range valueSpec.Names { + if name.Name == "_" { + continue + } + obj := pass.TypesInfo.Defs[name] + if obj == nil { + continue + } + t := obj.Type() + if t == nil { + continue + } + switch t.Underlying().(type) { + case *types.Slice, *types.Map: + targets[obj] = name.Name + } + } + } + } + } + return targets +} + +func analyzeNode(pass *analysis.Pass, n ast.Node, targets map[types.Object]string, generatedFiles filecheck.GeneratedIndex, noLintIndex nolint.DirectiveIndex) { + switch stmt := n.(type) { + case *ast.AssignStmt: + analyzeAssignStmt(pass, stmt, targets, generatedFiles, noLintIndex) + case *ast.ExprStmt: + analyzeExprStmt(pass, stmt, targets, generatedFiles, noLintIndex) + } +} + +func analyzeAssignStmt(pass *analysis.Pass, stmt *ast.AssignStmt, targets map[types.Object]string, generatedFiles filecheck.GeneratedIndex, noLintIndex nolint.DirectiveIndex) { + if name, ok := matchAppendReassign(pass, stmt, targets); ok { + report(pass, stmt.Pos(), name, "append() re-assignment", generatedFiles, noLintIndex) + return + } + if name, ok := matchIndexAssign(pass, stmt, targets); ok { + report(pass, stmt.Pos(), name, "index assignment", generatedFiles, noLintIndex) + } +} + +func analyzeExprStmt(pass *analysis.Pass, stmt *ast.ExprStmt, targets map[types.Object]string, generatedFiles filecheck.GeneratedIndex, noLintIndex nolint.DirectiveIndex) { + call, ok := stmt.X.(*ast.CallExpr) + if !ok { + return + } + if !isBuiltinCall(pass, call, "delete") || len(call.Args) != 2 { + return + } + name, ok := targetBaseName(pass, call.Args[0], targets) + if !ok { + return + } + report(pass, stmt.Pos(), name, "delete()", generatedFiles, noLintIndex) +} + +// matchAppendReassign reports whether stmt re-assigns a tracked package-level +// target from an append() call, returning its declared name. Every LHS/RHS +// pair is inspected so parallel assignments such as +// `globalSlice, err = append(globalSlice, v), nil` are matched too, and the +// append arguments are not required to reference the target itself, so +// `globalSlice = append(otherSlice, v)` is matched as well. +func matchAppendReassign(pass *analysis.Pass, stmt *ast.AssignStmt, targets map[types.Object]string) (string, bool) { + for i, lhs := range stmt.Lhs { + lhsIdent, ok := lhs.(*ast.Ident) + if !ok { + continue + } + name, tracked := targets[pass.TypesInfo.Uses[lhsIdent]] + if !tracked { + continue + } + rhs, ok := astutil.RhsExprForIndex(stmt.Rhs, i) + if !ok { + continue + } + call, ok := rhs.(*ast.CallExpr) + if !ok || len(call.Args) == 0 { + continue + } + if isBuiltinCall(pass, call, "append") { + return name, true + } + } + return "", false +} + +// matchIndexAssign reports whether stmt assigns into m[k] for a tracked +// package-level map/slice target m. +func matchIndexAssign(pass *analysis.Pass, stmt *ast.AssignStmt, targets map[types.Object]string) (string, bool) { + for _, lhs := range stmt.Lhs { + idxExpr, ok := lhs.(*ast.IndexExpr) + if !ok { + continue + } + if name, ok := targetBaseName(pass, idxExpr, targets); ok { + return name, true + } + } + return "", false +} + +// isBuiltinCall reports whether call invokes the named Go builtin. +func isBuiltinCall(pass *analysis.Pass, call *ast.CallExpr, name string) bool { + ident, ok := call.Fun.(*ast.Ident) + if !ok || ident.Name != name { + return false + } + builtin, ok := pass.TypesInfo.Uses[ident].(*types.Builtin) + return ok && builtin.Name() == name +} + +// targetBaseName reports whether expr is rooted at an identifier referring to +// a tracked package-level target, returning its declared name. Nested index +// expressions such as nested[a][b] resolve to their base identifier so +// mutations of nested collections are reported too. +func targetBaseName(pass *analysis.Pass, expr ast.Expr, targets map[types.Object]string) (string, bool) { + for { + switch e := expr.(type) { + case *ast.Ident: + obj := pass.TypesInfo.Uses[e] + if obj == nil { + return "", false + } + name, ok := targets[obj] + return name, ok + case *ast.IndexExpr: + expr = e.X + case *ast.ParenExpr: + expr = e.X + default: + return "", false + } + } +} + +func report(pass *analysis.Pass, pos token.Pos, varName, kind string, generatedFiles filecheck.GeneratedIndex, noLintIndex nolint.DirectiveIndex) { + position := pass.Fset.PositionFor(pos, false) + if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) { + return + } + if nolint.HasDirectiveForLinter(position, noLintIndex, "packagelevelmutableslicemap") { + return + } + pass.Report(analysis.Diagnostic{ + Pos: pos, + Message: "package-level slice/map variable " + varName + " is mutated via " + kind + "; mutating shared package state risks data races and can leak state across calls", + }) +} diff --git a/pkg/linters/packagelevelmutableslicemap/packagelevelmutableslicemap_test.go b/pkg/linters/packagelevelmutableslicemap/packagelevelmutableslicemap_test.go new file mode 100644 index 00000000000..17d6c6c8aed --- /dev/null +++ b/pkg/linters/packagelevelmutableslicemap/packagelevelmutableslicemap_test.go @@ -0,0 +1,16 @@ +//go:build !integration + +package packagelevelmutableslicemap_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + + "github.com/github/gh-aw/pkg/linters/packagelevelmutableslicemap" +) + +func TestAnalyzer(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, packagelevelmutableslicemap.Analyzer, "packagelevelmutableslicemap") +} diff --git a/pkg/linters/packagelevelmutableslicemap/testdata/src/packagelevelmutableslicemap/packagelevelmutableslicemap.go b/pkg/linters/packagelevelmutableslicemap/testdata/src/packagelevelmutableslicemap/packagelevelmutableslicemap.go new file mode 100644 index 00000000000..4a5314e1f65 --- /dev/null +++ b/pkg/linters/packagelevelmutableslicemap/testdata/src/packagelevelmutableslicemap/packagelevelmutableslicemap.go @@ -0,0 +1,89 @@ +package packagelevelmutableslicemap + +import "errors" + +type registry map[string]int + +type queue []int + +var globalSlice []int +var globalMap = map[string]int{} +var otherSlice = []int{7} +var nestedMap = map[string]map[string]int{} +var namedMap = registry{} +var namedSlice queue +var initialized []int +var readOnlySlice = []int{1, 2, 3} +var suppressedSlice []int + +func init() { + initialized = append(initialized, 1) + globalMap["seed"] = 1 + delete(globalMap, "seed") + go func() { + globalSlice = append(globalSlice, 2) // want `package-level slice/map variable globalSlice is mutated via append\(\) re-assignment; mutating shared package state risks data races and can leak state across calls` + }() +} + +func appendToGlobal(v int) { + globalSlice = append(globalSlice, v) // want `package-level slice/map variable globalSlice is mutated via append\(\) re-assignment; mutating shared package state risks data races and can leak state across calls` +} + +func appendFromOtherSlice(v int) { + globalSlice = append(otherSlice, v) // want `package-level slice/map variable globalSlice is mutated via append\(\) re-assignment; mutating shared package state risks data races and can leak state across calls` +} + +func appendInParallelAssign(v int) error { + var err error + globalSlice, err = append(globalSlice, v), errors.New("boom") // want `package-level slice/map variable globalSlice is mutated via append\(\) re-assignment; mutating shared package state risks data races and can leak state across calls` + return err +} + +func setInGlobalMap(k string, v int) { + globalMap[k] = v // want `package-level slice/map variable globalMap is mutated via index assignment; mutating shared package state risks data races and can leak state across calls` +} + +func setInNestedMap(outer, inner string, v int) { + nestedMap[outer][inner] = v // want `package-level slice/map variable nestedMap is mutated via index assignment; mutating shared package state risks data races and can leak state across calls` +} + +func setInNamedMap(k string, v int) { + namedMap[k] = v // want `package-level slice/map variable namedMap is mutated via index assignment; mutating shared package state risks data races and can leak state across calls` +} + +func appendToNamedSlice(v int) { + namedSlice = append(namedSlice, v) // want `package-level slice/map variable namedSlice is mutated via append\(\) re-assignment; mutating shared package state risks data races and can leak state across calls` +} + +func deleteFromGlobalMap(k string) { + delete(globalMap, k) // want `package-level slice/map variable globalMap is mutated via delete\(\); mutating shared package state risks data races and can leak state across calls` +} + +func deleteFromNamedMap(k string) { + delete(namedMap, k) // want `package-level slice/map variable namedMap is mutated via delete\(\); mutating shared package state risks data races and can leak state across calls` +} + +func readGlobal() int { + sum := 0 + for _, v := range readOnlySlice { + sum += v + } + return sum +} + +func appendSuppressed(v int) { + suppressedSlice = append(suppressedSlice, v) //nolint:packagelevelmutableslicemap +} + +func shadowedSliceIsFine() { + globalSlice := []int{1, 2} + globalSlice = append(globalSlice, 3) + _ = globalSlice +} + +func shadowedMapIsFine() { + globalMap := map[string]int{} + globalMap["a"] = 1 + delete(globalMap, "a") + _ = globalMap +} diff --git a/pkg/linters/registry.go b/pkg/linters/registry.go index 2514ca2e45d..26809aeb70e 100644 --- a/pkg/linters/registry.go +++ b/pkg/linters/registry.go @@ -38,6 +38,7 @@ import ( "github.com/github/gh-aw/pkg/linters/osexitinlibrary" "github.com/github/gh-aw/pkg/linters/osgetenvlibrary" "github.com/github/gh-aw/pkg/linters/ossetenvlibrary" + "github.com/github/gh-aw/pkg/linters/packagelevelmutableslicemap" panicinlibrarycode "github.com/github/gh-aw/pkg/linters/panic-in-library-code" "github.com/github/gh-aw/pkg/linters/rawloginlib" "github.com/github/gh-aw/pkg/linters/regexpcompileinfunction" @@ -111,6 +112,7 @@ var allAnalyzers = []*analysis.Analyzer{ osexitinlibrary.Analyzer, osgetenvlibrary.Analyzer, ossetenvlibrary.Analyzer, + packagelevelmutableslicemap.Analyzer, panicinlibrarycode.Analyzer, rawloginlib.Analyzer, regexpcompileinfunction.Analyzer, diff --git a/pkg/linters/spec_test.go b/pkg/linters/spec_test.go index bdc9a30f548..12651442b2c 100644 --- a/pkg/linters/spec_test.go +++ b/pkg/linters/spec_test.go @@ -46,6 +46,7 @@ import ( "github.com/github/gh-aw/pkg/linters/osexitinlibrary" "github.com/github/gh-aw/pkg/linters/osgetenvlibrary" "github.com/github/gh-aw/pkg/linters/ossetenvlibrary" + "github.com/github/gh-aw/pkg/linters/packagelevelmutableslicemap" panicinlibrarycode "github.com/github/gh-aw/pkg/linters/panic-in-library-code" "github.com/github/gh-aw/pkg/linters/rawloginlib" "github.com/github/gh-aw/pkg/linters/regexpcompileinfunction" @@ -90,7 +91,7 @@ type docAnalyzer struct { } // documentedAnalyzers returns the analyzer subpackages documented in the README -// "Public API > Subpackages" table. The README documents 64 analyzers +// "Public API > Subpackages" table. The README documents 65 analyzers // subpackages (the non-analyzer `internal` helper subpackage is excluded because // it exposes no Analyzer). // @@ -99,7 +100,7 @@ type docAnalyzer struct { // appendbytestring, appendoneelement, bytesbufferstring, bytescomparestring, contextcancelnotdeferred, ctxbackground, deferinloop, errorfwrapv, excessivefuncparams, errormessage, // errortypeassertion, errstringmatch, execcommandwithoutcontext, fileclosenotdeferred, fmterrorfnoverbs, fprintlnsprintf, // globwalkignorederror, goroutinemissingrecover, hardcodedfilepath, httpnoctx, httprespbodyclose, httpstatuscode, ioutildeprecated, jsonmarshalignoredeerror, largefunc, lenstringsplit, lenstringzero, -// logfatallibrary, manualmutexunlock, mapclearloop, mapdeletecheck, nilctxpassed, osexitinlibrary, osgetenvlibrary, ossetenvlibrary, panic-in-library-code, rawloginlib, +// logfatallibrary, manualmutexunlock, mapclearloop, mapdeletecheck, nilctxpassed, osexitinlibrary, osgetenvlibrary, ossetenvlibrary, packagelevelmutableslicemap, panic-in-library-code, rawloginlib, // regexpcompileinfunction, regexpdynamicpattern, seenmapbool, sortslice, sprintferrdot, sprintferrorsnew, sprintfbool, sprintfint, ssljson, // strconvparseignorederror, stringbytesroundtrip, stringreplaceminusone, stringsconcatloop, stringscountcontains, stringsindexcontains, stringsindexhasprefix, stringsjoinone, timeafterleak, timesleepnocontext, timenowsub, // tolowerequalfold, trimleftright, uncheckedflushreturn, uncheckedtypeassertion, walkfuncerrshadow, wgdonenotdeferred, writebytestring @@ -140,6 +141,7 @@ func documentedAnalyzers() []docAnalyzer { {"osexitinlibrary", osexitinlibrary.Analyzer}, {"osgetenvlibrary", osgetenvlibrary.Analyzer}, {"ossetenvlibrary", ossetenvlibrary.Analyzer}, + {"packagelevelmutableslicemap", packagelevelmutableslicemap.Analyzer}, {"panic-in-library-code", panicinlibrarycode.Analyzer}, {"rawloginlib", rawloginlib.Analyzer}, {"regexpcompileinfunction", regexpcompileinfunction.Analyzer},