Skip to content

Fix cacheRecoveryError's %v-not-%w bug and extend errorfwrapv/fmterrorfnoverbs to concatenated format strings - #58715

Open
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-cache-recovery-error
Open

Fix cacheRecoveryError's %v-not-%w bug and extend errorfwrapv/fmterrorfnoverbs to concatenated format strings#58715
pelikhan with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-cache-recovery-error

Conversation

Copilot AI commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

cacheRecoveryError formats its err argument with %v instead of %w, breaking errors.Is/errors.As chain inspection for callers of the permission/cache-recovery path. The errorfwrapv and fmterrorfnoverbs linters couldn't catch this because they only extract format strings from a plain *ast.BasicLit, bailing out on the message+"..." concatenation cacheRecoveryError actually uses.

Production fix

  • pkg/cli/audit_run_pipeline.go: cacheRecoveryError now wraps err with %w.

Shared format-string resolution

  • Added astutil.ResolveFormatString(expr ast.Expr) to resolve a format-string argument built from a chain of +-concatenated string literals and non-literal operands (e.g. message + "\n\n" + "..." + "%v").
  • Non-literal operands are replaced with a placeholder byte rather than dropped, so a literal ending in % can't merge across a dropped operand into a fabricated verb (e.g. "abc%" + errStr + "v..." won't be mistaken for "abc%v...").
  • Returns ok=false when no literal segment is found at all, so bare non-literal format strings remain correctly ignored.

Linter updates

  • errorfwrapv and fmterrorfnoverbs both use the new helper instead of duplicating *ast.BasicLit-only extraction, so they now analyze concatenated format strings.
  • fmterrorfnoverbs only proposes a concrete errors.New("...") replacement when the format string is a single literal; for concatenated expressions it falls back to a generic suggestion instead of synthesizing a misleading literal.

Tests

  • New testdata cases per linter for a message + "literal"-style format string (trailing %v error arg / no-verb string), plus a regression case for the verb-fabrication boundary.
  • TestCacheRecoveryError now asserts errors.Is succeeds through the returned error.

…ncatenation

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix %v-not-%w bug in cacheRecoveryError function Fix cacheRecoveryError's %v-not-%w bug and extend errorfwrapv/fmterrorfnoverbs to concatenated format strings Sep 5, 2026
Copilot AI requested a review from pelikhan September 5, 2026 04:48
@pelikhan
pelikhan marked this pull request as ready for review September 5, 2026 04:50
Copilot AI balanced review requested due to automatic review settings September 5, 2026 04:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Opaque format segments can contain directives, causing both linters to emit incorrect diagnostics and potentially behavior-changing recommendations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes #58712 by preserving cache-recovery error chains and extending format-string linting to concatenated expressions.

Changes:

  • Replaces %v with %w in cacheRecoveryError.
  • Adds shared concatenated format-string resolution.
  • Extends both linters and their regression coverage.
File summaries
File Description
pkg/cli/audit_run_pipeline.go Wraps the recovery error.
pkg/cli/audit_run_pipeline_test.go Verifies error-chain preservation.
pkg/linters/internal/astutil/astutil.go Adds format-string resolution.
pkg/linters/internal/astutil/astutil_test.go Tests literal and opaque segments.
pkg/linters/errorfwrapv/errorfwrapv.go Analyzes concatenated formats.
pkg/linters/errorfwrapv/testdata/src/errorfwrapv/errorfwrapv.go Adds concatenation fixtures.
pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go Checks concatenated formats for verbs.
pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go Adds concatenation fixtures.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +69 to +74
formatStr, ok := astutil.ResolveFormatString(call.Args[0])
if !ok {
return
}

verbs := parseFormatVerbs(lit.Value)
verbs := parseFormatVerbs(formatStr)
Comment on lines +44 to +49
formatStr, ok := astutil.ResolveFormatString(call.Args[0])
if !ok {
return
}

// Unquote the string value
val := lit.Value
if len(val) >= 2 {
val = val[1 : len(val)-1]
}

if !hasRealFormatVerb(val) {
if !hasRealFormatVerb(formatStr) {
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Reviewed PR #58715 and submitted review comments; no additional GitHub write was needed beyond the review actions.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • github.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "github.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by Ponytail Reviewer for #58715

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-09-05T05:00:28Z
review_event: REQUEST_CHANGES
top_themes:
  - unsound format-string analysis across opaque concatenation boundaries
  - false positives in errorfwrapv and fmterrorfnoverbs for dynamic prefixes
files_reviewed:
  - pkg/cli/audit_run_pipeline.go
  - pkg/cli/audit_run_pipeline_test.go
  - pkg/linters/errorfwrapv/errorfwrapv.go
  - pkg/linters/errorfwrapv/testdata/src/errorfwrapv/errorfwrapv.go
  - pkg/linters/fmterrorfnoverbs/fmterrorfnoverbs.go
  - pkg/linters/fmterrorfnoverbs/testdata/src/fmterrorfnoverbs/fmterrorfnoverbs.go
  - pkg/linters/internal/astutil/astutil.go
  - pkg/linters/internal/astutil/astutil_test.go
comment_count: 2

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • github.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "github.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 20.6 AIC · ⌖ 7.49 AIC · ⊞ 23.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes

The format-string resolver is still making claims it cannot prove once a non-literal operand is involved, so these linters will produce false positives on valid fmt.Errorf calls.

The new `ResolveFormatString` helper flattens opaque operands into placeholders, but both analyzers still treat the remaining verbs as if argument indexing stayed intact. That breaks on concatenations where the unknown prefix already contains verbs or width/precision operands, and `fmterrorfnoverbs` now also reports "no verbs" for calls whose runtime prefix may contain them. Until the analysis tracks opaque segments explicitly, the new support is not reliable enough to merge.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • github.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "github.com"

See Network Configuration for more information.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 20.6 AIC · ⌖ 7.49 AIC · ⊞ 23.5K
Comment /review to run again

}

if !hasRealFormatVerb(val) {
if !hasRealFormatVerb(formatStr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change makes fmterrorfnoverbs report errors.New for concatenated format strings even when the dynamic prefix may contain real verbs, so it will now suggest a broken rewrite for valid fmt.Errorf(prefix+" suffix") calls.

💡 A non-literal prefix means "no verbs in the literal tail" is not the same as "no verbs at runtime."

ResolveFormatString only proves facts about the literal pieces. If prefix contains %d, the existing call still has formatting semantics and errors.New is not equivalent. The current generic message avoids a bad auto-fix string, but the diagnostic itself is still unsound. Limit this rule to fully literal format strings, or teach the helper to surface an "unknown verbs present" state that suppresses the report.

}

verbs := parseFormatVerbs(lit.Value)
verbs := parseFormatVerbs(formatStr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This linter still misclassifies error arguments after an opaque prefix because ResolveFormatString preserves later verbs but throws away how many runtime verbs the prefix may have consumed. fmt.Errorf(prefix+" suffix: %v", err) is valid when prefix already contains %w, yet this code will still flag the trailing %v path incorrectly.

💡 The analyzer can only reason about argument positions when every earlier verb is known.

Once a non-literal segment appears before a literal verb, nextArgIdx is no longer trustworthy: the runtime prefix may consume zero, one, or several arguments, including an existing %w. That means classifyErrorArgs can attach the later %v to the wrong argument and emit a bogus diagnostic. A safer fix is to stop analyzing any call whose unresolved segment appears before the verb you want to judge, or carry structured segments plus an "unknown arg consumption" state instead of flattening to a plain string.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor
🏗️ ADR Required — draft added for PR #58715

I added a draft ADR at docs/adr/58715-fix-concatenated-fmt-errorf-analysis.md because this PR exceeds the design gate threshold (>100 added lines in business-logic directories) and did not include an ADR.

Evidence reviewed

  • Prefetch summary: default_business_additions=185, requires_adr_by_default_volume=true
  • PR title/body: fixes cacheRecoveryError error wrapping and extends linter analysis for concatenated fmt.Errorf format strings
  • Diff/files: introduces shared astutil.ResolveFormatString, updates errorfwrapv and fmterrorfnoverbs, and adds regression tests

Draft ADR scope

The draft captures the architectural decision to support concatenated fmt.Errorf format-string analysis through a shared AST utility, rather than keeping literal-only linting or applying a one-off production fix.

Next action

Please review and refine the ADR rationale/trade-offs in docs/adr/58715-fix-concatenated-fmt-errorf-analysis.md, then keep the PR aligned with that recorded decision.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • proxy.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"

See Network Configuration for more information.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · pi · gpt54 · 17.2 AIC · ⌖ 11.6 AIC · ⊞ 9.7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd to the %v%w fix and the new astutil.ResolveFormatString helper.

📋 Key Themes & Highlights

Key Themes

  • Root cause well-addressed: cacheRecoveryError now wraps with %w, verified via errors.Is in the updated test.
  • ResolveFormatString is a solid, well-documented shared abstraction, with the opaque-placeholder design specifically preventing verb fabrication across concatenation boundaries — good defensive test coverage in astutil_test.go.
  • Two existing Copilot review threads (argument-index misattribution when an opaque prefix's runtime value contains a %-verb) remain unresolved in this PR; I did not duplicate them but flagged one related gap: no explicit regression test documents this known limitation for future contributors.
  • Minor: the fmterrorfnoverbs concatenated-format fallback message isn't asserted precisely by its testdata want regex, so wording drift wouldn't be caught by CI.

Positive Highlights

  • cacheRecoveryError test now asserts both errors.Is and errors.Unwrap != nil — solid regression coverage for the actual bug.
  • ✅ New testdata added per-linter mirrors the real-world cacheRecoveryError shape (prefix + literal...), directly closing the gap that let the original bug through undetected.
  • formatOpaquePlaceholder design is a thoughtful, well-commented safeguard against a subtle false-negative/false-positive class of bugs.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 38.5 AIC · ⌖ 14.8 AIC · ⊞ 10.3K
Comment /matt to run again

@@ -1078,6 +1078,79 @@ func TestStringLitValue(t *testing.T) {
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] Good coverage of the opaque-placeholder verb-fabrication boundary, but there's no regression test for the argument-index shift when an opaque operand's runtime value itself contains a %-verb (e.g. prefix := "%d: "). ResolveFormatString correctly can't resolve that content, but downstream consumers (parseFormatVerbs in errorfwrapv) still count implicit arg indices as if the opaque segment consumed zero verbs, which can silently misattribute later verbs to the wrong call.Args index.

💡 Suggested regression test

A case like the reviewer's own example is worth adding as an explicit testdata scenario in errorfwrapv/testdata (or a targeted parseFormatVerbs/classifyErrorArgs unit test) that documents the current known-limitation, so a future contributor doesn't need to rediscover it from the existing PR review comment thread.

@copilot please address this.

return
}
pass.ReportRangef(call, "fmt.Errorf called with no format verbs; use errors.New(%s) instead", lit.Value)
if _, isPlainLit := call.Args[0].(*ast.BasicLit); isPlainLit {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] This branch's report message differs (errors.New(%q) for plain literals vs. a bare generic errors.New for concatenated ones) but neither test asserts the exact suggested message text for the concatenated case — concatNoVerbs's want regex only anchors on the prefix, so a future edit to the fallback wording wouldn't be caught by CI.

💡 Suggestion

Consider tightening the want regex in concatNoVerbs (fmterrorfnoverbs testdata) to match the full generic message, so the two code paths (plain-literal vs concatenated) stay independently verified as the message wording evolves.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I found one over-engineering issue in the new format-string handling: the shared recursive resolver and opaque-placeholder machinery are broader than the two linters need, and a smaller local helper would be easier to follow. net: -60 lines possible.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by ✂️ Ponytail Reviewer for #58715 · codex · mai10 · 5.11 AIC · ⌖ 10.6 AIC · ⊞ 14K
Comment /ponytail to run again

return s, true
}

// ResolveFormatString resolves a fmt.Errorf-style format-string argument

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pkg/linters/internal/astutil/astutil.go:L709: yagni: recursive ResolveFormatString helper with opaque-placeholder logic and supporting docs. Inline a tiny local helper in each linter and keep the AST handling scoped to the specific cases they need.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator
pr-sous-chef

@copilot Please take the next forward-progress pass on this PR.

  • Current review feedback says the new concatenated format-string resolver still makes claims it cannot prove when opaque operands are present.
  • Please address the blocking format-string-analysis concern on current HEAD, then run the pr-finisher skill and summarize the maintainer-facing outcome.
  • If there is a narrower safe fix than the current shared resolver approach, prefer that and explain the validation you ran.

Generated by PR Sous Chef: https://github.com/github/gh-aw/actions/runs/33946898762

Generated by 👨‍🍳 PR Sous Chef · pi · gpt54 · 26.1 AIC · ⌖ 8.68 AIC · ⊞ 9.2K ·
Comment /souschef to run again

…format string analysis

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author
pr-sous-chef
```...

Addressed review feedback on current HEAD:

  1. cacheRecoveryError refactored: Updated cacheRecoveryError to format message via %s in a static format string (fmt.Errorf("%s\n\n...", message, runID, runOutputDir, err)), preserving %w wrapping while avoiding dynamic concatenation in the format string argument.
  2. astutil.ResolveFormatString tightened: ResolveFormatString now requires full resolution of +-concatenated string literal trees. If an expression contains opaque non-literal operands (identifiers, calls), it returns ok = false so linters (errorfwrapv and fmterrorfnoverbs) skip analysis when verb counts or argument positions cannot be proven at compile time.
  3. Validation: All tests and linters pass locally via make agent-report-progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

errorfwrapv/fmterrorfnoverbs BasicLit-only format-string blind spot still hides cacheRecoveryError's %v-not-%w bug (expired issu

4 participants