Summary
Registry delta this run: 43 → 44 analyzers (grep -c Analyzer cmd/linters/main.go = 44; pkg/linters/doc.go:3 = "All 44 active analyzers"). The new 44th linter is bytescomparestring (cmd/linters/main.go:68), which flags string(a) == string(b) / string(a) != string(b) for []byte operands and suggests bytes.Equal(a, b) / !bytes.Equal(a, b).
Good news first — this linter is well-built and avoids every trap prior new linters fell into:
So there is no compile bug and no FP/FN here. The problem is the linter's stated rationale.
Finding 1 (primary): the "allocates" claim is factually wrong under the gc compiler
Both diagnostic messages and the package doc assert an allocation cost:
bytescomparestring.go:83 — "string(%s) == string(%s) allocates; use bytes.Equal(...) instead"
bytescomparestring.go:90 — "... allocates; use !bytes.Equal(...) instead"
bytescomparestring.go:1-3 (package doc) — "...to avoid unnecessary allocations."
pkg/linters/doc.go:6 — mirrors the same framing.
Under the gc compiler this is not true. A string([]byte) conversion whose result does not escape and is used only in a comparison is lowered to runtime.slicebytetostringtmp, which returns a string header aliasing the original backing array with no copy and no allocation. Comparison is exactly one of the documented non-escaping cases (runtime/string.go documents if string(b) == "..." / map-key / comparison usage as the safe non-allocating forms). In string(a) == string(b) both temporaries are consumed by the == and never escape, so gc allocates for neither side.
Net: string(a) == string(b) (both []byte) and bytes.Equal(a, b) are allocation-equivalent under gc. The rewrite's genuine value is idiomatic clarity and explicit intent (and robustness if the expression is later refactored so a conversion escapes) — not allocation avoidance.
Recommendation: reword the two messages and the package doc + doc.go line to drop the allocation claim and lead with clarity/idiom, e.g. "string(a) == string(b) is a []byte comparison written the long way; use bytes.Equal(a, b) for clearer intent". Sibling lenstringsplit.go:75 legitimately says "allocates a []string" because strings.Split genuinely allocates a slice — that message is fine; this one is the outlier.
Finding 2 (secondary, coupled to #1): enforce-readiness — but as a style lint
Production violation sites: zero. Broad scan grep -rEn 'string\([^)]*\) *(==|!=) *string\(' pkg/ cmd/ --include='*.go' (excluding tests/testdata and the linter's own doc strings) returns no hits. Combined with the RWSF test + nolint/test-skip parity, the analyzer is structurally CI-ready and is currently absent from cgo.yml LINTER_FLAGS (.github/workflows/cgo.yml:1208, 17 linters enforced), consistent with the rest of the un-enforced simplification family (appendbytestring, writebytestring, stringsindexcontains).
Recommendation: it can be added to the enforced set, but frame the enforcement as a readability/idiom gate, not a performance gate (see Finding 1). Do not justify CI enforcement on allocation grounds.
Finding 3 (minor): import-insertion misses the single non-grouped-import shape
addBytesImportEdit (bytescomparestring.go:139-150) only reuses an import block when genDecl.Lparen.IsValid() (grouped import ( ... )). A file whose only import is a single non-parenthesized import "fmt" skips that branch and falls through to the standalone insert (:152-157), producing a second top-level import "bytes" decl:
package foo
import "bytes"
import "fmt"
This is valid Go (compiles; gofmt/goimports would merge it), so it is low-severity, but it is untested — the two fixtures cover only "grouped import already present" and "no imports at all" (testdata/.../noimport.go). Consider folding the single-import case into the grouped branch (or add a fixture) so the fix emits one grouped block.
Validation checklist
Effort: S (messages/doc are one-line edits; import-edge is optional and localized to addBytesImportEdit).
Sergo R61 · strategy: registry-delta new-linter audit (44th=bytescomparestring) + novel message-premise validation. sg60a1=#44187 verified landed+closed; all prior sergo issues closed.
Generated by 🤖 Sergo - Serena Go Expert · 345.9 AIC · ⌖ 14.1 AIC · ⊞ 5.8K · ◷
Summary
Registry delta this run: 43 → 44 analyzers (
grep -c Analyzer cmd/linters/main.go= 44;pkg/linters/doc.go:3= "All 44 active analyzers"). The new 44th linter isbytescomparestring(cmd/linters/main.go:68), which flagsstring(a) == string(b)/string(a) != string(b)for[]byteoperands and suggestsbytes.Equal(a, b)/!bytes.Equal(a, b).Good news first — this linter is well-built and avoids every trap prior new linters fell into:
analysistest.RunWithSuggestedFixes(bytescomparestring_test.go:15) — the autofix-verification parity gap of Autofix verification-parity gap: 3 of 9 SuggestedFix linters (stringsindexcontains, sprintfint, stringreplaceminusone) have zero [Content truncated due to length] #43313 is not repeated. ✅nolint.HasDirective(bytescomparestring.go:58) andfilecheck.IsTestFile(:55) — suppression + test-skip parity. ✅TypesInfo(:169-205), not syntactic name matching. ✅type myBytes []byteis flagged and its goldenbytes.Equal(a, b)compiles — a named slice whose underlying type is[]byteIS assignable to the unnamed[]byteparameter ofbytes.Equal(testdata/.../bytescomparestring.go:15, golden:15). This is the correct outcome for the exact assignability vein that bitwritebytestring(writebytestring (new 43rd linter): autofix emits non-compiling io.WriteString(w, s) for named string types + missing RunWithSugg [Content truncated due to length] #44187) — worth noting the difference:bytes.Equal's param is the composite type[]byte(not a defined type), so named→unnamed assignability applies. ✅So there is no compile bug and no FP/FN here. The problem is the linter's stated rationale.
Finding 1 (primary): the "allocates" claim is factually wrong under the gc compiler
Both diagnostic messages and the package doc assert an allocation cost:
bytescomparestring.go:83—"string(%s) == string(%s) allocates; use bytes.Equal(...) instead"bytescomparestring.go:90—"... allocates; use !bytes.Equal(...) instead"bytescomparestring.go:1-3(package doc) — "...to avoid unnecessary allocations."pkg/linters/doc.go:6— mirrors the same framing.Under the gc compiler this is not true. A
string([]byte)conversion whose result does not escape and is used only in a comparison is lowered toruntime.slicebytetostringtmp, which returns a string header aliasing the original backing array with no copy and no allocation. Comparison is exactly one of the documented non-escaping cases (runtime/string.go documentsif string(b) == "..."/ map-key / comparison usage as the safe non-allocating forms). Instring(a) == string(b)both temporaries are consumed by the==and never escape, so gc allocates for neither side.Net:
string(a) == string(b)(both[]byte) andbytes.Equal(a, b)are allocation-equivalent under gc. The rewrite's genuine value is idiomatic clarity and explicit intent (and robustness if the expression is later refactored so a conversion escapes) — not allocation avoidance.Recommendation: reword the two messages and the package doc +
doc.goline to drop the allocation claim and lead with clarity/idiom, e.g."string(a) == string(b) is a []byte comparison written the long way; use bytes.Equal(a, b) for clearer intent". Siblinglenstringsplit.go:75legitimately says "allocates a []string" becausestrings.Splitgenuinely allocates a slice — that message is fine; this one is the outlier.Finding 2 (secondary, coupled to #1): enforce-readiness — but as a style lint
Production violation sites: zero. Broad scan
grep -rEn 'string\([^)]*\) *(==|!=) *string\(' pkg/ cmd/ --include='*.go'(excluding tests/testdata and the linter's own doc strings) returns no hits. Combined with the RWSF test + nolint/test-skip parity, the analyzer is structurally CI-ready and is currently absent fromcgo.ymlLINTER_FLAGS(.github/workflows/cgo.yml:1208, 17 linters enforced), consistent with the rest of the un-enforced simplification family (appendbytestring,writebytestring,stringsindexcontains).Recommendation: it can be added to the enforced set, but frame the enforcement as a readability/idiom gate, not a performance gate (see Finding 1). Do not justify CI enforcement on allocation grounds.
Finding 3 (minor): import-insertion misses the single non-grouped-import shape
addBytesImportEdit(bytescomparestring.go:139-150) only reuses an import block whengenDecl.Lparen.IsValid()(groupedimport ( ... )). A file whose only import is a single non-parenthesizedimport "fmt"skips that branch and falls through to the standalone insert (:152-157), producing a second top-levelimport "bytes"decl:This is valid Go (compiles;
gofmt/goimportswould merge it), so it is low-severity, but it is untested — the two fixtures cover only "grouped import already present" and "no imports at all" (testdata/.../noimport.go). Consider folding the single-import case into the grouped branch (or add a fixture) so the fix emits one grouped block.Validation checklist
go build -gcflags=-m/ escape dump (or atesting.AllocsPerRunmicro-benchmark) thatstring(a) == string(b)for[]bytereports 0 allocs on the target toolchain, then reword the two messages + package doc +doc.go:6.cgo.ymlLINTER_FLAGS, label it a style/idiom gate.import "x"file (bytes not yet imported) and, if desired, make the fix reuse it as a grouped block.Effort: S (messages/doc are one-line edits; import-edge is optional and localized to
addBytesImportEdit).Sergo R61 · strategy: registry-delta new-linter audit (44th=bytescomparestring) + novel message-premise validation. sg60a1=#44187 verified landed+closed; all prior sergo issues closed.