errstringmatch: extend brittle error-string detection to HasPrefix/HasSuffix/EqualFold/Index/LastIndex/Compare#40248
Conversation
…ex/Compare Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
✅ Test Quality Sentinel completed test quality analysis. No test files (as defined: Go *_test.go, JS *.test.cjs/test.js) were added or modified in PR #40248. The PR extends pkg/linters/errstringmatch to detect HasPrefix/HasSuffix/EqualFold/Index/LastIndex/Compare on err.Error(), and adds corresponding analysistest testdata cases (with // want directives) for all new functions. The existing TestErrStringMatch test function (errstringmatch_test.go) exercises these new testdata cases via analysistest.Run — but the test file itself was not modified. Test Quality Sentinel skipped per scope rules. |
|
✅ PR Code Quality Reviewer completed the code quality review. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #40248 does not have the 'implementation' label (has_implementation_label=false) and has only 73 new lines in business logic directories (≤100 threshold). Neither Condition A nor Condition B is met. |
There was a problem hiding this comment.
Pull request overview
This PR broadens the errstringmatch Go analysis linter so it no longer only flags strings.Contains(err.Error(), ...), but also detects the same brittle error-string matching pattern when implemented via other strings helpers (e.g., HasPrefix, Index, Compare), and updates testdata accordingly.
Changes:
- Expand detection from only
strings.Containsto a broader set ofstrings.<Func>(err.Error(), <string>)calls via a centralized function-name matcher. - Improve the diagnostic to name the specific matched
stringsfunction. - Add new
analysistesttestdata cases to exercise the newly detected functions.
Show a summary per file
| File | Description |
|---|---|
| pkg/linters/errstringmatch/errstringmatch.go | Expands function matching to additional brittle strings APIs and updates the reported diagnostic. |
| pkg/linters/errstringmatch/testdata/src/errstringmatch/errstringmatch.go | Adds positive/negative testdata cases for newly covered strings functions. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 2
| // Package errstringmatch implements a Go analysis linter that flags calls to | ||
| // strings.Contains/HasPrefix/HasSuffix/EqualFold/Index/LastIndex/Compare on | ||
| // err.Error() with a string literal — all perform brittle substring matching on | ||
| // error messages instead of using errors.Is or errors.As. |
| var Analyzer = &analysis.Analyzer{ | ||
| Name: "errstringmatch", | ||
| Doc: "reports strings.Contains(err.Error(), \"...\") calls that perform brittle substring matching on error messages", | ||
| Doc: "reports strings.Contains/HasPrefix/HasSuffix/EqualFold/Index/LastIndex/Compare(err.Error(), \"...\") calls that perform brittle substring matching on error messages", |
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /tdd and /zoom-out — two minor observations, no blocking issues.
📋 Key Themes & Highlights
Key Themes
- Test coverage gap: 4 of the 6 newly covered functions (
HasSuffix,Index,LastIndex,Compare) are missing negative test cases confirming that plain-string callers are not flagged. See inline comment on testdata. - Known second-arg gap:
EqualFoldandCompareare symmetric functions;strings.EqualFold(s, err.Error())is equally brittle but would not be flagged (consistent with the pre-existing behaviour forContains). Worth a brief acknowledgement comment.
Positive Highlights
- ✅ Clean extension: replacing the single
isStringsContainsboolean predicate with a map-basedbrittleErrStringFuncNamereturning(string, bool)is the right abstraction — easy to extend, easy to read. - ✅ Diagnostic message now names the matched function (
avoid strings.HasPrefix(err.Error(), ...)), making the lint output actionable. - ✅ All six new positive test cases are present with correctly escaped
wantdirectives that verify the function name in the message. - ✅
map[string]boolset pattern is consistent withrawloginliband other linters in this package. - ✅ Package comment and
Analyzer.Docare updated in sync with the implementation.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 70.3 AIC · ⌖ 7.71 AIC · ⊞ 6.9K
| // not flagged: strings.EqualFold on a plain string, not err.Error() | ||
| func checkEqualFoldString(s string) bool { | ||
| return strings.EqualFold(s, "value") | ||
| } |
There was a problem hiding this comment.
[/tdd] Only HasPrefix and EqualFold have negative ("not flagged") test cases; HasSuffix, Index, LastIndex, and Compare are missing equivalent coverage. If isErrDotError were inadvertently broken for those functions, no test would catch it.
💡 Suggested additions
// not flagged: strings.HasSuffix on a plain string, not err.Error()
func checkHasSuffixString(s string) bool {
return strings.HasSuffix(s, "suffix")
}
// not flagged: strings.Index on a plain string, not err.Error()
func checkIndexString(s string) bool {
return strings.Index(s, "sub") >= 0
}
// not flagged: strings.LastIndex on a plain string, not err.Error()
func checkLastIndexString(s string) bool {
return strings.LastIndex(s, "sub") >= 0
}
// not flagged: strings.Compare on a plain string, not err.Error()
func checkCompareString(s string) bool {
return strings.Compare(s, "value") == 0
}One negative test per function makes the testdata file a self-documenting specification of what is and is not flagged.
| "Contains": true, | ||
| "HasPrefix": true, | ||
| "HasSuffix": true, | ||
| "EqualFold": true, |
There was a problem hiding this comment.
[/zoom-out] EqualFold and Compare are semantically symmetric — strings.EqualFold(someString, err.Error()) and strings.Compare(someString, err.Error()) == 0 are equally brittle patterns, but they would not be flagged because only outer.Args[0] is checked for err.Error(). This is consistent with the existing behaviour for Contains, so it is not a regression introduced here.
Worth adding a brief comment (or a TODO) to the package doc or isErrDotError acknowledging this known gap, so future contributors do not confuse the absence of second-arg detection with intentional design.
There was a problem hiding this comment.
🔎 Code quality review by PR Code Quality Reviewer · 71.5 AIC · ⌖ 7.71 AIC · ⊞ 5.1K
| "EqualFold": true, | ||
| "Index": true, | ||
| "LastIndex": true, | ||
| "Compare": true, |
There was a problem hiding this comment.
EqualFold and Compare with swapped arguments silently bypass the linter, creating a false-confidence gap for the two symmetric functions this PR introduces.
💡 Details
strings.EqualFold and strings.Compare are symmetric (argument order does not change the equality result). The linter only checks outer.Args[0] against err.Error(), so:
strings.EqualFold("timeout", err.Error()) // not flagged — false negative
strings.Compare("EOF", err.Error()) == 0 // not flagged — false negativeFor the non-symmetric functions (HasPrefix, HasSuffix, Index, LastIndex) the reversed order has a different semantic, so skipping it is defensible. But EqualFold and Compare are genuinely equivalent in either order when used for equality testing.
Consider extending the check for these two specifically:
// for EqualFold and Compare, either arg can be err.Error()
isSymmetric := funcName == "EqualFold" || funcName == "Compare"
arg0Err := isErrDotError(pass, outer.Args[0])
arg1Err := isErrDotError(pass, outer.Args[1])
if !arg0Err && !(isSymmetric && arg1Err) {
return
}| // not flagged: strings.EqualFold on a plain string, not err.Error() | ||
| func checkEqualFoldString(s string) bool { | ||
| return strings.EqualFold(s, "value") | ||
| } |
There was a problem hiding this comment.
No nolint:errstringmatch suppression test covers any of the six newly added functions, leaving the nolint path untested for the expanded diagnostic message format.
💡 Details
The existing nolint tests at lines 30–37 verify suppression only for strings.Contains. None of the new functions (HasPrefix, HasSuffix, EqualFold, Index, LastIndex, Compare) have a corresponding suppression test. The diagnostic message changed from a hardcoded string to a fmt.Sprintf-formatted string ("avoid strings.%s(err.Error(), ..."). If the format regresses, the nolint directive lookup could silently break for any of these six new functions without a test catching it.
Add at least one suppression test for a representative new function:
func checkHasPrefixNolint(err error) bool {
(nolint/redacted):errstringmatch
return strings.HasPrefix(err.Error(), "connection refused")
}| return strings.Compare(err.Error(), "timeout") == 0 // want `avoid strings\.Compare\(err\.Error\(\)` | ||
| } | ||
|
|
||
| // not flagged: strings.HasPrefix on a plain string, not err.Error() |
There was a problem hiding this comment.
Negative test coverage is incomplete: Index, LastIndex, and Compare have no "not flagged" test cases for non-error string arguments.
💡 Details
Lines 69–77 add negative cases only for HasPrefix and EqualFold with non-error string arguments. Index, LastIndex, and Compare have no corresponding negative tests. A regression where the linter incorrectly flags strings.Index(someString, "x") would go undetected. Add:
func checkIndexString(s string) bool {
return strings.Index(s, "denied") >= 0
}
func checkLastIndexString(s string) bool {
return strings.LastIndex(s, "denied") >= 0
}
func checkCompareString(s string) bool {
return strings.Compare(s, "timeout") == 0
}
The
errstringmatchlinter only flaggedstrings.Contains(err.Error(), ...), leaving equally brittle patterns usingHasPrefix,HasSuffix,EqualFold,Index,LastIndex, andComparesilently unchecked.Changes
errstringmatch.go: Replaces the narrowisStringsContainspredicate withbrittleErrStringFuncName, which matches the full set of brittlestringsfunctions via a map lookup and returns the matched name. The diagnostic message now names the matched function:brittleErrStringFuncsmap:Contains,HasPrefix,HasSuffix,EqualFold,Index,LastIndex,Compare— all share the same two-argument shape(err.Error(), literal), so the existing arg-guard logic transfers unchanged.err.Error()calls to the same functions are not flagged.Analyzer.Docto reflect the expanded function set.