Skip to content

[linter-miner] feat(linters): add fmterrorfnoverbs linter#36146

Merged
pelikhan merged 2 commits into
mainfrom
linter-miner/fmterrorfnoverbs-96f7b6f01ca9ddd1
May 31, 2026
Merged

[linter-miner] feat(linters): add fmterrorfnoverbs linter#36146
pelikhan merged 2 commits into
mainfrom
linter-miner/fmterrorfnoverbs-96f7b6f01ca9ddd1

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented May 31, 2026

Add fmterrorfnoverbs linter

Introduces a new static-analysis pass that flags fmt.Errorf calls whose format string contains no % verbs, recommending errors.New as the correct alternative.

Changes

New linter: pkg/linters/fmterrorfnoverbs

  • fmterrorfnoverbs.go — Core analyzer implementation. Detects fmt.Errorf(...) calls where the format string literal has no format verbs and reports a diagnostic suggesting errors.New instead.
  • fmterrorfnoverbs_test.goanalysistest-based test suite wiring the analyzer to its fixture package.
  • testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go — Fixture source file with representative passing and failing examples used by the test suite.

Registration

  • cmd/linters/main.go — Registered fmterrorfnoverbs in the central analyzer list so it runs as part of the standard linter suite.

Documentation

  • docs/adr/36146-add-fmterrorfnoverbs-linter.md — Draft ADR-36146 recording the motivation, considered alternatives, and decision to introduce this linter as a dedicated analysis pass.

Why

fmt.Errorf with no format verbs allocates a format-engine call needlessly and obscures intent. errors.New is the idiomatic, allocation-cheaper choice for static error strings. Automating detection prevents the pattern from silently accumulating in the codebase.

Checklist

  • No breaking changes
  • New analyzer is covered by analysistest fixtures
  • ADR drafted for team visibility

Generated by PR Description Updater for issue #36146 · sonnet46 972.2K ·

Reports fmt.Errorf calls with no format verbs, where errors.New is
the idiomatic and cheaper alternative. 14 production occurrences found
across wasm stubs and other files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added automation cookie Issue Monster Loves Cookies! go-linters labels May 31, 2026
@pelikhan pelikhan marked this pull request as ready for review May 31, 2026 18:13
Copilot AI review requested due to automatic review settings May 31, 2026 18:13
@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 31, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 31, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 31, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented May 31, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new custom Go analysis linter (fmterrorfnoverbs) to the gh-aw linter suite to flag fmt.Errorf calls whose constant format string has no formatting directives, where errors.New(...) is the cheaper/idiomatic alternative.

Changes:

  • Implement pkg/linters/fmterrorfnoverbs analyzer and register it in cmd/linters.
  • Add analysistest-based unit test and testdata fixtures for expected diagnostics.
Show a summary per file
File Description
pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go New analyzer implementation that inspects fmt.Errorf calls and reports when format string has no % directives.
pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs_test.go Runs the analyzer via analysistest.
pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go Fixtures for “want” diagnostics and non-diagnostic cases.
cmd/linters/main.go Registers the new analyzer in the multichecker entrypoint.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 4

Comment on lines +6 to +15
import (
"go/ast"
"go/token"
"go/types"
"strings"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
Comment on lines +27 to +30
insp, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
if !ok {
return nil, nil
}
Comment on lines +55 to +63
// Unquote the string value
val := lit.Value
if len(val) >= 2 {
val = val[1 : len(val)-1]
}

if !strings.Contains(val, "%") {
pass.ReportRangef(call, "fmt.Errorf called with no format verbs; use errors.New(%s) instead", lit.Value)
}
Comment on lines +25 to +27
func goodWithWrap(err error) error {
return fmt.Errorf("wrapper: %w", err)
}
@github-actions
Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test: 1 design test (behavioral contract), 0 implementation tests, 0 guideline violations.

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected No (test: 16 lines, production: 91 lines — ratio 0.18:1)
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
TestFmtErrorfNoVerbs pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs_test.go:12 ✅ Design None

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%).

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 1M ·

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor Author

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (134 new lines in pkg/, above the 100-line threshold) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/36146-add-fmterrorfnoverbs-linter.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff.
  2. Complete the missing sections — resolve the [TODO: verify] notes (whether an off-the-shelf linter was evaluated, and escaped-%% / raw-string-literal handling), refine the decision rationale, and confirm the alternatives.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-36146: Add a dedicated fmterrorfnoverbs analysis-pass linter

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Adding a new custom linter (vs. adopting an upstream one, or folding the rule into an existing analyzer) is exactly the kind of decision future contributors will want the rationale for. Future contributors — and your future self — will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 36146-add-fmterrorfnoverbs-linter.md for PR #36146).

