From 18c8a69b4f9cfa50474c2faf166154ef07c0ce55 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:20:32 +0000 Subject: [PATCH 1/4] =?UTF-8?q?linter:=20add=20sprintfint=20=E2=80=94=20fl?= =?UTF-8?q?ag=20fmt.Sprintf("%d",=20x)=20where=20x=20is=20int?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new custom Go analysis linter, `sprintfint`, that reports calls of the form `fmt.Sprintf("%d", x)` where `x` is a single value of type `int`. These can be rewritten as `strconv.Itoa(x)`, which is simpler, avoids format-string parsing at runtime, and makes the intent clearer. ## What it catches ```go // flagged s := fmt.Sprintf("%d", n) // n is int // preferred s := strconv.Itoa(n) ``` ## What it does NOT flag - `fmt.Sprintf("%d", n64)` — n64 is int64 (use strconv.FormatInt) - `fmt.Sprintf("%d", u)` — u is uint - `fmt.Sprintf("%d %d", a, b)` — multiple verbs - Calls suppressed with `//nolint:sprintfint` ## Implementation - `pkg/linters/sprintfint/sprintfint.go` — analyzer - `pkg/linters/sprintfint/sprintfint_test.go` — tests via analysistest - `pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go` — fixtures - `cmd/linters/main.go` — registration in multichecker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cmd/linters/main.go | 2 + pkg/linters/sprintfint/sprintfint.go | 109 ++++++++++++++++++ pkg/linters/sprintfint/sprintfint_test.go | 17 +++ .../testdata/src/sprintfint/sprintfint.go | 48 ++++++++ 4 files changed, 176 insertions(+) create mode 100644 pkg/linters/sprintfint/sprintfint.go create mode 100644 pkg/linters/sprintfint/sprintfint_test.go create mode 100644 pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go diff --git a/cmd/linters/main.go b/cmd/linters/main.go index 5c247299329..0b1050598e9 100644 --- a/cmd/linters/main.go +++ b/cmd/linters/main.go @@ -46,6 +46,7 @@ import ( "github.com/github/gh-aw/pkg/linters/sortslice" "github.com/github/gh-aw/pkg/linters/sprintferrdot" "github.com/github/gh-aw/pkg/linters/sprintferrorsnew" + "github.com/github/gh-aw/pkg/linters/sprintfint" "github.com/github/gh-aw/pkg/linters/ssljson" "github.com/github/gh-aw/pkg/linters/strconvparseignorederror" "github.com/github/gh-aw/pkg/linters/stringreplaceminusone" @@ -85,6 +86,7 @@ func main() { seenmapbool.Analyzer, sortslice.Analyzer, sprintferrdot.Analyzer, + sprintfint.Analyzer, sprintferrorsnew.Analyzer, strconvparseignorederror.Analyzer, stringreplaceminusone.Analyzer, diff --git a/pkg/linters/sprintfint/sprintfint.go b/pkg/linters/sprintfint/sprintfint.go new file mode 100644 index 00000000000..f9c7359a166 --- /dev/null +++ b/pkg/linters/sprintfint/sprintfint.go @@ -0,0 +1,109 @@ +// Package sprintfint implements a Go analysis linter that flags +// fmt.Sprintf("%d", x) calls where x is a single int value and suggests +// using strconv.Itoa(x) instead. +package sprintfint + +import ( + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + + "github.com/github/gh-aw/pkg/linters/internal/astutil" + "github.com/github/gh-aw/pkg/linters/internal/filecheck" + "github.com/github/gh-aw/pkg/linters/internal/nolint" +) + +// Analyzer is the sprintfint analysis pass. +var Analyzer = &analysis.Analyzer{ + Name: "sprintfint", + Doc: "reports fmt.Sprintf(\"%d\", x) calls where x is a single int value; use strconv.Itoa(x) instead", + URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/sprintfint", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +func run(pass *analysis.Pass) (any, error) { + insp, err := astutil.Inspector(pass) + if err != nil { + return nil, err + } + noLintLinesByFile := nolint.BuildLineIndex(pass, "sprintfint") + + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + + insp.Preorder(nodeFilter, func(n ast.Node) { + call, ok := n.(*ast.CallExpr) + if !ok { + return + } + + pos := pass.Fset.PositionFor(call.Pos(), false) + if filecheck.IsTestFile(pos.Filename) { + return + } + if nolint.HasDirective(pos, noLintLinesByFile) { + return + } + + // Match fmt.Sprintf(format, arg) with exactly two arguments. + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok || sel.Sel.Name != "Sprintf" { + return + } + if !astutil.IsPkgSelector(pass, sel, "fmt") { + return + } + if len(call.Args) != 2 { + return + } + + // The format argument must be the string literal "%d". + formatLit, ok := call.Args[0].(*ast.BasicLit) + if !ok || formatLit.Kind != token.STRING || formatLit.Value != `"%d"` { + return + } + + // The value argument must have the exact type int (not int64, uint, etc.). + arg := call.Args[1] + argType := pass.TypesInfo.TypeOf(arg) + if argType == nil { + return + } + if argType.Underlying() != types.Typ[types.Int] { + return + } + + pass.Report(analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: `use strconv.Itoa(x) instead of fmt.Sprintf("%d", x)`, + SuggestedFixes: buildItoaFix(pass, call, arg), + }) + }) + + return nil, nil +} + +// buildItoaFix returns a SuggestedFix rewriting +// fmt.Sprintf("%d", x) → strconv.Itoa(x). +func buildItoaFix(pass *analysis.Pass, call *ast.CallExpr, arg ast.Expr) []analysis.SuggestedFix { + argText := astutil.NodeText(pass.Fset, arg) + if argText == "" { + return nil + } + return []analysis.SuggestedFix{{ + Message: "Replace fmt.Sprintf with strconv.Itoa", + TextEdits: []analysis.TextEdit{ + { + Pos: call.Pos(), + End: call.End(), + NewText: []byte("strconv.Itoa(" + argText + ")"), + }, + }, + }} +} diff --git a/pkg/linters/sprintfint/sprintfint_test.go b/pkg/linters/sprintfint/sprintfint_test.go new file mode 100644 index 00000000000..40d3fa685b5 --- /dev/null +++ b/pkg/linters/sprintfint/sprintfint_test.go @@ -0,0 +1,17 @@ +//go:build !integration + +// Package sprintfint_test provides tests for the sprintfint analyzer. +package sprintfint_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + + "github.com/github/gh-aw/pkg/linters/sprintfint" +) + +func TestSprintfInt(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, sprintfint.Analyzer, "sprintfint") +} diff --git a/pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go b/pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go new file mode 100644 index 00000000000..7b7d75fa849 --- /dev/null +++ b/pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go @@ -0,0 +1,48 @@ +// Package sprintfint is the test fixture for the sprintfint analyzer. +package sprintfint + +import ( + "fmt" + "strconv" +) + +// bad demonstrates the flagged pattern: fmt.Sprintf with a single "%d" verb +// and an int argument, which should be replaced by strconv.Itoa. +func bad(n int) string { + return fmt.Sprintf("%d", n) // want `use strconv\.Itoa\(x\) instead of fmt\.Sprintf\("%d", x\)` +} + +// badLiteral demonstrates the flagged pattern with an integer literal. +func badLiteral() string { + return fmt.Sprintf("%d", 42) // want `use strconv\.Itoa\(x\) instead of fmt\.Sprintf\("%d", x\)` +} + +// goodStrconvItoa is already using the preferred form — no diagnostic expected. +func goodStrconvItoa(n int) string { + return strconv.Itoa(n) +} + +// goodInt64 uses int64 rather than int — not flagged; use strconv.FormatInt. +func goodInt64(n int64) string { + return fmt.Sprintf("%d", n) +} + +// goodUint uses uint — not flagged. +func goodUint(n uint) string { + return fmt.Sprintf("%d", n) +} + +// goodMultipleVerbs uses more than one verb — not flagged. +func goodMultipleVerbs(a, b int) string { + return fmt.Sprintf("%d %d", a, b) +} + +// goodOtherVerb uses a different verb — not flagged. +func goodOtherVerb(n int) string { + return fmt.Sprintf("%v", n) +} + +// suppressed suppresses the linter directive — no diagnostic expected. +func suppressed(n int) string { + return fmt.Sprintf("%d", n) //nolint:sprintfint +} From 52f92c3e0f99bb40b6fa668817e5cf2a1e4fb6b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:41:24 +0000 Subject: [PATCH 2/4] docs(adr): draft ADR-42538 for sprintfint linter addition Co-Authored-By: Claude Sonnet 4.6 --- docs/adr/42538-add-sprintfint-linter.md | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/adr/42538-add-sprintfint-linter.md diff --git a/docs/adr/42538-add-sprintfint-linter.md b/docs/adr/42538-add-sprintfint-linter.md new file mode 100644 index 00000000000..ff8e3e9b1f1 --- /dev/null +++ b/docs/adr/42538-add-sprintfint-linter.md @@ -0,0 +1,49 @@ +# ADR-42538: Add sprintfint Linter — Flag fmt.Sprintf("%d", int) in Favor of strconv.Itoa + +**Date**: 2026-06-30 +**Status**: Draft +**Deciders**: pelikhan, linter-miner automation + +--- + +### Context + +Two production code sites in the repository (`pkg/stringutil/stringutil.go:59` and `pkg/console/console_wasm.go:41`) use `fmt.Sprintf("%d", x)` where `x` is a plain `int`. This idiom is correct but suboptimal: it incurs format-string parsing at runtime, requires importing the `fmt` package unnecessarily, and obscures intent compared to `strconv.Itoa(x)`. The project already encodes code-quality preferences as custom Go static-analysis linters in `pkg/linters/` (e.g., `sprintferrdot`, `sprintferrorsnew`), making a new linter the natural enforcement mechanism. Automated enforcement prevents this pattern from re-entering the codebase after the two existing instances are fixed. + +### Decision + +We will add a new custom Go analysis pass, `sprintfint`, that flags every call of the form `fmt.Sprintf("%d", x)` where `x` has the exact type `int` and reports `use strconv.Itoa(x) instead`. The analyzer includes a `SuggestedFix` that rewrites the call in place, skips `_test.go` files, and respects `//nolint:sprintfint` suppressions. It is registered in the project-wide `cmd/linters` multichecker following the same conventions as existing linters. + +### Alternatives Considered + +#### Alternative 1: Rely on code review alone + +Do not add any automated rule; trust that code authors and reviewers catch `fmt.Sprintf("%d", int)` during review. This was the implicit status quo and did not prevent the two existing instances from entering production code. Without automation the pattern will continue to recur as the codebase grows. + +#### Alternative 2: Use an existing external linter (e.g., perfsprint from golangci-lint) + +The `perfsprint` linter (available in `golangci-lint`) covers similar patterns. However, it operates outside the project's custom-linter infrastructure, requires an additional external dependency and configuration layer, and its scope is broader than the narrow `fmt.Sprintf("%d", int)` → `strconv.Itoa` substitution the team wants to enforce. Adopting it would also break the convention of keeping all custom lint rules within `pkg/linters/` where they share internal helpers (`astutil`, `filecheck`, `nolint`). + +#### Alternative 3: Extend the linter to cover all integer types (int8, int32, int64, uint, etc.) + +A broader rule would flag `fmt.Sprintf("%d", n64)` (int64) and suggest `strconv.FormatInt(n64, 10)`, and similarly for other integer widths. This was not chosen because the safe mechanical replacement (`strconv.Itoa`) is only available for the plain `int` type; wider coverage would require type-specific suggestions that complicate the implementation and increase false-positive risk for less common types. The narrow scope can be expanded in a follow-up linter if needed. + +### Consequences + +#### Positive +- Eliminates a class of redundant format-string calls in production code, reducing minor runtime overhead and improving readability. +- Provides a `SuggestedFix` so editors and `go tool` can auto-apply the rewrite, lowering friction for contributors. +- Consistent with the project's established pattern of encoding style decisions as independently testable `golang.org/x/tools/go/analysis` passes. +- Prevents regression: the two existing instances and any future occurrences will be caught in CI. + +#### Negative +- Scope is restricted to the exact type `int`; the same smell with `int64`, `uint`, `int32`, etc. remains undetected and must be addressed separately. +- Adds another analyzer to the multichecker that must be maintained as Go's standard library and `x/tools/go/analysis` API evolve. + +#### Neutral +- Test fixtures follow the project's `analysistest` convention (`testdata/src//.go` with `// want` annotations), adding a small amount of checked-in test data. +- The `//nolint:sprintfint` suppression mechanism follows the same pattern as other linters in the suite, so team norms around nolint comments carry over without new conventions. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From 0c271b5118c9fb6d210264f7324e3b8ab5e1c32a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:05:51 +0000 Subject: [PATCH 3/4] chore: start pr finisher pass Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/mattpocock-skills-reviewer.lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mattpocock-skills-reviewer.lock.yml b/.github/workflows/mattpocock-skills-reviewer.lock.yml index 2280323fb07..2677ae9206f 100644 --- a/.github/workflows/mattpocock-skills-reviewer.lock.yml +++ b/.github/workflows/mattpocock-skills-reviewer.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"26e8a754482b7347113adc6d77b27f62e3dee49e84d972a2e35662fd319720b5","body_hash":"62ce0bd5c9851424f73f018934c48ae0ca6493b0bc7415fdc5564e808707829a","strict":true,"agent_id":"copilot","agent_model":"claude-sonnet-4.6","engine_versions":{"copilot":"1.0.65"}} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.13","digest":"sha256:691a06b64961b5b35aac117eaace202fa721e91da19d1d2e22dcdd6663cd571b","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.13@sha256:691a06b64961b5b35aac117eaace202fa721e91da19d1d2e22dcdd6663cd571b"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.13","digest":"sha256:c57febf4aeeefbb4fd96f5b12c07f4279ca55edca6a700032debf9dd0787286e","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.13@sha256:c57febf4aeeefbb4fd96f5b12c07f4279ca55edca6a700032debf9dd0787286e"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.13","digest":"sha256:79dfd3a5d139bd1956ba6d7d1782b831a07175cf5afa29c45cb20bb0140f23c5","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.13@sha256:79dfd3a5d139bd1956ba6d7d1782b831a07175cf5afa29c45cb20bb0140f23c5"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.13","digest":"sha256:700b1b5a73098373b04fb684f291e95d9be0124ab559717b04f27acaf8b41bed","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.13@sha256:700b1b5a73098373b04fb684f291e95d9be0124ab559717b04f27acaf8b41bed"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.32","digest":"sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.32@sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.15"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.15"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.32","digest":"sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.32@sha256:63e46b56dfd70895a701b6fc6dd0189e11e2d875f327f1781e81b31848735477"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ From 02539014cf7cb7287efc5ad227400f42ccd76db3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:13:07 +0000 Subject: [PATCH 4/4] fix sprintfint review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- cmd/linters/main.go | 2 +- pkg/linters/sprintfint/sprintfint.go | 4 ++-- .../sprintfint/testdata/src/sprintfint/sprintfint.go | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cmd/linters/main.go b/cmd/linters/main.go index 0b1050598e9..5abac288891 100644 --- a/cmd/linters/main.go +++ b/cmd/linters/main.go @@ -86,8 +86,8 @@ func main() { seenmapbool.Analyzer, sortslice.Analyzer, sprintferrdot.Analyzer, - sprintfint.Analyzer, sprintferrorsnew.Analyzer, + sprintfint.Analyzer, strconvparseignorederror.Analyzer, stringreplaceminusone.Analyzer, jsonmarshalignoredeerror.Analyzer, diff --git a/pkg/linters/sprintfint/sprintfint.go b/pkg/linters/sprintfint/sprintfint.go index f9c7359a166..1003267f937 100644 --- a/pkg/linters/sprintfint/sprintfint.go +++ b/pkg/linters/sprintfint/sprintfint.go @@ -19,7 +19,7 @@ import ( // Analyzer is the sprintfint analysis pass. var Analyzer = &analysis.Analyzer{ Name: "sprintfint", - Doc: "reports fmt.Sprintf(\"%d\", x) calls where x is a single int value; use strconv.Itoa(x) instead", + Doc: "reports fmt.Sprintf(\"%d\", x) calls where x is a single int value; use strconv.Itoa(x) instead (suggested fixes may require goimports to add/remove imports)", URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/sprintfint", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: run, @@ -74,7 +74,7 @@ func run(pass *analysis.Pass) (any, error) { if argType == nil { return } - if argType.Underlying() != types.Typ[types.Int] { + if argType != types.Typ[types.Int] { return } diff --git a/pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go b/pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go index 7b7d75fa849..7391894a6f9 100644 --- a/pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go +++ b/pkg/linters/sprintfint/testdata/src/sprintfint/sprintfint.go @@ -6,6 +6,8 @@ import ( "strconv" ) +type counter int + // bad demonstrates the flagged pattern: fmt.Sprintf with a single "%d" verb // and an int argument, which should be replaced by strconv.Itoa. func bad(n int) string { @@ -42,6 +44,12 @@ func goodOtherVerb(n int) string { return fmt.Sprintf("%v", n) } +// goodNamedInt uses a named int type — not flagged because strconv.Itoa +// requires the exact predeclared int type. +func goodNamedInt(n counter) string { + return fmt.Sprintf("%d", n) +} + // suppressed suppresses the linter directive — no diagnostic expected. func suppressed(n int) string { return fmt.Sprintf("%d", n) //nolint:sprintfint