Summary
seenmapbool reports the same map[string]bool declaration two or more times when that declaration lives inside a function literal (closure). The linter's node filter visits both *ast.FuncDecl and *ast.FuncLit, while inspectBody independently recurses into nested closures via ast.Inspect — so a set-map declared in a closure is collected and reported once by the enclosing function's pass and again by the closure's own pass.
Location
pkg/linters/seenmapbool/seenmapbool.go
run node filter: lines 34-37 register both (*ast.FuncDecl)(nil) and (*ast.FuncLit)(nil).
inspectBody first pass: ast.Inspect(body, ...) at line 70 descends into nested *ast.FuncLit bodies (the closure returns true/keeps walking), so it collects candidate maps declared inside nested closures.
- Report site: lines 161-173
pass.ReportRangef(declNode, ...).
Mechanism
For a set-map declared in a closure nested in a function, insp.Preorder invokes the callback once for the enclosing FuncDecl and once for the nested FuncLit:
- FuncDecl visit →
inspectBody(funcDecl.Body) → its ast.Inspect walks the whole body, descending into the closure, collects seen as a candidate, and reports it.
- FuncLit visit →
inspectBody(funcLit.Body) → collects seen again and reports it a second time at the identical position.
A closure nested two levels deep is reported three times, etc. go/analysis does not de-duplicate diagnostics by position, so the user sees the warning N times.
Reproduction
package p
func Dedup(xs []string) []string {
unique := func(in []string) []string {
seen := make(map[string]bool) // reported TWICE
var out []string
for _, x := range in {
if !seen[x] {
seen[x] = true
out = append(out, x)
}
}
return out
}
return unique(xs)
}
seenmapbool emits the map[string]bool "seen" used as a set diagnostic twice at the seen := line.
Contrast with the correct sibling
pkg/linters/largefunc/largefunc.go uses the same {FuncDecl, FuncLit} node filter but does not recurse — each Preorder visit measures only that node's own Lbrace..Rbrace span (lines 70-73). Because it never re-walks nested closures, every function/closure is reported exactly once. seenmapbool is the odd one out: it recurses and gets re-visited.
Test coverage gap
pkg/linters/seenmapbool/testdata/src/seenmapbool/seenmapbool.go only exercises set-maps in top-level FuncDecls (BadSetBool, BadSetBoolLiteral) — no closure case — so analysistest (which counts // want matches) never observes the duplicate. A closure case must be added to catch this and any regression.
Impact
- Duplicate / noisy diagnostics whenever a
seen-style set-map lives in a closure (a very common Go idiom for local dedup helpers).
- A correctness blocker for any future CI enforcement that counts violations: a single offending closure inflates the count by 2-3x.
Recommended fix (directional)
Process each map declaration in exactly one scope. Either:
- Make
inspectBody's traversals stop at nested *ast.FuncLit boundaries (return false for nested FuncLit nodes) so each closure is handled solely by its own Preorder visit — but keep the write-disqualification pass able to observe a non-true write that happens inside a nested closure (else a closure write of false would be missed, turning the double-report into a false positive); or
- Keep the recursive single pass rooted only at the nearest enclosing function and de-duplicate reports by declaration position.
Add a closure-scoped testdata case (set-map inside a func() {...}) asserting a single diagnostic.
Effort
Small — node-filter/recursion scoping change plus one testdata function. Behavior is fully covered by analysistest.
Generated by 🤖 Sergo - Serena Go Expert · 357.9 AIC · ⌖ 10.9 AIC · ⊞ 5.8K · ◷
Summary
seenmapboolreports the samemap[string]booldeclaration two or more times when that declaration lives inside a function literal (closure). The linter's node filter visits both*ast.FuncDecland*ast.FuncLit, whileinspectBodyindependently recurses into nested closures viaast.Inspect— so a set-map declared in a closure is collected and reported once by the enclosing function's pass and again by the closure's own pass.Location
pkg/linters/seenmapbool/seenmapbool.gorunnode filter: lines 34-37 register both(*ast.FuncDecl)(nil)and(*ast.FuncLit)(nil).inspectBodyfirst pass:ast.Inspect(body, ...)at line 70 descends into nested*ast.FuncLitbodies (the closure returnstrue/keeps walking), so it collects candidate maps declared inside nested closures.pass.ReportRangef(declNode, ...).Mechanism
For a set-map declared in a closure nested in a function,
insp.Preorderinvokes the callback once for the enclosingFuncDecland once for the nestedFuncLit:inspectBody(funcDecl.Body)→ itsast.Inspectwalks the whole body, descending into the closure, collectsseenas a candidate, and reports it.inspectBody(funcLit.Body)→ collectsseenagain and reports it a second time at the identical position.A closure nested two levels deep is reported three times, etc.
go/analysisdoes not de-duplicate diagnostics by position, so the user sees the warning N times.Reproduction
seenmapboolemits themap[string]bool "seen" used as a setdiagnostic twice at theseen :=line.Contrast with the correct sibling
pkg/linters/largefunc/largefunc.gouses the same{FuncDecl, FuncLit}node filter but does not recurse — each Preorder visit measures only that node's ownLbrace..Rbracespan (lines 70-73). Because it never re-walks nested closures, every function/closure is reported exactly once.seenmapboolis the odd one out: it recurses and gets re-visited.Test coverage gap
pkg/linters/seenmapbool/testdata/src/seenmapbool/seenmapbool.goonly exercises set-maps in top-levelFuncDecls (BadSetBool,BadSetBoolLiteral) — no closure case — soanalysistest(which counts// wantmatches) never observes the duplicate. A closure case must be added to catch this and any regression.Impact
seen-style set-map lives in a closure (a very common Go idiom for local dedup helpers).Recommended fix (directional)
Process each map declaration in exactly one scope. Either:
inspectBody's traversals stop at nested*ast.FuncLitboundaries (returnfalsefor nestedFuncLitnodes) so each closure is handled solely by its own Preorder visit — but keep the write-disqualification pass able to observe a non-truewrite that happens inside a nested closure (else a closure write offalsewould be missed, turning the double-report into a false positive); orAdd a closure-scoped testdata case (set-map inside a
func() {...}) asserting a single diagnostic.Effort
Small — node-filter/recursion scoping change plus one testdata function. Behavior is fully covered by
analysistest.