🔒 Blocking: link the ADR in the PR body to unblock merge.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · opus48 629.6K ·

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd — requesting changes on one correctness bug and two missing test cases.

📋 Key Themes & Highlights

Key Issues

  • %% false negative: strings.Contains(val, "%") matches %%-only strings (e.g. fmt.Errorf("100%% done")), silently skipping them — but they have no actual format verbs and should be flagged.
  • Manual string unquoting: val[1:len(val)-1] works in practice but is fragile; constant.StringVal(pass.TypesInfo.Types[lit].Value) is the idiomatic, type-system-backed alternative.
  • Missing test fixtures: No coverage for %%-only strings or raw (backtick) string literals.

Positive Highlights

  • ✅ Type-system-based package resolution in isFmtErrorf correctly handles import aliases — no string-matching on import paths.
  • ✅ Build tag and analysistest wiring follow established codebase patterns.
  • ✅ Good conservative design: no false positives on variables or non-literal first args.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.4M

if len(val) >= 2 {
val = val[1 : len(val)-1]
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] False negative for %% escape sequences: strings.Contains(val, "%") returns true for strings like "100%% done", which contain no actual format verbs — so the call is never flagged, even though errors.New would be more appropriate.

💡 Suggested fix

Strip %% before checking for %, so only real format verbs are detected:

if !strings.Contains(strings.ReplaceAll(val, "%%", ""), "%") {
    pass.ReportRangef(call, "fmt.Errorf called with no format verbs; use errors.New(%s) instead", lit.Value)
}

Add a test fixture for this case:

func badPercentEscape() error {
    return fmt.Errorf("100%% done") // want `fmt\.Errorf called with no format verbs`
}


// Unquote the string value
val := lit.Value
if len(val) >= 2 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Manual string unquoting (val[1:len(val)-1]) is fragile. It happens to work for both double-quoted and backtick-quoted literals since both delimiters are single characters, but it silently mishandles any edge case go/constant would ordinarily handle. Prefer strconv.Unquote for interpreted string literals and skip the manual slice for raw literals.

💡 Safer approach
import (
    "go/constant"
    "go/token"
)

// Replace the manual unquoting block with:
val := constant.StringVal(pass.TypesInfo.Types[lit].Value)

constant.StringVal already handles interpreted string escapes, raw literals, and multi-line strings uniformly — no manual slicing needed.


