Skip to content

stringsjoinone autofix drops precedence: bare unparenthesized element replacement emits non-compiling / behavior-changed code in [Content truncated due to length] #47467

Description

@github-actions

Summary

stringsjoinone rewrites strings.Join([]string{x}, sep) to the bare element x, but it injects x's raw source text with no parentheses as a full-expression replacement of the entire call. When the element x is a compound (lower-precedence) expression and the strings.Join(...) call is itself an operand of a tighter postfix operator (index [i], slice [i:j]), the autofix silently changes semantics or produces non-compiling code.

This is the same autofix-correctness family as the already-filed #44865 (tolowerequalfold not behavior-preserving) and the fprintlnsprintf trailing-newline fix — but here the failure mode includes an outright compile error. The fix is not CI-enforced (stringsjoinone is absent from the cgo.yml LINTER_FLAGS gate), so this is a latent gap that only surfaces when a developer runs -fix.

Location

  • pkg/linters/stringsjoinone/stringsjoinone.go:88-96 — the SuggestedFix sets NewText: []byte(elemText), replacing call.Pos()..call.End() (the whole strings.Join(...) expression) with the unparenthesized element text.
  • pkg/linters/stringsjoinone/stringsjoinone.go:131matchSingleElementStringSlice returns astutil.NodeText(pass.Fset, elem) verbatim, so any operator structure inside the element is carried over as-is.

Evidence / failing cases

Both inputs are flagged (single-element []string literal, constant separator) and rewritten by -fix:

Case A — non-compiling result. a, b are string:

first := strings.Join([]string{a + b}, "")[0]

The fix replaces strings.Join([]string{a + b}, "") with a + b, yielding:

first := a + b[0]   // b[0] is a byte -> string + byte -> does not compile

Case B — silent behavior change (compiles).

tail := strings.Join([]string{a + b}, "")[1:]

becomes

tail := a + b[1:]   // original meaning: (a+b)[1:]; now: a + (b[1:])

This compiles but returns a different string.

Root cause

The rewrite treats elemText as if it were a primary expression. strings.Join(...) is a primary expression (highest precedence), so replacing it with a bare binary expression (a + b) is only safe when the call sits in a context that does not bind tighter than the element's top-level operator. Postfix index/slice (and, for a compound element, any tighter enclosing operator) break that assumption.

Contrast the sibling simplification linters, which are correct precisely because they inject the extracted sub-expression into precedence-safe positions:

  • appendoneelement -> append(sliceText, elemText) (comma-delimited arg position)
  • stringreplaceminusone -> pkg.ReplaceAll(sText, oldText, newText) (arg positions)
  • bytesbufferstring -> receiverText + ".String()" where the receiver is always a selector operand (primary expr)

stringsjoinone is the only one doing a bare full-expression substitution, so it is the only one exposed.

Why the tests miss it

stringsjoinone_test.go uses analysistest.RunWithSuggestedFixes, but the golden only exercises simple elements (name, "hello", s) in return/assignment contexts (testdata/src/stringsjoinone/stringsjoinone.go.golden:7,12,17). No case combines a compound element with a precedence-sensitive parent (index/slice), so RWSF passes while the fix is unsound.

Recommendation

Make the replacement precedence-safe. Options, simplest first:

  1. Parenthesize when the element is not already atomic: if elem is anything other than *ast.Ident, *ast.BasicLit, *ast.ParenExpr, *ast.SelectorExpr, *ast.IndexExpr, or *ast.CallExpr, emit "(" + elemText + ")". This is always behavior-preserving and cheap.
  2. Or gate the SuggestedFix (keep the diagnostic, drop the fix) when the strings.Join call's parent node is an *ast.IndexExpr/*ast.SliceExpr (or more generally binds tighter than the element's root operator).

Add golden coverage for a compound element in an indexed and a sliced context to lock the fix.

Validation checklist

  • -fix on Case A produces compiling code
  • -fix on Case B preserves the original value (a+b)[1:]
  • New RWSF golden cases: strings.Join([]string{a+b}, "")[0] and ...[1:]
  • Existing goldens (bare identifier/literal elements) still emit no extra parens

Effort

Small — one guard in the fix builder (~5 lines) plus two golden cases.

References: §29980457915

Generated by 🤖 Sergo - Serena Go Expert · age00 260.1 AIC · ⌖ 13.9 AIC · ⊞ 5.8K ·

  • expires on Jul 29, 2026, 9:00 PM UTC-08:00

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