diff --git a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go index 72fca0ce73f..1d362a9723c 100644 --- a/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go +++ b/pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go @@ -6,7 +6,6 @@ package fmterrorfnoverbs import ( "go/ast" "go/token" - "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -60,7 +59,7 @@ func run(pass *analysis.Pass) (any, error) { val = val[1 : len(val)-1] } - if !strings.Contains(val, "%") { + if !hasRealFormatVerb(val) { position := pass.Fset.PositionFor(call.Pos(), false) if nolint.HasDirective(position, noLintLinesByFile) { return @@ -71,3 +70,27 @@ func run(pass *analysis.Pass) (any, error) { return nil, nil } + +// hasRealFormatVerb reports whether val (the raw content between the surrounding +// quotes of a Go string literal) contains at least one format verb that is not +// an escaped percent pair (%%). The sequence %% renders as a literal % at +// runtime and does not consume an argument, so it is not a real verb. +func hasRealFormatVerb(val string) bool { + for i := 0; i < len(val); i++ { + if val[i] != '%' { + continue + } + i++ + if i >= len(val) { + // Trailing lone % is a malformed verb directive; treat it as present + // rather than suggesting errors.New for a broken format string. + return true + } + if val[i] == '%' { + // %% is an escaped percent, not a verb; skip the second % and continue. + continue + } + return true + } + return false +} diff --git a/pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go b/pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go index 610309a7638..e85ef4abc3e 100644 --- a/pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go +++ b/pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go @@ -47,3 +47,19 @@ func localShadowNotFlagged() error { fmt := shadowFormatter{} return fmt.Errorf("not fmt package") } + +// escapePercentOnly has only %% (escaped literal percent) and no real verb; +// it should be flagged like a plain string. +func escapePercentOnly() error { + return fmtalias.Errorf("disk usage exceeds 90%% limit") // want `fmt\.Errorf called with no format verbs; use errors\.New` +} + +// realVerbWithEscapePercent has a real verb (%d) alongside %%; must NOT be flagged. +func realVerbWithEscapePercent(n int) error { + return fmtalias.Errorf("%d%% done", n) +} + +// multipleEscapePercents has multiple %% sequences but no real verb; should be flagged. +func multipleEscapePercents() error { + return fmtalias.Errorf("between 50%% and 90%% utilised") // want `fmt\.Errorf called with no format verbs; use errors\.New` +}