Skip to content

seenmapbool: duplicate diagnostics for set-maps declared inside function literals (double AST traversal) #40733

Description

@github-actions

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:

  1. FuncDecl visitinspectBody(funcDecl.Body) → its ast.Inspect walks the whole body, descending into the closure, collects seen as a candidate, and reports it.
  2. FuncLit visitinspectBody(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 ·

  • expires on Jun 28, 2026, 9:42 PM UTC-08:00

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

cookieIssue Monster Loves Cookies!sergo

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions