From 0ba0075623a20dd7411008e1f87268c8899c16c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:47:48 +0000 Subject: [PATCH 1/2] Initial plan From fe6b1fd835ef1c46622eaecea1ef03d92d923af2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:57:12 +0000 Subject: [PATCH 2/2] errstringmatch: extend to HasPrefix/HasSuffix/EqualFold/Index/LastIndex/Compare Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/linters/errstringmatch/errstringmatch.go | 45 ++++++++++++++----- .../src/errstringmatch/errstringmatch.go | 40 +++++++++++++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/pkg/linters/errstringmatch/errstringmatch.go b/pkg/linters/errstringmatch/errstringmatch.go index db0175faacc..bdae67e0a8a 100644 --- a/pkg/linters/errstringmatch/errstringmatch.go +++ b/pkg/linters/errstringmatch/errstringmatch.go @@ -1,6 +1,7 @@ -// Package errstringmatch implements a Go analysis linter that flags -// calls to strings.Contains(err.Error(), "literal") that perform brittle -// substring matching on error messages instead of using errors.Is or errors.As. +// 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. package errstringmatch import ( @@ -18,12 +19,24 @@ import ( // Analyzer is the err-string-match analysis pass. 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", URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/errstringmatch", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: run, } +// brittleErrStringFuncs is the set of strings package functions that perform +// brittle error-message matching when their first argument is err.Error(). +var brittleErrStringFuncs = map[string]bool{ + "Contains": true, + "HasPrefix": true, + "HasSuffix": true, + "EqualFold": true, + "Index": true, + "LastIndex": true, + "Compare": true, +} + func run(pass *analysis.Pass) (any, error) { insp, err := astutil.Inspector(pass) if err != nil { @@ -45,8 +58,9 @@ func run(pass *analysis.Pass) (any, error) { return } - // Match strings.Contains(X, Y) - if !isStringsContains(outer) { + // Match strings.(X, Y) + funcName, matched := brittleErrStringFuncName(outer) + if !matched { return } if len(outer.Args) != 2 { @@ -66,23 +80,30 @@ func run(pass *analysis.Pass) (any, error) { return } - pass.ReportRangef(outer, "avoid strings.Contains(err.Error(), ...) — use errors.Is, errors.As, or a sentinel error instead") + pass.ReportRangef(outer, "avoid strings.%s(err.Error(), ...) — use errors.Is, errors.As, or a sentinel error instead", funcName) }) return nil, nil } -// isStringsContains returns true for strings.Contains(...) call expressions. -func isStringsContains(call *ast.CallExpr) bool { +// brittleErrStringFuncName returns the matched strings function name and true +// when call is a strings.(...) call expression. +func brittleErrStringFuncName(call *ast.CallExpr) (string, bool) { sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { - return false + return "", false } ident, ok := sel.X.(*ast.Ident) if !ok { - return false + return "", false + } + if ident.Name != "strings" { + return "", false + } + if brittleErrStringFuncs[sel.Sel.Name] { + return sel.Sel.Name, true } - return ident.Name == "strings" && sel.Sel.Name == "Contains" + return "", false } // isErrDotError returns true when expr is a method call of the form .Error() diff --git a/pkg/linters/errstringmatch/testdata/src/errstringmatch/errstringmatch.go b/pkg/linters/errstringmatch/testdata/src/errstringmatch/errstringmatch.go index aa2db862eb0..39a3fd588c9 100644 --- a/pkg/linters/errstringmatch/testdata/src/errstringmatch/errstringmatch.go +++ b/pkg/linters/errstringmatch/testdata/src/errstringmatch/errstringmatch.go @@ -35,3 +35,43 @@ func checkIgnoredPreviousLine(err error) bool { func checkIgnoredSameLine(err error) bool { return strings.Contains(err.Error(), "already merged") //nolint:errstringmatch // gh CLI merge status is only available as text. } + +// flagged: strings.HasPrefix on err.Error() with a string literal +func checkHasPrefix(err error) bool { + return strings.HasPrefix(err.Error(), "connection refused") // want `avoid strings\.HasPrefix\(err\.Error\(\)` +} + +// flagged: strings.HasSuffix on err.Error() with a string literal +func checkHasSuffix(err error) bool { + return strings.HasSuffix(err.Error(), "not found") // want `avoid strings\.HasSuffix\(err\.Error\(\)` +} + +// flagged: strings.EqualFold on err.Error() with a string literal +func checkEqualFold(err error) bool { + return strings.EqualFold(err.Error(), "timeout") // want `avoid strings\.EqualFold\(err\.Error\(\)` +} + +// flagged: strings.Index on err.Error() with a string literal +func checkIndex(err error) bool { + return strings.Index(err.Error(), "denied") >= 0 // want `avoid strings\.Index\(err\.Error\(\)` +} + +// flagged: strings.LastIndex on err.Error() with a string literal +func checkLastIndex(err error) bool { + return strings.LastIndex(err.Error(), "denied") >= 0 // want `avoid strings\.LastIndex\(err\.Error\(\)` +} + +// flagged: strings.Compare on err.Error() with a string literal +func checkCompare(err error) bool { + return strings.Compare(err.Error(), "timeout") == 0 // want `avoid strings\.Compare\(err\.Error\(\)` +} + +// not flagged: strings.HasPrefix on a plain string, not err.Error() +func checkHasPrefixString(s string) bool { + return strings.HasPrefix(s, "prefix") +} + +// not flagged: strings.EqualFold on a plain string, not err.Error() +func checkEqualFoldString(s string) bool { + return strings.EqualFold(s, "value") +}