You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
trimleftright: the "repeated rune" cutset heuristic is an arbitrary discriminator that misses the most common TrimLeft/TrimRight
[Content truncated due to length] #46526
trimleftright (new 53rd registered analyzer, pkg/linters/trimleftright/trimleftright.go) exists to catch the well-known Go gotcha where strings.TrimLeft(s, "foo") is used when strings.TrimPrefix(s, "foo") was intended (TrimLeft treats the second arg as a character set, not a prefix). But its trigger heuristic looksSuspiciousCutset only fires when the cutset contains a repeated rune, which systematically misses the most common form of the bug: short, distinct-character prefixes/suffixes such as "http", "https", "bar", "abc", "0x", "v1", "github", "prefix".
Evidence
pkg/linters/trimleftright/trimleftright.go:125-146 — looksSuspiciousCutset returns true only if some rune repeats:
funclooksSuspiciousCutset(cutsetstring) bool {
runes:= []rune(cutset)
iflen(runes) <=1 { returnfalse }
for_, r:=rangerunes { // must be ALL alphanumericif!unicode.IsLetter(r) &&!unicode.IsDigit(r) { returnfalse }
}
seen:=make(map[rune]struct{})
for_, r:=rangerunes {
if_, ok:=seen[r]; ok { returntrue } // <-- only fires on a REPEATED runeseen[r] =struct{}{}
}
returnfalse
}
The linter's own testdata (pkg/linters/trimleftright/testdata/src/trimleftright/trimleftright.go:46-49) encodes the gap as intended behavior:
// good: alphanumeric literal with no repeated runesfuncgoodNoRepeatedAlnum(sstring) string {
returnstrings.TrimLeft(s, "abc") // NOT flagged
}
Yet strings.TrimLeft(s, "abc") is exactly the confusion the analyzer is meant to detect: it strips any leading run of a/b/c characters, almost never what someone writing "abc" intends.
Why the heuristic is the wrong axis
The package doc-comment justifies the narrowing as avoiding false positives on "intentional character-set trimming like whitespace classes" (trimleftright.go:81-83). But whitespace and punctuation are already excluded by the all-alphanumeric guard (trimleftright.go:131-135). So the repeated-rune requirement contributes nothing toward the stated FP-avoidance goal; it only removes true positives.
Crucially, "has a repeated rune" does not correlate with "is a word/token rather than a character class":
cutset
repeated rune?
flagged?
likely intent
"foo"
yes (o)
✅
prefix bug (correctly caught)
"http"
no
❌
prefix bug (missed)
"bar" / "abc"
no
❌
prefix bug (missed)
"0x" / "v1"
no
❌
prefix bug (missed)
"aeiou"
no
❌
intentional class (correctly ignored)
"0123456789"
no
❌
intentional class (correctly ignored)
The filter drops both distinct-char classes (good) and distinct-char prefixes (bad) together, because it cannot tell them apart on the repeated-rune axis. The short, distinct-character prefixes it misses are precisely the highest-frequency real-world instances of this bug.
Impact
False negatives on the linter's core case. Any codebase that mistakenly writes TrimLeft(url, "http") / TrimRight(name, ".go")-style distinct-char cutsets is silently unprotected.
Latent only: trimleftright is not in the CI enforce list (.github/workflows/cgo.yml:1208), so this does not regress CI today — but it undercuts the analyzer's value if/when it is enforced.
This matches the recurring pattern-too-narrow vein previously filed for other analyzers (e.g. #40244 errstringmatch, #40434 sprintferrdot, #40581 lenstringzero), where a linter matches only a subset of an equivalent family.
Recommendation
Replace the repeated-rune proxy with a discriminator that actually separates "intentional character class" from "probable prefix/suffix":
Invert the logic — flag all multi-rune alphanumeric cutsets except recognized character-class idioms (contiguous rune ranges, known classes such as hex 0123456789abcdefABCDEF, digit runs, vowel sets). This catches "http"/"bar"/"abc" while still ignoring "aeiou"/"0123456789"/"abcdef".
Or, if a conservative scope is deliberate, document the limitation explicitly in the analyzer doc and rename the internal helper away from the misleading "suspicious" framing, and update testdata to record "abc" as a known uncovered case rather than a good case.
Validation checklist
Add testdata cases for distinct-char prefixes: TrimLeft(s, "http"), TrimLeft(s, "abc"), TrimRight(s, "txt") → expect a diagnostic.
Keep negative cases for genuine classes: TrimLeft(s, "aeiou"), TrimLeft(s, "0123456789"), TrimLeft(s, "abcdef") → expect no diagnostic.
Preserve single-rune (" ", "→") and empty-cutset no-ops.
Summary
trimleftright(new 53rd registered analyzer,pkg/linters/trimleftright/trimleftright.go) exists to catch the well-known Go gotcha wherestrings.TrimLeft(s, "foo")is used whenstrings.TrimPrefix(s, "foo")was intended (TrimLeft treats the second arg as a character set, not a prefix). But its trigger heuristiclooksSuspiciousCutsetonly fires when the cutset contains a repeated rune, which systematically misses the most common form of the bug: short, distinct-character prefixes/suffixes such as"http","https","bar","abc","0x","v1","github","prefix".Evidence
pkg/linters/trimleftright/trimleftright.go:125-146—looksSuspiciousCutsetreturnstrueonly if some rune repeats:The linter's own testdata (
pkg/linters/trimleftright/testdata/src/trimleftright/trimleftright.go:46-49) encodes the gap as intended behavior:Yet
strings.TrimLeft(s, "abc")is exactly the confusion the analyzer is meant to detect: it strips any leading run ofa/b/ccharacters, almost never what someone writing"abc"intends.Why the heuristic is the wrong axis
The package doc-comment justifies the narrowing as avoiding false positives on "intentional character-set trimming like whitespace classes" (
trimleftright.go:81-83). But whitespace and punctuation are already excluded by the all-alphanumeric guard (trimleftright.go:131-135). So the repeated-rune requirement contributes nothing toward the stated FP-avoidance goal; it only removes true positives.Crucially, "has a repeated rune" does not correlate with "is a word/token rather than a character class":
"foo""http""bar"/"abc""0x"/"v1""aeiou""0123456789"The filter drops both distinct-char classes (good) and distinct-char prefixes (bad) together, because it cannot tell them apart on the repeated-rune axis. The short, distinct-character prefixes it misses are precisely the highest-frequency real-world instances of this bug.
Impact
TrimLeft(url, "http")/TrimRight(name, ".go")-style distinct-char cutsets is silently unprotected.trimleftrightis not in the CI enforce list (.github/workflows/cgo.yml:1208), so this does not regress CI today — but it undercuts the analyzer's value if/when it is enforced.This matches the recurring pattern-too-narrow vein previously filed for other analyzers (e.g. #40244 errstringmatch, #40434 sprintferrdot, #40581 lenstringzero), where a linter matches only a subset of an equivalent family.
Recommendation
Replace the repeated-rune proxy with a discriminator that actually separates "intentional character class" from "probable prefix/suffix":
0123456789abcdefABCDEF, digit runs, vowel sets). This catches"http"/"bar"/"abc"while still ignoring"aeiou"/"0123456789"/"abcdef"."abc"as a known uncovered case rather than agoodcase.Validation checklist
TrimLeft(s, "http"),TrimLeft(s, "abc"),TrimRight(s, "txt")→ expect a diagnostic.TrimLeft(s, "aeiou"),TrimLeft(s, "0123456789"),TrimLeft(s, "abcdef")→ expect no diagnostic." ","→") and empty-cutset no-ops.go test ./pkg/linters/trimleftright/....Effort: small–medium (single-file heuristic change + testdata).
References: §29673838676