-
Notifications
You must be signed in to change notification settings - Fork 458
trimleftright: widen heuristic to catch distinct-character prefix/suffix bugs #46536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1713911
c416be6
43b221c
219a638
322ddb3
e327444
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ import ( | |
| // Analyzer is the trimleftright analysis pass. | ||
| var Analyzer = &analysis.Analyzer{ | ||
| Name: "trimleftright", | ||
| Doc: "reports likely mistaken strings.TrimLeft/TrimRight calls using repeated alphanumeric literal cutsets", | ||
| Doc: "reports likely mistaken strings.TrimLeft/TrimRight calls using multi-character alphanumeric literal cutsets", | ||
| URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/trimleftright", | ||
| Requires: []*analysis.Analyzer{inspect.Analyzer, nolint.Analyzer, filecheck.Analyzer}, | ||
| Run: run, | ||
|
|
@@ -78,9 +78,8 @@ func run(pass *analysis.Pass) (any, error) { | |
| return | ||
| } | ||
|
|
||
| // Only flag suspicious cutsets where a multi-rune alphanumeric literal | ||
| // contains repeated runes (e.g., "foo"). This avoids flagging common, | ||
| // intentional character-set trimming like whitespace classes. | ||
| // Only flag suspicious cutsets: multi-rune all-alphanumeric literals | ||
| // that do not look like intentional character-set trimming. | ||
| cutset, isCutset := stringLitValue(call.Args[1]) | ||
| if !isCutset || !looksSuspiciousCutset(cutset) { | ||
| return | ||
|
|
@@ -119,9 +118,19 @@ func stringLitValue(expr ast.Expr) (string, bool) { | |
| } | ||
|
|
||
| // looksSuspiciousCutset reports likely TrimPrefix/TrimSuffix confusion. | ||
| // We require a multi-rune alphanumeric cutset with at least one repeated rune | ||
| // so valid character-set trimming (whitespace, punctuation, unique rune sets) | ||
| // is not flagged. | ||
| // It returns true for any multi-rune all-alphanumeric cutset that does not | ||
| // look like an intentional character-class trimmer. | ||
| // | ||
| // Recognised character-class exceptions (returned false): | ||
| // - Pure decimal-digit sets (e.g. "0123456789", "012") — digit trimming. | ||
| // - Pure ASCII-vowel sets (e.g. "aeiou", "aei") — vowel trimming. | ||
| // - Complete hex-letter alphabet in any case (all six of a–f present, | ||
| // mixed with optional digits, e.g. "abcdef", "ABCDEF", | ||
| // "0123456789abcdef") — hex trimming. | ||
| // | ||
| // Single-rune cutsets and cutsets containing non-alphanumeric runes are | ||
| // returned false because they are either trivially correct or already | ||
| // covered by other idioms (whitespace trimming, punctuation sets). | ||
| func looksSuspiciousCutset(cutset string) bool { | ||
| runes := []rune(cutset) | ||
| if len(runes) <= 1 { | ||
|
|
@@ -134,13 +143,77 @@ func looksSuspiciousCutset(cutset string) bool { | |
| } | ||
| } | ||
|
|
||
| seen := make(map[rune]struct{}) | ||
| // Exception: intentional decimal-digit set. | ||
| if isAllASCIIDigits(runes) { | ||
| return false | ||
| } | ||
|
|
||
| // Exception: intentional ASCII-vowel set. | ||
| if isAllASCIIVowels(runes) { | ||
| return false | ||
| } | ||
|
|
||
| // Exception: complete hex-letter alphabet (all six of a–f in any case, | ||
| // optionally accompanied by decimal digits). | ||
| if isCompleteHexAlphabet(runes) { | ||
| return false | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| // isAllASCIIDigits reports whether every rune is an ASCII decimal digit. | ||
| // An empty slice returns true (vacuous truth); callers must apply the length | ||
| // guard in looksSuspiciousCutset before invoking this helper. | ||
| func isAllASCIIDigits(runes []rune) bool { | ||
| for _, r := range runes { | ||
| if _, ok := seen[r]; ok { | ||
| return true | ||
| if r < '0' || r > '9' { | ||
| return false | ||
| } | ||
| seen[r] = struct{}{} | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // isAllASCIIVowels reports whether every rune is an ASCII vowel (a e i o u), | ||
| // case-insensitive. | ||
| // An empty slice returns true (vacuous truth); callers must apply the length | ||
| // guard in looksSuspiciousCutset before invoking this helper. | ||
| func isAllASCIIVowels(runes []rune) bool { | ||
| for _, r := range runes { | ||
| switch unicode.ToLower(r) { | ||
| case 'a', 'e', 'i', 'o', 'u': | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| return false | ||
| // isCompleteHexAlphabet reports whether runes consist solely of ASCII decimal | ||
| // digits and/or ASCII hex letters (a–f, A–F), and include all six hex-letter | ||
| // code points (a b c d e f, case-insensitively). This matches intentional | ||
| // hex-class trimming like "abcdef", "ABCDEF", or "0123456789abcdef". | ||
| // Partial hex-letter subsets such as "abc" are not matched and return false. | ||
| // Duplicate hex letters (e.g. "aabcdef") are also rejected and return false; | ||
| // a repeated hex letter is almost certainly a bug rather than a character class. | ||
| func isCompleteHexAlphabet(runes []rune) bool { | ||
| if len(runes) > 16 { // 6 unique hex letters (a–f, case-folded to lowercase) + 10 digits | ||
| return false | ||
| } | ||
| seen := make(map[rune]struct{}) | ||
| for _, r := range runes { | ||
| lower := unicode.ToLower(r) | ||
| switch { | ||
| case lower >= 'a' && lower <= 'f': | ||
| if _, ok := seen[lower]; ok { | ||
| return false // repeated hex letter → suspicious | ||
| } | ||
| seen[lower] = struct{}{} | ||
| case r >= '0' && r <= '9': | ||
| // decimal digits are permissible alongside hex letters | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
| return len(seen) == 6 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] 💡 Suggested test + doc noteAdd to the safe-hex block in {name: "hex letters with duplicate", cutset: "aabcdef", want: false},Add a comment inside // Duplicate hex letters are allowed; only distinct coverage of all six matters.@copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regression fix in thread r3610090990 now explicitly rejects duplicate hex letters, so |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,12 +10,49 @@ func TestLooksSuspiciousCutset(t *testing.T) { | |
| cutset string | ||
| want bool | ||
| }{ | ||
| // Trivially safe: too short or non-alphanumeric. | ||
| {name: "empty", cutset: "", want: false}, | ||
| {name: "single rune", cutset: "a", want: false}, | ||
| {name: "repeated alnum", cutset: "foo", want: true}, | ||
| {name: "unique alnum", cutset: "aeiou", want: false}, | ||
| {name: "contains whitespace", cutset: " \t", want: false}, | ||
| {name: "contains punctuation", cutset: "a!", want: false}, | ||
|
|
||
| // Suspicious: repeated rune (legacy coverage). | ||
| {name: "repeated alnum", cutset: "foo", want: true}, | ||
| {name: "repeated suffix alnum", cutset: "barr", want: true}, | ||
|
|
||
| // Suspicious: distinct-char prefixes/suffixes — the main gap fixed. | ||
| {name: "http prefix", cutset: "http", want: true}, | ||
| {name: "abc prefix", cutset: "abc", want: true}, | ||
| {name: "txt suffix", cutset: "txt", want: true}, | ||
| {name: "bar prefix", cutset: "bar", want: true}, | ||
| {name: "v1 version prefix", cutset: "v1", want: true}, | ||
| {name: "mixed alpha digit", cutset: "0x", want: true}, | ||
|
|
||
| // Safe: intentional decimal-digit class. | ||
| {name: "all digits", cutset: "0123456789", want: false}, | ||
| {name: "digit subset", cutset: "012", want: false}, | ||
|
|
||
| // Safe: intentional ASCII-vowel class. | ||
| {name: "all vowels", cutset: "aeiou", want: false}, | ||
| {name: "vowel subset", cutset: "aei", want: false}, | ||
| {name: "vowels uppercase", cutset: "AEIOU", want: false}, | ||
|
|
||
| // Safe: complete hex-letter alphabet (all six of a–f present). | ||
| {name: "hex letters lower", cutset: "abcdef", want: false}, | ||
| {name: "hex letters upper", cutset: "ABCDEF", want: false}, | ||
| {name: "full hex with digits lower", cutset: "0123456789abcdef", want: false}, | ||
| {name: "full hex with digits upper", cutset: "0123456789ABCDEF", want: false}, | ||
| {name: "hex mixed case", cutset: "abcDEF", want: false}, | ||
|
|
||
| // Suspicious: partial hex-letter subset (not the complete alphabet). | ||
| // "abc" is tested above via "abc prefix"; here we confirm the | ||
| // 4-letter subset also escapes the hex exception. | ||
| {name: "partial hex abce", cutset: "abce", want: true}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] Missing boundary test: 💡 Suggested test caseAdd to the suspicious section: {name: "almost hex abcdefg", cutset: "abcdefg", want: true},This documents that adding any non-hex-letter to all-six-hex-letters still triggers the linter, locking down the boundary of @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
| // Boundary: all six hex letters plus a non-hex character. | ||
| {name: "almost hex abcdefg", cutset: "abcdefg", want: true}, | ||
| // Regression: repeated hex letters look like a bug, not a character class. | ||
| {name: "repeated hex letters", cutset: "aabbccddeeff", want: true}, | ||
| {name: "hex with duplicate letter", cutset: "aabcdef", want: true}, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regression:
isCompleteHexAlphabetsilently accepts suspicious repeated-rune hex cutsets — e.g."aabbccddeeff"now passes the hex exception when it would have been flagged by the old repeated-rune check.💡 Details and fix
The hex exception is intended to recognise intentional hex-class trimming like
"abcdef". But the current implementation only checks that all 6 hex letters appear inseen; it never rejects duplicate runes. A cutset like"aabbccddeeff"populatesseenwith all 6 letters and returnsfalse(not suspicious), silently un-flagging what is almost certainly a bug.The old code caught this class via the repeated-rune check; widening the heuristic regressed it.
Simple fix — add a uniqueness check:
Add a test case:
{name: "repeated hex letters", cutset: "aabbccddeeff", want: true}.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit
322ddb3:isCompleteHexAlphabetnow has alen(runes) > 16guard and a per-letter uniqueness check, so"aabbccddeeff"(and any other repeated-hex-letter cutset) is correctly returned as suspicious. The test case{name: "repeated hex letters", cutset: "aabbccddeeff", want: true}was also added.