Skip to content

[linter-miner] linter: add sprintfbool — flag fmt.Sprintf("%t", b) → strconv.FormatBool(b)#46898

Merged
pelikhan merged 4 commits into
mainfrom
linter-miner/sprintfbool-b9bf09c656c2114f
Jul 21, 2026
Merged

[linter-miner] linter: add sprintfbool — flag fmt.Sprintf("%t", b) → strconv.FormatBool(b)#46898
pelikhan merged 4 commits into
mainfrom
linter-miner/sprintfbool-b9bf09c656c2114f

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new sprintfbool Go static analysis linter that flags fmt.Sprintf("%t", b) calls where b is the exact predeclared bool type and suggests rewriting to strconv.FormatBool(b). The suggested fix automatically manages fmt/strconv import additions and removals.

Changes

New: pkg/linters/sprintfbool/

  • sprintfbool.go — Analysis pass (~450 lines): matches fmt.Sprintf("%t", b) with exact predeclared bool type; excludes named bool types, multi-arg calls, non-%t verbs; skips generated files; honours //nolint:sprintfbool; generates import-aware SuggestedFix edits (add strconv, remove fmt if unused, swap sole import, preserve aliases).
  • sprintfbool_test.go — Test harness using analysistest.RunWithSuggestedFixes.
  • testdata/src/sprintfbool/ — 9 fixture pairs: basic variable/literal, named bool (no diagnostic), multiple calls (single import edit), single fmt use (swap import), mixed use (keep fmt), aliased import, raw backtick imports, shadowed identifier (no fix), comment overlap (no fix).

Modified: cmd/linters/main.go

Adds sprintfbool import and registers sprintfbool.Analyzer alongside existing passes.

New: docs/adr/46898-add-sprintfbool-linter.md

Draft ADR documenting the decision, alternatives considered (extend sprintfint; use external linter), and consequences.

Files Changed

  • cmd/linters/main.go
  • pkg/linters/sprintfbool/sprintfbool.go
  • pkg/linters/sprintfbool/sprintfbool_test.go
  • pkg/linters/sprintfbool/testdata/src/sprintfbool/ (9 fixture pairs)
  • docs/adr/46898-add-sprintfbool-linter.md

Generated by PR Description Updater for #46898 · 37.1 AIC · ⌖ 5.6 AIC · ⊞ 4.8K ·

Reports fmt.Sprintf("%t", b) calls where b is a single bool value and
suggests using strconv.FormatBool(b) instead. This is a natural
complement to the existing sprintfint linter (which flags Sprintf("%d",n)
→ strconv.Itoa).

The linter:
- Matches only the exact predeclared bool type (not named bool aliases)
- Provides a SuggestedFix that rewrites the call and manages imports
- Skips generated files and respects //nolint:sprintfbool directives

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 Jul 20, 2026
@pelikhan
pelikhan marked this pull request as ready for review July 20, 2026 18:28
Copilot AI review requested due to automatic review settings July 20, 2026 18:28
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

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

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

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

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

PR Code Quality Reviewer completed the code quality review.

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.

Pull request overview

Adds the sprintfbool Go analyzer, recommending strconv.FormatBool over fmt.Sprintf("%t", ...).

Changes:

  • Implements detection, suppression, and suggested fixes.
  • Registers the analyzer with the multichecker.
  • Adds analyzer fixtures and golden tests.
Show a summary per file
File Description
cmd/linters/main.go Registers sprintfbool.
pkg/linters/sprintfbool/sprintfbool.go Implements analysis and fixes.
pkg/linters/sprintfbool/sprintfbool_test.go Runs analyzer tests.
pkg/linters/sprintfbool/testdata/src/sprintfbool/sprintfbool.go Provides test cases.
pkg/linters/sprintfbool/testdata/src/sprintfbool/sprintfbool.go.golden Defines expected fixes.

Review details

Tip

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

Comments suppressed due to low confidence (2)

pkg/linters/sprintfbool/sprintfbool.go:155

  • A suggested fix is attached to one diagnostic and must be usable independently. In a file with two violations and no existing strconv import, this guard causes the second diagnostic's fix to emit strconv.FormatBool(...) without adding strconv, so applying that editor code action alone leaves uncompilable code. Please restructure reporting so each offered action is self-contained (for example, expose a file-wide fix once rather than statefully stripping edits from later diagnostics).
	if seenImportFiles[file.Pos()] {
		return nil

pkg/linters/sprintfbool/sprintfbool.go:169

  • This only removes fmt when there is exactly one package use. If a file consists of two flagged fmt.Sprintf("%t", ...) calls, applying all suggested replacements adds strconv once but leaves fmt imported even though both uses disappeared, producing an unused-import compile error. Count whether all fmt uses are target calls, or build one file-level fix that rewrites every target and updates imports consistently.
	orphanFmt := fmtImported && countPkgUsesInFile(pass, file, fmtPkg) == 1
  • Files reviewed: 5/5 changed files
  • Comments generated: 3
  • Review effort level: Medium

Comment thread pkg/linters/sprintfbool/sprintfbool.go Outdated
Comment on lines +161 to +165
switch imp.Path.Value {
case `"` + strconvPkg + `"`:
strconvImported = true
case `"` + fmtPkg + `"`:
fmtImported = true
Comment thread pkg/linters/sprintfbool/sprintfbool.go Outdated
Comment on lines +133 to +137
if astutil.QualifierShadowed(pass.Pkg, call.Pos(), qualifier, strconvPkg) {
return nil
}

edits := []analysis.TextEdit{{
Comment on lines +143 to +145
if file != nil {
edits = append(edits, buildImportEdits(pass, file, seenImportFiles)...)
}

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /tdd — requesting changes on test coverage gaps and a correctness issue in the import-management logic.

📋 Key Themes & Highlights

Key Themes

  • Misleading diagnostic message: hardcodes b regardless of the actual argument expression
  • Missing import-edge-case fixtures: the three branching paths in buildImportEdits are not independently covered by test fixtures
  • Correctness risk in orphan-fmt detection: a file with multiple flagged calls where fmt is used only by those calls may not have fmt removed correctly

Positive Highlights

  • ✅ Clean structural symmetry with sprintfint — easy to review and maintain
  • ✅ Correct named-bool exclusion using types.Typ[types.Bool] identity
  • seenImportFiles guard prevents duplicate overlapping edits
  • nolint and generated-file skipping wired correctly

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 41.9 AIC · ⌖ 7.5 AIC · ⊞ 6.7K
Comment /matt to run again

Comment thread pkg/linters/sprintfbool/sprintfbool.go Outdated

pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),

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] The diagnostic message hardcodes b as the variable name, but the actual argument may be any expression (e.g. true, isFoo). The message reads use strconv.FormatBool(b) instead of fmt.Sprintf("%t", b) even when the argument is a literal true, which is confusing.

💡 Suggestion

Incorporate the actual argText into the message (same opportunity exists in sprintfint):

Message: fmt.Sprintf(`use strconv.FormatBool(%s) instead of fmt.Sprintf("%%t", %s)`, argText, argText),

@copilot please address this.


func TestSprintfBool(t *testing.T) {
testdata := analysistest.TestData()
analysistest.RunWithSuggestedFixes(t, testdata, sprintfbool.Analyzer, "sprintfbool")

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] The test file contains only a single RunWithSuggestedFixes call covering all cases in the fixture. There are no targeted sub-tests for the import-management edge cases: (a) file where strconv is already imported, (b) file where fmt has multiple callers (so fmt should be kept), and (c) file with a single non-parenthesised import. These paths in buildImportEdits are hard to exercise from a single golden file.

💡 Suggestion

Add additional fixture files / sub-directories for each import scenario, e.g.:

  • testdata/src/sprintfbool_already_strconv/strconv already imported, only call rewrite needed
  • testdata/src/sprintfbool_keep_fmt/fmt used elsewhere, only add strconv
  • testdata/src/sprintfbool_single_import/ — single bare import "fmt" that must be rewritten to a grouped import

This mirrors how other linters in this repo organise multi-fixture tests.

@copilot please address this.

Comment thread pkg/linters/sprintfbool/sprintfbool.go Outdated
strconvImported := false
fmtImported := false
for _, imp := range file.Imports {
switch imp.Path.Value {

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] countPkgUsesInFile counts all uses of fmt to decide if fmt is an orphan, but the Preorder visitor has already matched exactly one fmt.Sprintf call. If the file has two flagged fmt.Sprintf("%t", b) calls and nothing else using fmt, orphanFmt will be false on the first call (count == 2) and true on the second — leading to an incorrect edit order where fmt is only removed for the second occurrence. seenImportFiles stops further edits, so the removal is skipped entirely.

💡 Analysis

The root issue is that countPkgUsesInFile counts at static analysis time (before any rewrites), so it cannot know how many uses will remain after all fixes are applied. Consider computing the orphan check lazily or deferring import cleanup to a separate pass — as golang.org/x/tools/go/analysis/passes/buildssa does.

@copilot please address this.


// badLiteral demonstrates the flagged pattern with a bool literal.
func badLiteral() string {
return fmt.Sprintf("%t", true) // want `use strconv\.FormatBool\(b\) instead of fmt\.Sprintf\("%t", b\)`

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] The golden file keeps the // want comment on the rewritten line, but RunWithSuggestedFixes verifies the fixed output — the want directive in the golden file is a false marker that will never match and could mislead future maintainers.

💡 Suggestion

The analysistest golden-file convention is that the .golden file shows the state after the fix, with no // want annotations. Remove the // want lines from sprintfbool.go.golden (lines 15 and 19) to keep the golden file clean and consistent with the rest of the codebase.

@copilot please address this.

Co-Authored-By: Claude Sonnet 4.6 <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 (443 new lines in pkg/linters/) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/46898-add-sprintfbool-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 at docs/adr/46898-add-sprintfbool-linter.md — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  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-46898: Add sprintfbool 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. 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., 46898-add-sprintfbool-linter.md for PR #46898).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 54.8 AIC · ⌖ 10.2 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

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.

The implementation is clean and follows established linter patterns in this repo.

  • Named bool types are correctly excluded via types.Typ[types.Bool] identity check
  • Import management (add strconv / remove orphaned fmt) handles all edge cases correctly
  • seenImportFiles guard prevents overlapping edits when a file has multiple flagged calls
  • Test fixture and golden file are consistent

No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 32 AIC · ⌖ 4.32 AIC · ⊞ 5K

@github-actions github-actions Bot left a comment

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.

REQUEST_CHANGES — two correctness bugs in the suggested-fix path must be addressed before merging.

Blocking issues

1. orphanFmt count is wrong for files with multiple flagged calls (HIGH)

Line 195: countPkgUsesInFile returns the total number of fmt uses in the file. When a file has 2+ fmt.Sprintf("%t", b) calls and nothing else that uses fmt, orphanFmt is false for every diagnostic. After all fixes are applied, the fmt import is orphaned and the fixed file does not compile. The test fixture only exercises the single-call case, so this is untested.

2. addStrconvRemoveFmtEdits emits edits in reverse-position order (MEDIUM)

Line 267–279: the strconv insertion (at fmtDecl.Rparen, higher offset) is emitted before the fmt-line deletion (at lineStart, lower offset). analysistest sorts before applying (so tests pass), but gopls and golangci-lint consume edits in slice order, corrupting the import block when both edits are applied.

🔎 Code quality review by PR Code Quality Reviewer · 37 AIC · ⌖ 4.7 AIC · ⊞ 5.6K
Comment /review to run again


func countPkgUsesInFile(pass *analysis.Pass, file *ast.File, pkgPath string) int {
fileStart, fileEnd := file.Pos(), file.End()
count := 0

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.

Orphan-fmt removal silently fails when a file has multiple flagged calls, leaving an unused fmt import after the fix is applied — this produces a compile error.

💡 Explanation and suggested fix

countPkgUsesInFile counts all uses of fmt in the file at analysis time (original AST). When a file has two fmt.Sprintf("%t", b) calls and no other fmt use, the count is 2, so orphanFmt is false for both diagnostics. Neither fix emits a fmt import removal. After both suggested fixes are accepted, fmt is still imported but has no remaining uses, causing a compile error.

A clean fix: before emitting diagnostics, do a single pre-scan of the file to count matching calls, then:

orthanFmt := fmtImported && countPkgUsesInFile(pass, file, fmtPkg) == matchingCallsInThisFile

Alternatively, emit the import-removal edit only for the first diagnostic (index 0) so it fires once per file regardless of call count, paired with the seenImportFiles guard.

The golden file in testdata only covers a single call — this bug is untested.

if specText == "" {
continue
}
return analysis.TextEdit{

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.

addStrconvRemoveFmtEdits returns TextEdits in reverse-position order, which can corrupt source when tools apply edits sequentially without sorting.

💡 Explanation and fix

The returned slice is:

[]analysis.TextEdit{
    {Pos: fmtDecl.Rparen, ...},  // INSERT strconv — higher offset
    {Pos: lineStart, End: lineEnd, NewText: nil},  // DELETE fmt line — lower offset
}

The Go analysis framework's analysistest sorts edits before applying them (so tests pass), but gopls and golangci-lint's autofixer apply edits in slice order. Applying the higher-offset insertion first shifts byte offsets, so the subsequent delete targets a now-wrong range, corrupting the import block.

Fix: reverse the slice order so the lower-position deletion comes first:

return []analysis.TextEdit{
    {Pos: lineStart, End: lineEnd, NewText: nil},          // delete fmt line first
    {Pos: fmtDecl.Rparen, End: fmtDecl.Rparen,
     NewText: []byte("\t\"strconv\"\n")},                 // then insert strconv
}

@github-actions

Copy link
Copy Markdown
Contributor Author

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 1 test(s): 1 design, 0 implementation, 0 violation(s).

📊 Metrics (1 test)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 7 edge case scenarios ✅
Duplicate clusters 0
Inflation 0.19:1 (17 test lines / 334 prod lines) — Excellent ✅
🚨 Violations 0
Test File Classification Issues
TestSprintfBool pkg/linters/sprintfbool/sprintfbool_test.go:14 Behavioral contract None — comprehensive analyzer test

✅ Test Coverage Highlights

Test Function: TestSprintfBool — Uses analysistest.RunWithSuggestedFixes() to execute the analyzer and verify both diagnostics and suggested fixes.

Testdata Coverage (7 scenarios):

  1. Bad case: fmt.Sprintf("%t", b) with bool parameter → correctly flagged with suggestion
  2. Bool literal: fmt.Sprintf("%t", true) → correctly flagged
  3. Already good: strconv.FormatBool(b) → NOT flagged (negative case)
  4. Named type edge case: fmt.Sprintf("%t", myBool) → NOT flagged (type mismatch)
  5. Multiple verbs edge case: fmt.Sprintf("%t %t", a, b) → NOT flagged (wrong format)
  6. Different verb: fmt.Sprintf("%v", b) → NOT flagged (wrong format string)
  7. Nolint suppression: //nolint:sprintfbool directive respected

Build tag compliance: ✅ Correctly includes //go:build !integration

No mocking violations: ✅ No gomock, testify/mock, or .EXPECT() patterns

Verdict

Passed. 0% implementation tests (threshold: 30%). Test is a high-value behavioral contract test that comprehensively verifies analyzer diagnostics and suggested fixes across 7 edge cases with proper negative cases. Build tag present. No violations.

🧪 Test quality analysis by Test Quality Sentinel · 12.5 AIC · ⌖ 13.2 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

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.

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%).

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Run: https://github.com/github/gh-aw/actions/runs/29769168911

Warning

Firewall blocked 1 domain

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

  • pi.dev

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

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 9.71 AIC · ⌖ 5.66 AIC · ⊞ 6.9K ·
Comment /souschef to run again

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

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Run: https://github.com/github/gh-aw/actions/runs/29769168911...

Addressed unresolved review feedback in 26e0756. I ran make fmt, make lint, make test-unit, make test, and make recompile locally; all passed. CI on this new head won’t auto-rerun from the agent push, so a maintainer re-trigger is still needed.

Copilot AI requested a review from gh-aw-bot July 20, 2026 19:27
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Run: https://github.com/github/gh-aw/actions/runs/29777731070

Warning

Firewall blocked 1 domain

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

  • pi.dev

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

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 7.66 AIC · ⌖ 7.84 AIC · ⊞ 6.9K ·
Comment /souschef to run again

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@copilot please run the pr-finisher skill, address any unresolved review feedback, and rerun checks once the branch is up to date.

Run: https://github.com/github/gh-aw/actions/runs/29777731070...

Checked the current head (26e0756) and there are no new actionable unresolved review items beyond feedback already addressed in that commit. I re-ran local checks (make fmt, make lint, make test-unit, make test, make recompile) and they pass. CI on the latest agent head is still unverified because agent pushes don’t trigger full PR workflows; a maintainer needs to re-trigger CI before merge.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.

Warning

Firewall blocked 1 domain

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

  • pi.dev

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

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 4.79 AIC · ⌖ 5.1 AIC · ⊞ 6.9K ·
Comment /souschef to run again

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.

...

Reviewed unresolved threads on the current head and found no additional actionable code issues beyond what was fixed in 26e0756 (now on updated head 5c0c64c). Re-ran make fmt, make lint, make test-unit, make test, and make recompile; all pass locally. CI on this agent head remains unverified until a maintainer re-triggers workflows.

@pelikhan
pelikhan merged commit ba5a436 into main Jul 21, 2026
24 checks passed
@pelikhan
pelikhan deleted the linter-miner/sprintfbool-b9bf09c656c2114f branch July 21, 2026 03:44
@github-actions

Copy link
Copy Markdown
Contributor Author

🎉 This pull request is included in a new release.

Release: v0.82.15

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.

4 participants