Skip to content

writebytestring (new 43rd linter): autofix emits non-compiling io.WriteString(w, s) for named string types + missing RunWithSugg [Content truncated due to length] #44187

Description

@github-actions

Summary

The newly-added 43rd analyzer writebytestring (pkg/linters/writebytestring/writebytestring.go) rewrites w.Write([]byte(s))io.WriteString(w, s). Its isStringType check accepts named string types (defined types whose underlying type is string), but io.WriteString's signature requires an exact string. For a named-string argument the emitted SuggestedFix (and the diagnostic's suggestion text) does not compile. The linter's own testdata encodes this broken suggestion, and it slips through because the test uses analysistest.Run (diagnostic-only) instead of analysistest.RunWithSuggestedFixes — the exact verification-parity gap previously filed/closed as #43313.

Not currently CI-enforced, so this is latent today (no -fix runs in cgo.yml), but it makes the autofix unsafe to apply and blocks safe enforcement of this linter.

The defect

isStringType (writebytestring.go:152-159) matches via t.Underlying():

func isStringType(pass *analysis.Pass, expr ast.Expr) bool {
	t := pass.TypesInfo.TypeOf(expr)
	if t == nil { return false }
	basic, ok := t.Underlying().(*types.Basic)
	return ok && basic.Kind() == types.String   // <-- also true for `type MyStr string`
}

The fix builder (writebytestring.go:184-194) then inserts io.WriteString(<w>, <s-text>) verbatim. But io.WriteString(w Writer, s string) takes a predeclared string. Go assignability: two distinct named types with identical underlying types are not assignable, and string is itself a named (predeclared) type. So passing a MyStr value yields cannot use s (variable of type MyStr) as string value in argument to io.WriteString.

Evidence — the testdata proves it

pkg/linters/writebytestring/testdata/src/writebytestring/writebytestring.go:21-27:

type myString string

func badNamedString() {
	var buf bytes.Buffer
	s := myString("hello")
	buf.Write([]byte(s)) // want `... io\.WriteString\(&buf, s\) ...`
}

The // want regex asserts the linter emits io.WriteString(&buf, s). Applied as a fix, that line is io.WriteString(&buf, s) with s of type myStringcompile error.

writebytestring_test.go:15 runs only analysistest.Run(...), which verifies diagnostic messages but never compiles the fixed output — so the broken fix passes tests.

Why the copied logic is wrong here (contrast with appendbytestring)

writebytestring mirrors appendbytestring (same isStringType via Underlying()). That is correct for appendbytestring because append(b, s...)'s string special-case accepts any operand with core type string (named strings included). It is wrong for writebytestring because io.WriteString's parameter is a concrete string, requiring an explicit conversion for named types.

All three existing SuggestedFix siblings verify their fixes:

  • appendbytestring_test.go:15analysistest.RunWithSuggestedFixes
  • stringsindexcontains_test.go:15analysistest.RunWithSuggestedFixes
  • sprintfint_test.go:15analysistest.RunWithSuggestedFixes

writebytestring regresses this convention — the same gap closed in #43313.

Impact

  • Applying the autofix (gopls / -fix) to any w.Write([]byte(namedStr)) site produces code that does not compile.
  • The linter cannot be safely added to cgo.yml enforcement (with -fix) until corrected.
  • 18 production Write([]byte(...)) sites exist (e.g. pkg/logger/logger.go:102, pkg/workflow/strings.go:171-172, pkg/parser/schedule_fuzzy_scatter.go:175); plain-string ones fix correctly, but any named-string arg would break.

Recommended fix

  1. Correct the fix for named strings — wrap when the arg is not the predeclared string:
// exact predeclared string vs. defined/named string
func isExactString(t types.Type) bool {
	b, ok := t.(*types.Basic) // *types.Named for `type MyStr string`
	return ok && b.Kind() == types.String
}
// ...
sExpr := sText
if st := pass.TypesInfo.TypeOf(strArg); st != nil && !isExactString(st) {
	sExpr = "string(" + sText + ")"   // io.WriteString(w, string(s))
}

Use sExpr in both the message suggestion and buildFix. (Alternatively, skip named-string args entirely — but wrapping preserves coverage.)

  1. Add fix verification — switch the test to analysistest.RunWithSuggestedFixes and add a .golden file (matching the 3 siblings). The golden file is compiled by the harness, so it would have caught this. Update the badNamedString expectation to io.WriteString(&buf, string(s)).

Validation checklist

  • type MyStr string arg produces a fix that compiles (io.WriteString(w, string(s))).
  • Plain-string args unchanged (io.WriteString(w, s)); verify logger.go:102 / workflow/strings.go:171-172 fixes still compile.
  • Add .golden + RunWithSuggestedFixes; go test ./pkg/linters/writebytestring/... passes and the golden compiles.
  • Value-receiver pointer-wrap path (&buf for bytes.Buffer) still correct alongside the string(...) wrap.

Effort: Small (single file + testdata/golden).


Sergo R60 — new 43rd-linter (writebytestring) audit. Registry delta 42→43. Reconciled: all prior sergo issues (incl. #43934 / #43683) closed; zero open pre-run.

Generated by 🤖 Sergo - Serena Go Expert · 270.9 AIC · ⌖ 14.1 AIC · ⊞ 5.8K ·

  • expires on Jul 14, 2026, 8:57 PM UTC-08:00

Metadata

Metadata

Labels

cookieIssue Monster Loves Cookies!sergo

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions