Skip to content

lenstringzero precision: flag len() stored in an intermediate variable (n := len(s); n == 0) using the alias-tracking pattern th [Content truncated due to length] #37741

Description

@github-actions

Summary

The new lenstringzero analyzer only inspects direct len(s) == 0 / len(s) != 0 binary expressions. When the length is first stored in a local variable — n := len(s); if n == 0 { ... } — the comparison is not flagged. This false-negative is explicitly acknowledged in the linter's own ADR, and the fix has a proven in-repo template: the sibling tolowerequalfold analyzer just landed exactly this kind of alias-tracking in #37492.

  • Issue type: linter precision (false negative)
  • Linter: lenstringzero (pkg/linters/lenstringzero/lenstringzero.go)
  • Status: ⚠️ documented limitation, now fixable with a landed precedent

Evidence

The analyzer's run only matches a *ast.BinaryExpr whose operand is itself a len(...) *ast.CallExpr:

// pkg/linters/lenstringzero/lenstringzero.go:48-56
var lenArg ast.Expr
if isLenCall(expr.X) && isIntZero(expr.Y) {
    lenArg = lenCallArg(expr.X)
} else if isIntZero(expr.X) && isLenCall(expr.Y) {
    lenArg = lenCallArg(expr.Y)
}
if lenArg == nil {
    return
}

There is no handling for an identifier whose value is a previously-assigned len(...) result, so n := len(s); n == 0 slips through.

The ADR documents this as a known gap:

"Detection is limited to direct len(...) comparisons against the literal 0; an equivalent length stored in an intermediate variable (n := len(s); n == 0) is not flagged (false negatives)."docs/adr/37618-add-lenstringzero-linter.md, Consequences › Negative

Why this is actionable now (proven template)

The tolowerequalfold analyzer faced the same class of var-alias false-negative (Sergo sg29a1) and it was fixed in #37492 by adding an alias-collection pass:

tolowerequalfold alias-tracking precedent

pkg/linters/tolowerequalfold/tolowerequalfold.go now builds a map[types.Object]ast.Expr of local variables assigned a ToLower/ToUpper result:

func collectCaseConvAliases(pass *analysis.Pass) map[types.Object]ast.Expr {
    aliases := make(map[types.Object]ast.Expr)
    for _, file := range pass.Files {
        ast.Inspect(file, func(node ast.Node) bool {
            switch n := node.(type) {
            case *ast.AssignStmt: collectAliasesFromAssignStmt(pass, n, aliases)
            case *ast.ValueSpec:  collectAliasesFromValueSpec(pass, n, aliases)
            case *ast.IncDecStmt: /* invalidate */
            case *ast.RangeStmt:  /* invalidate */
            }
            return true
        })
    }
    return aliases
}

It correctly invalidates aliases that are mutated (IncDecStmt, re-assignment via RangeStmt) so a reassigned variable is not falsely flagged.

Recommendation

Mirror the tolowerequalfold approach in lenstringzero:

  1. Add a pre-pass that records local types.Objects assigned a single-argument len(...) call whose argument type-checks to an underlying string (reuse the existing isLenCall / type check).
  2. Invalidate an alias on any subsequent mutation (IncDecStmt, re-assignment, ++/--, address-of) — copy the invalidation cases from collectCaseConvAliases to avoid false positives.
  3. In the BinaryExpr handler, when an operand is an identifier resolving to a recorded len-of-string alias and the other operand is integer 0, report the same diagnostic.

Before / after intent

// not currently flagged -> should be flagged
n := len(s)        // s is a string
if n == 0 { ... }  // want: use s == \"\" to check for empty string

Validation checklist

  • Add an analysistest fixture under pkg/linters/lenstringzero/testdata with a n := len(s); n == 0 case carrying a // want annotation
  • Add a negative fixture where the alias is reassigned/incremented before the comparison (must NOT be flagged)
  • Ensure []byte/slice/array length aliases are still excluded (string-only)
  • go test ./pkg/linters/lenstringzero/... passes

Effort: Medium (single-package change with a directly-applicable in-repo template; ~30–60 LOC + fixtures).

References

Generated by 🤖 Sergo - Serena Go Expert · 236 AIC · ⌖ 15.2 AIC · ⊞ 6.5K ·

  • expires on Jun 14, 2026, 9:25 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