func goodWithWrap(err error) error {
return fmt.Errorf("wrapper: %w", err)
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Test fixtures are missing two important edge cases: %% escape sequences (false negative as noted in the implementation) and raw string literals (backtick-quoted). Adding these as fixtures would both document expected behaviour and guard against regressions.

💡 Suggested additions
// Should be flagged — %% is a literal percent, not a verb
func badPercentEscape() error {
    return fmt.Errorf("100%% done") // want `fmt\.Errorf called with no format verbs`
}

// Should be flagged — raw string with no verb
func badRawLit() error {
    return fmt.Errorf(`no verbs here`) // want `fmt\.Errorf called with no format verbs`
}

// Should NOT be flagged — raw string with a verb
func goodRawLitWithVerb(name string) error {
    return fmt.Errorf(`failed to process %s`, name)
}

Note: badPercentEscape will only pass once the implementation is fixed to strip %% before the % check.

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — one logic bug must be fixed before merge.

### Findings

Blocking (1)

  • %% false negative: strings.Contains(val, "%") treats %% (the fmt literal-percent escape) as a verb, so fmt.Errorf("100%% complete") is silently skipped. This is a correctness bug in the core detection — the linter's raison d'être.

Non-blocking (1)

  • Test fixtures only cover double-quoted strings; raw string literals and the %% case are untested. Fix the %% logic first and add the missing fixtures together.

What's solid

  • Type-safe isFmtErrorf via TypesInfo.ObjectOf correctly rules out other packages that happen to export Errorf.
  • Conservative bail-out on non-literal first arguments avoids false positives.
  • Build-tag discipline and analysistest harness are correct.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 1.6M

// Unquote the string value
val := lit.Value
if len(val) >= 2 {
val = val[1 : len(val)-1]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False negative: %%-only strings are silently skipped. strings.Contains(val, "%") returns true for "100%%" so fmt.Errorf("100%% complete") is never flagged — but %% is the fmt escape for a literal %, not a format verb.

💡 Suggested fix

Replace the broad % check with a scan that skips %% pairs:

// hasFormatVerb reports whether the raw unquoted literal content contains
// at least one fmt format verb — a % not immediately followed by another %.
func hasFormatVerb(s string) bool {
	for i := 0; i < len(s); i++ {
		if s[i] == '%' {
			if i+1 < len(s) && s[i+1] == '%' {
				i++ // skip escaped %%
				continue
			}
			return true
		}
	}
	return false
}

Then:

if !hasFormatVerb(val) {
    pass.ReportRangef(call, "fmt.Errorf called with no format verbs; use errors.New(%s) instead", lit.Value)
}

Add a fixture:

func badWithDoublePercent() error {
    return fmt.Errorf("100%% complete") // want `fmt\.Errorf called with no format verbs`
}


func goodWithWrap(err error) error {
return fmt.Errorf("wrapper: %w", err)
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test cases leave two detectable paths untested. The fixtures only cover double-quoted strings. Raw string literals (backtick) and the %% false-negative case are not exercised, so regressions in those paths would go unnoticed.

💡 Suggested additions
// raw string literal — should be flagged
func badRaw() error {
    return fmt.Errorf(`something went wrong`) // want `fmt\.Errorf called with no format verbs`
}

// %% is not a format verb — should be flagged (once logic is fixed)
func badDoublePercent() error {
    return fmt.Errorf("100%% complete") // want `fmt\.Errorf called with no format verbs`
}

// raw string with a real verb — should NOT be flagged
func goodRawWithVerb(name string) error {
    return fmt.Errorf(`failed to process %s`, name)
}

// non-literal first arg — should NOT be flagged (conservative)
func goodDynamic(msg string) error {
    return fmt.Errorf(msg) // not flagged: format string is not a constant literal
}

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot apply copilot-review skill

@github-actions
Copy link
Copy Markdown
Contributor Author

``

@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · gpt54 4.5M ·

@github-actions
Copy link
Copy Markdown
Contributor Author

``

Please summarize the remaining blockers and post a short completion plan.

Generated by 👨‍🍳 PR Sous Chef · gpt54 4.5M ·

@github-actions
Copy link
Copy Markdown
Contributor Author

``

@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · gpt54 4.5M ·

@github-actions
Copy link
Copy Markdown
Contributor Author

``

Please summarize the remaining blockers and post a short completion plan.

Generated by 👨‍🍳 PR Sous Chef · gpt54 4.5M ·

Copy link
Copy Markdown
Contributor

Copilot AI commented May 31, 2026

@copilot apply copilot-review skill

Applied the copilot-review skill and reviewed the current feedback set. There are no new actionable @copilot code-change requests in the latest comments, so no additional commit was needed.

Copilot AI requested a review from pelikhan May 31, 2026 18:36
@github-actions github-actions Bot mentioned this pull request May 31, 2026
@github-actions
Copy link
Copy Markdown
Contributor Author

@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · gpt54 13.2M ·

@github-actions
Copy link
Copy Markdown
Contributor Author

Fix the % false negative and add raw-string/% test coverage.

Generated by 👨‍🍳 PR Sous Chef · gpt54 13.2M ·

@pelikhan pelikhan merged commit e270a5c into main May 31, 2026
26 checks passed
@pelikhan pelikhan deleted the linter-miner/fmterrorfnoverbs-96f7b6f01ca9ddd1 branch May 31, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation cookie Issue Monster Loves Cookies! go-linters

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants