diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ef47dbe..c050f4c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,9 +5,21 @@ on: branches: - main pull_request: + workflow_dispatch: + inputs: + arch: + description: Runner architecture for benchmarks + required: true + default: all + type: choice + options: + - all + - amd64 + - arm64 jobs: test: + if: ${{ github.event_name != 'workflow_dispatch' }} runs-on: ubuntu-latest env: GOTOOLCHAIN: local @@ -29,3 +41,123 @@ jobs: - name: Run workspace tests run: ./scripts/test-all.sh + + benchmark-amd64: + name: Benchmarks (linux/amd64) + if: ${{ github.event_name == 'workflow_dispatch' && (inputs.arch == 'all' || inputs.arch == 'amd64') }} + runs-on: ubuntu-latest + env: + ARCH: amd64 + GOTOOLCHAIN: local + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up latest stable Go + uses: actions/setup-go@v6 + with: + go-version: stable + check-latest: true + + - name: Run string builder benchmarks + shell: bash + run: | + set -euo pipefail + mkdir -p benchmark-results + { + printf '# stringbuilderlint benchmarks (%s/%s)\n' "$(go env GOOS)" "$(go env GOARCH)" + go version + printf '\n' + cd stringbuilderlint + go test -run '^$' -bench 'Benchmark(Concat|BuilderConcat|Sprintf|BuilderSprintf)' -benchmem -count 5 + } | tee "benchmark-results/stringbuilderlint-${ARCH}.txt" + + - name: Run lookup table benchmarks + shell: bash + run: | + set -euo pipefail + mkdir -p benchmark-results + { + printf '# lookuptablelint benchmarks (%s/%s)\n' "$(go env GOOS)" "$(go env GOARCH)" + go version + printf '\n' + cd lookuptablelint + go test -run '^$' -bench 'Benchmark(MapLookup|SwitchLookup)' -benchmem -count 5 + } | tee "benchmark-results/lookuptablelint-${ARCH}.txt" + + - name: Write benchmark summary + shell: bash + run: | + set -euo pipefail + { + printf '## Benchmarks (linux/%s)\n\n' "${ARCH}" + printf '```text\n' + cat benchmark-results/*.txt + printf '```\n' + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Upload benchmark results + uses: actions/upload-artifact@v7 + with: + name: benchmark-results-amd64 + path: benchmark-results/*.txt + + benchmark-arm64: + name: Benchmarks (linux/arm64) + if: ${{ github.event_name == 'workflow_dispatch' && (inputs.arch == 'all' || inputs.arch == 'arm64') }} + runs-on: ubuntu-24.04-arm + env: + ARCH: arm64 + GOTOOLCHAIN: local + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up latest stable Go + uses: actions/setup-go@v6 + with: + go-version: stable + check-latest: true + + - name: Run string builder benchmarks + shell: bash + run: | + set -euo pipefail + mkdir -p benchmark-results + { + printf '# stringbuilderlint benchmarks (%s/%s)\n' "$(go env GOOS)" "$(go env GOARCH)" + go version + printf '\n' + cd stringbuilderlint + go test -run '^$' -bench 'Benchmark(Concat|BuilderConcat|Sprintf|BuilderSprintf)' -benchmem -count 5 + } | tee "benchmark-results/stringbuilderlint-${ARCH}.txt" + + - name: Run lookup table benchmarks + shell: bash + run: | + set -euo pipefail + mkdir -p benchmark-results + { + printf '# lookuptablelint benchmarks (%s/%s)\n' "$(go env GOOS)" "$(go env GOARCH)" + go version + printf '\n' + cd lookuptablelint + go test -run '^$' -bench 'Benchmark(MapLookup|SwitchLookup)' -benchmem -count 5 + } | tee "benchmark-results/lookuptablelint-${ARCH}.txt" + + - name: Write benchmark summary + shell: bash + run: | + set -euo pipefail + { + printf '## Benchmarks (linux/%s)\n\n' "${ARCH}" + printf '```text\n' + cat benchmark-results/*.txt + printf '```\n' + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Upload benchmark results + uses: actions/upload-artifact@v7 + with: + name: benchmark-results-arm64 + path: benchmark-results/*.txt diff --git a/go.work b/go.work index a3ed35b..9bf6e5f 100644 --- a/go.work +++ b/go.work @@ -17,6 +17,7 @@ use ( ./shutdown ./slicer ./sqlconv + ./stringbuilderlint ./structify ./strutils ./tasker diff --git a/lookuptablelint/benchmark_test.go b/lookuptablelint/benchmark_test.go new file mode 100644 index 0000000..64e6fb7 --- /dev/null +++ b/lookuptablelint/benchmark_test.go @@ -0,0 +1,136 @@ +package lookuptablelint + +import "testing" + +var benchmarkBoolSink bool + +var lookupInputs = [...]string{ + "a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", +} + +var lookupMap2 = map[string]struct{}{ + "a": {}, + "b": {}, +} + +var lookupMap4 = map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, +} + +var lookupMap8 = map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, + "f": {}, + "g": {}, + "h": {}, +} + +var lookupMap16 = map[string]struct{}{ + "a": {}, + "b": {}, + "c": {}, + "d": {}, + "e": {}, + "f": {}, + "g": {}, + "h": {}, + "i": {}, + "j": {}, + "k": {}, + "l": {}, + "m": {}, + "n": {}, + "o": {}, + "p": {}, +} + +func lookupSwitch2(value string) bool { + switch value { + case "a", "b": + return true + default: + return false + } +} + +func lookupSwitch4(value string) bool { + switch value { + case "a", "b", "c", "d": + return true + default: + return false + } +} + +func lookupSwitch8(value string) bool { + switch value { + case "a", "b", "c", "d", "e", "f", "g", "h": + return true + default: + return false + } +} + +func lookupSwitch16(value string) bool { + switch value { + case "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p": + return true + default: + return false + } +} + +func BenchmarkMapLookup2(b *testing.B) { + for i := 0; b.Loop(); i++ { + _, benchmarkBoolSink = lookupMap2[lookupInputs[i&15]] + } +} + +func BenchmarkSwitchLookup2(b *testing.B) { + for i := 0; b.Loop(); i++ { + benchmarkBoolSink = lookupSwitch2(lookupInputs[i&15]) + } +} + +func BenchmarkMapLookup4(b *testing.B) { + for i := 0; b.Loop(); i++ { + _, benchmarkBoolSink = lookupMap4[lookupInputs[i&15]] + } +} + +func BenchmarkSwitchLookup4(b *testing.B) { + for i := 0; b.Loop(); i++ { + benchmarkBoolSink = lookupSwitch4(lookupInputs[i&15]) + } +} + +func BenchmarkMapLookup8(b *testing.B) { + for i := 0; b.Loop(); i++ { + _, benchmarkBoolSink = lookupMap8[lookupInputs[i&15]] + } +} + +func BenchmarkSwitchLookup8(b *testing.B) { + for i := 0; b.Loop(); i++ { + benchmarkBoolSink = lookupSwitch8(lookupInputs[i&15]) + } +} + +func BenchmarkMapLookup16(b *testing.B) { + for i := 0; b.Loop(); i++ { + _, benchmarkBoolSink = lookupMap16[lookupInputs[i&15]] + } +} + +func BenchmarkSwitchLookup16(b *testing.B) { + for i := 0; b.Loop(); i++ { + benchmarkBoolSink = lookupSwitch16(lookupInputs[i&15]) + } +} diff --git a/stringbuilderlint/analyzer.go b/stringbuilderlint/analyzer.go new file mode 100644 index 0000000..a94a006 --- /dev/null +++ b/stringbuilderlint/analyzer.go @@ -0,0 +1,812 @@ +package stringbuilderlint + +import ( + "bytes" + "fmt" + "go/ast" + "go/constant" + "go/format" + "go/token" + "go/types" + "path" + "strconv" + "strings" + + "golang.org/x/tools/go/analysis" +) + +const ( + analyzerName = "stringbuilderlint" + minConcatDynamicParts = 32 + minSprintfBuilderParts = 2 + stringsPath = "strings" + strconvPath = "strconv" +) + +// Analyzer reports string concatenation and primitive fmt.Sprintf calls that +// should use strings.Builder. +var Analyzer = &analysis.Analyzer{ + Name: analyzerName, + Doc: "check for string concatenation and primitive fmt.Sprintf calls that should use strings.Builder", + URL: "https://pkg.go.dev/github.com/omniaura/go-kit/stringbuilderlint", + Run: run, +} + +type reporter struct { + pass *analysis.Pass + file *ast.File + parents map[ast.Node]ast.Node + ignores suppressions +} + +func run(pass *analysis.Pass) (any, error) { + for _, file := range pass.Files { + if generated(file) { + continue + } + + r := reporter{ + pass: pass, + file: file, + parents: parentMap(file), + ignores: newSuppressions(pass, file), + } + ast.Inspect(file, func(node ast.Node) bool { + switch node := node.(type) { + case *ast.BinaryExpr: + r.checkConcat(node) + case *ast.CallExpr: + r.checkSprintf(node) + } + return true + }) + } + + return nil, nil +} + +func (r reporter) checkConcat(expr *ast.BinaryExpr) { + if expr.Op != token.ADD { + return + } + if !isString(r.pass.TypesInfo.Types[expr].Type) { + return + } + if isStringConcatParent(r.pass, r.parents, expr) { + return + } + if r.pass.TypesInfo.Types[expr].Value != nil && r.pass.TypesInfo.Types[expr].Value.Kind() == constant.String { + return + } + parts := stringConcatParts(r.pass, expr) + dynamicParts := countDynamicStringParts(r.pass, parts) + if dynamicParts < minConcatDynamicParts { + return + } + if r.ignores.ignored(expr.Pos()) { + return + } + + pieces := make([]builderPiece, 0, len(parts)) + for _, part := range parts { + if literal, ok := stringLiteralPiece(r.pass, part); ok { + pieces = append(pieces, literal) + continue + } + text, ok := renderNode(r.pass.Fset, part) + if !ok { + return + } + pieces = append(pieces, builderPiece{ + expr: part, + exprText: text, + conversion: conversion{kind: convertString}, + }) + } + + fix, ok := r.builderFix(expr.Pos(), expr.End(), pieces, "Replace with strings.Builder") + diag := analysis.Diagnostic{ + Pos: expr.Pos(), + End: expr.End(), + Message: fmt.Sprintf("string concatenation with %d dynamic parts should use strings.Builder", dynamicParts), + } + if ok { + diag.SuggestedFixes = []analysis.SuggestedFix{fix} + } + r.pass.Report(diag) +} + +func (r reporter) checkSprintf(call *ast.CallExpr) { + if !isFmtSprintfCall(r.pass, call) || r.ignores.ignored(call.Pos()) { + return + } + pieces, ok := r.sprintfPieces(call) + if !ok { + return + } + if len(pieces) < minSprintfBuilderParts { + return + } + fix, ok := r.builderFix(call.Pos(), call.End(), pieces, "Replace with strings.Builder") + diag := analysis.Diagnostic{ + Pos: call.Pos(), + End: call.End(), + Message: "fmt.Sprintf with primitive arguments should use strings.Builder and strconv", + } + if ok { + diag.SuggestedFixes = []analysis.SuggestedFix{fix} + } + r.pass.Report(diag) +} + +type builderPiece struct { + literal string + literalLen int + expr ast.Expr + exprText string + conversion conversion + tempName string +} + +func stringLiteralPiece(pass *analysis.Pass, expr ast.Expr) (builderPiece, bool) { + tv := pass.TypesInfo.Types[expr] + if tv.Value == nil || tv.Value.Kind() != constant.String { + return builderPiece{}, false + } + value := constant.StringVal(tv.Value) + return builderPiece{ + literal: strconv.Quote(value), + literalLen: len(value), + }, true +} + +func countDynamicStringParts(pass *analysis.Pass, parts []ast.Expr) int { + count := 0 + for _, part := range parts { + if _, ok := stringLiteralPiece(pass, part); ok { + continue + } + count++ + } + return count +} + +type conversionKind int + +const ( + convertString conversionKind = iota + 1 + convertBool + convertSigned + convertUnsigned + convertFloat +) + +type conversion struct { + kind conversionKind + base int + verb byte + precision int + bitSize int + upper bool +} + +func (r reporter) sprintfPieces(call *ast.CallExpr) ([]builderPiece, bool) { + if len(call.Args) == 0 { + return nil, false + } + formatValue := r.pass.TypesInfo.Types[call.Args[0]].Value + if formatValue == nil || formatValue.Kind() != constant.String { + return nil, false + } + segments, ok := parseFormat(constant.StringVal(formatValue)) + if !ok { + return nil, false + } + + argIndex := 1 + pieces := make([]builderPiece, 0, len(segments)) + for _, segment := range segments { + if segment.literal != "" { + pieces = append(pieces, builderPiece{ + literal: strconv.Quote(segment.literal), + literalLen: len(segment.literal), + }) + continue + } + if argIndex >= len(call.Args) { + return nil, false + } + expr := call.Args[argIndex] + argIndex++ + conv, ok := primitiveConversion(r.pass.TypesInfo.Types[expr].Type, segment) + if !ok { + return nil, false + } + text, ok := renderNode(r.pass.Fset, expr) + if !ok { + return nil, false + } + pieces = append(pieces, builderPiece{ + expr: expr, + exprText: text, + conversion: conv, + }) + } + if argIndex != len(call.Args) { + return nil, false + } + return pieces, true +} + +type formatSegment struct { + literal string + verb byte + precision int +} + +func parseFormat(format string) ([]formatSegment, bool) { + var segments []formatSegment + var literal strings.Builder + for i := 0; i < len(format); i++ { + if format[i] != '%' { + literal.WriteByte(format[i]) + continue + } + if i+1 >= len(format) { + return nil, false + } + if format[i+1] == '%' { + literal.WriteByte('%') + i++ + continue + } + if literal.Len() > 0 { + segments = append(segments, formatSegment{literal: literal.String()}) + literal.Reset() + } + + i++ + if strings.ContainsRune("#0+- ", rune(format[i])) { + return nil, false + } + if format[i] >= '0' && format[i] <= '9' { + return nil, false + } + precision := -2 + if format[i] == '.' { + i++ + if i >= len(format) || format[i] < '0' || format[i] > '9' { + return nil, false + } + precision = 0 + for i < len(format) && format[i] >= '0' && format[i] <= '9' { + precision = precision*10 + int(format[i]-'0') + i++ + } + if i >= len(format) { + return nil, false + } + } + if format[i] == '[' || format[i] == '*' { + return nil, false + } + segments = append(segments, formatSegment{ + verb: format[i], + precision: precision, + }) + } + if literal.Len() > 0 { + segments = append(segments, formatSegment{literal: literal.String()}) + } + return segments, true +} + +func primitiveConversion(t types.Type, segment formatSegment) (conversion, bool) { + basic, ok := t.Underlying().(*types.Basic) + if !ok { + return conversion{}, false + } + kind := basic.Kind() + switch segment.verb { + case 's': + return conversion{kind: convertString}, kind == types.String + case 't': + return conversion{kind: convertBool}, kind == types.Bool + case 'v': + switch { + case kind == types.String: + return conversion{kind: convertString}, true + case kind == types.Bool: + return conversion{kind: convertBool}, true + case signedInteger(kind): + return conversion{kind: convertSigned, base: 10}, true + case unsignedInteger(kind): + return conversion{kind: convertUnsigned, base: 10}, true + case floatKind(kind): + return conversion{kind: convertFloat, verb: 'g', precision: -1, bitSize: floatBitSize(kind)}, true + default: + return conversion{}, false + } + case 'd': + return integerConversion(kind, 10, false) + case 'b': + return integerConversion(kind, 2, false) + case 'o': + return integerConversion(kind, 8, false) + case 'x': + return integerConversion(kind, 16, false) + case 'X': + return integerConversion(kind, 16, true) + case 'e', 'E', 'f', 'F': + if !floatKind(kind) { + return conversion{}, false + } + verb := segment.verb + if verb == 'F' { + verb = 'f' + } + precision := segment.precision + if precision == -2 { + precision = 6 + } + return conversion{kind: convertFloat, verb: verb, precision: precision, bitSize: floatBitSize(kind)}, true + case 'g', 'G': + if !floatKind(kind) { + return conversion{}, false + } + precision := segment.precision + if precision == -2 { + precision = -1 + } + return conversion{kind: convertFloat, verb: segment.verb, precision: precision, bitSize: floatBitSize(kind)}, true + default: + return conversion{}, false + } +} + +func integerConversion(kind types.BasicKind, base int, upper bool) (conversion, bool) { + switch { + case signedInteger(kind): + return conversion{kind: convertSigned, base: base, upper: upper}, true + case unsignedInteger(kind): + return conversion{kind: convertUnsigned, base: base, upper: upper}, true + default: + return conversion{}, false + } +} + +func signedInteger(kind types.BasicKind) bool { + switch kind { + case types.Int, types.Int8, types.Int16, types.Int32, types.Int64: + return true + default: + return false + } +} + +func unsignedInteger(kind types.BasicKind) bool { + switch kind { + case types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64, types.Uintptr: + return true + default: + return false + } +} + +func floatKind(kind types.BasicKind) bool { + return kind == types.Float32 || kind == types.Float64 +} + +func floatBitSize(kind types.BasicKind) int { + if kind == types.Float32 { + return 32 + } + return 64 +} + +func (r reporter) builderFix(start, end token.Pos, pieces []builderPiece, message string) (analysis.SuggestedFix, bool) { + needsStrconv := false + for _, piece := range pieces { + switch piece.conversion.kind { + case convertBool, convertSigned, convertUnsigned, convertFloat: + needsStrconv = true + } + } + + imports := []importNeed{{pkgPath: stringsPath, defaultName: "strings"}} + if needsStrconv { + imports = append(imports, importNeed{pkgPath: strconvPath, defaultName: "strconv"}) + } + qualifiers, importEdits, ok := r.importsFor(imports) + if !ok { + return analysis.SuggestedFix{}, false + } + + used := namesInPieces(pieces) + for _, qualifier := range qualifiers { + used[qualifier] = true + } + builderName := uniqueName(used, "sb") + used[builderName] = true + for i := range pieces { + if pieces[i].expr == nil { + continue + } + name := uniqueName(used, fmt.Sprintf("builderPart%d", i)) + used[name] = true + pieces[i].tempName = name + } + + replacement, ok := renderBuilderReplacement(pieces, builderName, qualifiers[stringsPath], qualifiers[strconvPath]) + if !ok { + return analysis.SuggestedFix{}, false + } + edits := append([]analysis.TextEdit{}, importEdits...) + edits = append(edits, analysis.TextEdit{Pos: start, End: end, NewText: []byte(replacement)}) + return analysis.SuggestedFix{Message: message, TextEdits: edits}, true +} + +func renderBuilderReplacement(pieces []builderPiece, builderName, stringsQualifier, strconvQualifier string) (string, bool) { + var buf strings.Builder + buf.WriteString("func() string {\n") + for _, piece := range pieces { + if piece.expr == nil { + continue + } + converted, ok := convertedExpr(piece, stringsQualifier, strconvQualifier) + if !ok { + return "", false + } + fmt.Fprintf(&buf, "\t%s := %s\n", piece.tempName, converted) + } + fmt.Fprintf(&buf, "\tvar %s %s.Builder\n", builderName, stringsQualifier) + growParts := growTerms(pieces) + if len(growParts) > 0 { + fmt.Fprintf(&buf, "\t%s.Grow(%s)\n", builderName, strings.Join(growParts, " + ")) + } + for _, piece := range pieces { + if piece.literal != "" { + fmt.Fprintf(&buf, "\t%s.WriteString(%s)\n", builderName, piece.literal) + continue + } + fmt.Fprintf(&buf, "\t%s.WriteString(%s)\n", builderName, piece.tempName) + } + fmt.Fprintf(&buf, "\treturn %s.String()\n", builderName) + buf.WriteString("}()") + + formatted, err := format.Source([]byte("package p\n\nvar _ = " + buf.String())) + if err != nil { + return buf.String(), true + } + const prefix = "package p\n\nvar _ = " + return strings.TrimSpace(strings.TrimPrefix(string(formatted), prefix)), true +} + +func convertedExpr(piece builderPiece, stringsQualifier, strconvQualifier string) (string, bool) { + switch piece.conversion.kind { + case convertString: + return piece.exprText, true + case convertBool: + return fmt.Sprintf("%s.FormatBool(%s)", strconvQualifier, piece.exprText), true + case convertSigned: + text := fmt.Sprintf("%s.FormatInt(int64(%s), %d)", strconvQualifier, piece.exprText, piece.conversion.base) + if piece.conversion.upper { + text = fmt.Sprintf("%s.ToUpper(%s)", stringsQualifier, text) + } + return text, true + case convertUnsigned: + text := fmt.Sprintf("%s.FormatUint(uint64(%s), %d)", strconvQualifier, piece.exprText, piece.conversion.base) + if piece.conversion.upper { + text = fmt.Sprintf("%s.ToUpper(%s)", stringsQualifier, text) + } + return text, true + case convertFloat: + return fmt.Sprintf("%s.FormatFloat(float64(%s), %q, %d, %d)", strconvQualifier, piece.exprText, piece.conversion.verb, piece.conversion.precision, piece.conversion.bitSize), true + default: + return "", false + } +} + +func growTerms(pieces []builderPiece) []string { + var terms []string + literalBytes := 0 + flushLiteral := func() { + if literalBytes == 0 { + return + } + terms = append(terms, strconv.Itoa(literalBytes)) + literalBytes = 0 + } + for _, piece := range pieces { + if piece.literal != "" { + literalBytes += piece.literalLen + continue + } + flushLiteral() + terms = append(terms, "len("+piece.tempName+")") + } + flushLiteral() + return terms +} + +func namesInPieces(pieces []builderPiece) map[string]bool { + used := make(map[string]bool) + for _, piece := range pieces { + if piece.expr == nil { + continue + } + ast.Inspect(piece.expr, func(node ast.Node) bool { + ident, ok := node.(*ast.Ident) + if ok { + used[ident.Name] = true + } + return true + }) + } + return used +} + +func uniqueName(used map[string]bool, base string) string { + if !used[base] { + return base + } + for i := 1; ; i++ { + name := fmt.Sprintf("%s%d", base, i) + if !used[name] { + return name + } + } +} + +func stringConcatParts(pass *analysis.Pass, expr ast.Expr) []ast.Expr { + if paren, ok := expr.(*ast.ParenExpr); ok { + return stringConcatParts(pass, paren.X) + } + binary, ok := expr.(*ast.BinaryExpr) + if !ok || binary.Op != token.ADD || !isString(pass.TypesInfo.Types[binary].Type) { + return []ast.Expr{expr} + } + parts := stringConcatParts(pass, binary.X) + parts = append(parts, stringConcatParts(pass, binary.Y)...) + return parts +} + +func parentMap(root ast.Node) map[ast.Node]ast.Node { + parents := make(map[ast.Node]ast.Node) + var stack []ast.Node + ast.Inspect(root, func(node ast.Node) bool { + if node == nil { + stack = stack[:len(stack)-1] + return true + } + if len(stack) > 0 { + parents[node] = stack[len(stack)-1] + } + stack = append(stack, node) + return true + }) + return parents +} + +func isStringConcatParent(pass *analysis.Pass, parents map[ast.Node]ast.Node, node ast.Node) bool { + parent := parents[node] + if paren, ok := parent.(*ast.ParenExpr); ok { + parent = parents[paren] + } + expr, ok := parent.(*ast.BinaryExpr) + return ok && expr.Op == token.ADD && isString(pass.TypesInfo.Types[expr].Type) +} + +func isString(t types.Type) bool { + if t == nil { + return false + } + basic, ok := t.Underlying().(*types.Basic) + return ok && basic.Kind() == types.String +} + +func isFmtSprintfCall(pass *analysis.Pass, call *ast.CallExpr) bool { + selector, ok := call.Fun.(*ast.SelectorExpr) + return ok && selector.Sel.Name == "Sprintf" && objectPackagePath(pass.TypesInfo.Uses[selector.Sel]) == "fmt" +} + +func objectPackagePath(obj types.Object) string { + if obj == nil || obj.Pkg() == nil { + return "" + } + return obj.Pkg().Path() +} + +func renderNode(fset *token.FileSet, node any) (string, bool) { + var buf bytes.Buffer + if err := format.Node(&buf, fset, node); err != nil { + return "", false + } + return buf.String(), true +} + +type importNeed struct { + pkgPath string + defaultName string +} + +func (r reporter) importsFor(needs []importNeed) (map[string]string, []analysis.TextEdit, bool) { + qualifiers := make(map[string]string, len(needs)) + var missing []importNeed + used := fileNames(r.file) + for _, need := range needs { + if qualifier, ok := importedQualifier(r.file, need.pkgPath); ok { + if qualifier == "" { + return nil, nil, false + } + qualifiers[need.pkgPath] = qualifier + used[qualifier] = true + continue + } + qualifier := uniqueName(used, need.defaultName) + used[qualifier] = true + qualifiers[need.pkgPath] = qualifier + missing = append(missing, importNeed{pkgPath: need.pkgPath, defaultName: qualifier}) + } + if len(missing) == 0 { + return qualifiers, nil, true + } + edits, ok := addImportEdits(r.pass.Fset, r.file, missing) + return qualifiers, edits, ok +} + +func importedQualifier(file *ast.File, pkgPath string) (string, bool) { + for _, spec := range file.Imports { + if strings.Trim(spec.Path.Value, `"`) != pkgPath { + continue + } + if spec.Name == nil { + return path.Base(pkgPath), true + } + if spec.Name.Name == "_" || spec.Name.Name == "." { + return "", true + } + return spec.Name.Name, true + } + return "", false +} + +func fileNames(file *ast.File) map[string]bool { + used := make(map[string]bool) + ast.Inspect(file, func(node ast.Node) bool { + ident, ok := node.(*ast.Ident) + if ok { + used[ident.Name] = true + } + return true + }) + for _, spec := range file.Imports { + if spec.Name != nil { + used[spec.Name.Name] = true + continue + } + used[path.Base(strings.Trim(spec.Path.Value, `"`))] = true + } + return used +} + +func addImportEdits(fset *token.FileSet, file *ast.File, missing []importNeed) ([]analysis.TextEdit, bool) { + specs := make([]string, 0, len(missing)) + for _, need := range missing { + specs = append(specs, importSpecText(need.defaultName, path.Base(need.pkgPath), need.pkgPath)) + } + importDecl := firstImportDecl(file) + if importDecl == nil { + return []analysis.TextEdit{{ + Pos: file.Name.End(), + End: file.Name.End(), + NewText: []byte("\n\nimport (\n\t" + strings.Join(specs, "\n\t") + "\n)\n"), + }}, true + } + + if importDecl.Lparen.IsValid() { + return []analysis.TextEdit{{ + Pos: importDecl.Rparen, + End: importDecl.Rparen, + NewText: []byte("\n\t" + strings.Join(specs, "\n\t")), + }}, true + } + + if len(importDecl.Specs) != 1 { + return nil, false + } + existing, ok := renderNode(fset, importDecl.Specs[0]) + if !ok { + return nil, false + } + allSpecs := append([]string{existing}, specs...) + return []analysis.TextEdit{{ + Pos: importDecl.Pos(), + End: importDecl.End(), + NewText: []byte("import (\n\t" + strings.Join(allSpecs, "\n\t") + "\n)"), + }}, true +} + +func importSpecText(qualifier, defaultName, pkgPath string) string { + if qualifier != defaultName { + return fmt.Sprintf("%s %q", qualifier, pkgPath) + } + return fmt.Sprintf("%q", pkgPath) +} + +func firstImportDecl(file *ast.File) *ast.GenDecl { + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if ok && gen.Tok == token.IMPORT { + return gen + } + } + return nil +} + +func generated(file *ast.File) bool { + return ast.IsGenerated(file) +} + +type suppressions struct { + pass *analysis.Pass + line map[int]bool + file bool +} + +func newSuppressions(pass *analysis.Pass, file *ast.File) suppressions { + s := suppressions{ + pass: pass, + line: make(map[int]bool), + } + for _, group := range file.Comments { + for _, comment := range group.List { + kind, ok := directiveApplies(comment.Text) + if !ok { + continue + } + switch kind { + case "lint:ignore": + s.line[pass.Fset.PositionFor(comment.End(), false).Line+1] = true + case "lint:file-ignore": + s.file = true + } + } + } + return s +} + +func (s suppressions) ignored(pos token.Pos) bool { + return s.file || s.line[s.pass.Fset.PositionFor(pos, false).Line] +} + +func directiveApplies(raw string) (string, bool) { + fields := strings.Fields(commentText(raw)) + if len(fields) < 3 { + return "", false + } + kind := fields[0] + if kind != "lint:ignore" && kind != "lint:file-ignore" { + return "", false + } + for analyzer := range strings.SplitSeq(fields[1], ",") { + analyzer = strings.TrimSpace(analyzer) + if analyzer == analyzerName || analyzer == "all" || analyzer == "*" { + return kind, true + } + } + return "", false +} + +func commentText(raw string) string { + switch { + case strings.HasPrefix(raw, "//"): + return strings.TrimSpace(strings.TrimPrefix(raw, "//")) + case strings.HasPrefix(raw, "/*") && strings.HasSuffix(raw, "*/"): + raw = strings.TrimPrefix(raw, "/*") + raw = strings.TrimSuffix(raw, "*/") + return strings.TrimSpace(raw) + default: + return strings.TrimSpace(raw) + } +} diff --git a/stringbuilderlint/analyzer_test.go b/stringbuilderlint/analyzer_test.go new file mode 100644 index 0000000..c2a4a77 --- /dev/null +++ b/stringbuilderlint/analyzer_test.go @@ -0,0 +1,11 @@ +package stringbuilderlint + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), Analyzer, "a", "fileignore", "sprintf") +} diff --git a/stringbuilderlint/benchmark_test.go b/stringbuilderlint/benchmark_test.go new file mode 100644 index 0000000..1b55fdb --- /dev/null +++ b/stringbuilderlint/benchmark_test.go @@ -0,0 +1,306 @@ +package stringbuilderlint + +import ( + "fmt" + "strconv" + "strings" + "testing" +) + +var benchmarkStringSink string + +var ( + benchA = "alpha" + benchB = "bravo" + benchC = "charlie" + benchD = "delta" + benchE = "echo" + benchF = "foxtrot" + benchG = "golf" + benchH = "hotel" + benchI = "india" + benchJ = "juliet" + benchK = "kilo" + benchL = "lima" + benchM = "mike" + benchN = "november" + benchO = "oscar" + benchP = "papa" + benchQ = "quebec" + benchR = "romeo" + benchS = "sierra" + benchT = "tango" + benchU = "uniform" + benchV = "victor" + benchW = "whiskey" + benchX = "xray" + benchY = "yankee" + benchZ = "zulu" + benchAA = "alpha-alpha" + benchAB = "alpha-bravo" + benchAC = "alpha-charlie" + benchAD = "alpha-delta" + benchAE = "alpha-echo" + benchAF = "alpha-foxtrot" + benchCount = 12345 + benchEnabled = true +) + +func BenchmarkConcat3(b *testing.B) { + for b.Loop() { + benchmarkStringSink = benchA + benchB + benchC + } +} + +func BenchmarkBuilderConcat3(b *testing.B) { + for b.Loop() { + part0 := benchA + part1 := benchB + part2 := benchC + var sb strings.Builder + sb.Grow(len(part0) + len(part1) + len(part2)) + sb.WriteString(part0) + sb.WriteString(part1) + sb.WriteString(part2) + benchmarkStringSink = sb.String() + } +} + +func BenchmarkConcat4(b *testing.B) { + for b.Loop() { + benchmarkStringSink = benchA + benchB + benchC + benchD + } +} + +func BenchmarkBuilderConcat4(b *testing.B) { + for b.Loop() { + part0 := benchA + part1 := benchB + part2 := benchC + part3 := benchD + var sb strings.Builder + sb.Grow(len(part0) + len(part1) + len(part2) + len(part3)) + sb.WriteString(part0) + sb.WriteString(part1) + sb.WriteString(part2) + sb.WriteString(part3) + benchmarkStringSink = sb.String() + } +} + +func BenchmarkConcat5(b *testing.B) { + for b.Loop() { + benchmarkStringSink = benchA + benchB + benchC + benchD + benchE + } +} + +func BenchmarkBuilderConcat5(b *testing.B) { + for b.Loop() { + part0 := benchA + part1 := benchB + part2 := benchC + part3 := benchD + part4 := benchE + var sb strings.Builder + sb.Grow(len(part0) + len(part1) + len(part2) + len(part3) + len(part4)) + sb.WriteString(part0) + sb.WriteString(part1) + sb.WriteString(part2) + sb.WriteString(part3) + sb.WriteString(part4) + benchmarkStringSink = sb.String() + } +} + +func BenchmarkConcat8(b *testing.B) { + for b.Loop() { + benchmarkStringSink = benchA + benchB + benchC + benchD + benchE + benchF + benchG + benchH + } +} + +func BenchmarkBuilderConcat8(b *testing.B) { + for b.Loop() { + part0 := benchA + part1 := benchB + part2 := benchC + part3 := benchD + part4 := benchE + part5 := benchF + part6 := benchG + part7 := benchH + var sb strings.Builder + sb.Grow(len(part0) + len(part1) + len(part2) + len(part3) + len(part4) + len(part5) + len(part6) + len(part7)) + sb.WriteString(part0) + sb.WriteString(part1) + sb.WriteString(part2) + sb.WriteString(part3) + sb.WriteString(part4) + sb.WriteString(part5) + sb.WriteString(part6) + sb.WriteString(part7) + benchmarkStringSink = sb.String() + } +} + +func BenchmarkConcat16(b *testing.B) { + for b.Loop() { + benchmarkStringSink = benchA + benchB + benchC + benchD + benchE + benchF + benchG + benchH + benchI + benchJ + benchK + benchL + benchM + benchN + benchO + benchP + } +} + +func BenchmarkBuilderConcat16(b *testing.B) { + for b.Loop() { + part0 := benchA + part1 := benchB + part2 := benchC + part3 := benchD + part4 := benchE + part5 := benchF + part6 := benchG + part7 := benchH + part8 := benchI + part9 := benchJ + part10 := benchK + part11 := benchL + part12 := benchM + part13 := benchN + part14 := benchO + part15 := benchP + var sb strings.Builder + sb.Grow(len(part0) + len(part1) + len(part2) + len(part3) + len(part4) + len(part5) + len(part6) + len(part7) + len(part8) + len(part9) + len(part10) + len(part11) + len(part12) + len(part13) + len(part14) + len(part15)) + sb.WriteString(part0) + sb.WriteString(part1) + sb.WriteString(part2) + sb.WriteString(part3) + sb.WriteString(part4) + sb.WriteString(part5) + sb.WriteString(part6) + sb.WriteString(part7) + sb.WriteString(part8) + sb.WriteString(part9) + sb.WriteString(part10) + sb.WriteString(part11) + sb.WriteString(part12) + sb.WriteString(part13) + sb.WriteString(part14) + sb.WriteString(part15) + benchmarkStringSink = sb.String() + } +} + +func BenchmarkConcat32(b *testing.B) { + for b.Loop() { + benchmarkStringSink = benchA + benchB + benchC + benchD + benchE + benchF + benchG + benchH + benchI + benchJ + benchK + benchL + benchM + benchN + benchO + benchP + benchQ + benchR + benchS + benchT + benchU + benchV + benchW + benchX + benchY + benchZ + benchAA + benchAB + benchAC + benchAD + benchAE + benchAF + } +} + +func BenchmarkBuilderConcat32(b *testing.B) { + for b.Loop() { + part0 := benchA + part1 := benchB + part2 := benchC + part3 := benchD + part4 := benchE + part5 := benchF + part6 := benchG + part7 := benchH + part8 := benchI + part9 := benchJ + part10 := benchK + part11 := benchL + part12 := benchM + part13 := benchN + part14 := benchO + part15 := benchP + part16 := benchQ + part17 := benchR + part18 := benchS + part19 := benchT + part20 := benchU + part21 := benchV + part22 := benchW + part23 := benchX + part24 := benchY + part25 := benchZ + part26 := benchAA + part27 := benchAB + part28 := benchAC + part29 := benchAD + part30 := benchAE + part31 := benchAF + var sb strings.Builder + sb.Grow(len(part0) + len(part1) + len(part2) + len(part3) + len(part4) + len(part5) + len(part6) + len(part7) + len(part8) + len(part9) + len(part10) + len(part11) + len(part12) + len(part13) + len(part14) + len(part15) + len(part16) + len(part17) + len(part18) + len(part19) + len(part20) + len(part21) + len(part22) + len(part23) + len(part24) + len(part25) + len(part26) + len(part27) + len(part28) + len(part29) + len(part30) + len(part31)) + sb.WriteString(part0) + sb.WriteString(part1) + sb.WriteString(part2) + sb.WriteString(part3) + sb.WriteString(part4) + sb.WriteString(part5) + sb.WriteString(part6) + sb.WriteString(part7) + sb.WriteString(part8) + sb.WriteString(part9) + sb.WriteString(part10) + sb.WriteString(part11) + sb.WriteString(part12) + sb.WriteString(part13) + sb.WriteString(part14) + sb.WriteString(part15) + sb.WriteString(part16) + sb.WriteString(part17) + sb.WriteString(part18) + sb.WriteString(part19) + sb.WriteString(part20) + sb.WriteString(part21) + sb.WriteString(part22) + sb.WriteString(part23) + sb.WriteString(part24) + sb.WriteString(part25) + sb.WriteString(part26) + sb.WriteString(part27) + sb.WriteString(part28) + sb.WriteString(part29) + sb.WriteString(part30) + sb.WriteString(part31) + benchmarkStringSink = sb.String() + } +} + +func BenchmarkSprintfOnePrimitive(b *testing.B) { + for b.Loop() { + benchmarkStringSink = fmt.Sprintf("id=%d", benchCount) + } +} + +func BenchmarkBuilderSprintfOnePrimitive(b *testing.B) { + for b.Loop() { + part1 := strconv.FormatInt(int64(benchCount), 10) + var sb strings.Builder + sb.Grow(3 + len(part1)) + sb.WriteString("id=") + sb.WriteString(part1) + benchmarkStringSink = sb.String() + } +} + +func BenchmarkSprintfPrimitive(b *testing.B) { + for b.Loop() { + benchmarkStringSink = fmt.Sprintf("%s/%d/%t", benchA, benchCount, benchEnabled) + } +} + +func BenchmarkBuilderSprintfPrimitive(b *testing.B) { + for b.Loop() { + part0 := benchA + part2 := strconv.FormatInt(int64(benchCount), 10) + part4 := strconv.FormatBool(benchEnabled) + var sb strings.Builder + sb.Grow(len(part0) + 1 + len(part2) + 1 + len(part4)) + sb.WriteString(part0) + sb.WriteString("/") + sb.WriteString(part2) + sb.WriteString("/") + sb.WriteString(part4) + benchmarkStringSink = sb.String() + } +} diff --git a/stringbuilderlint/doc.go b/stringbuilderlint/doc.go new file mode 100644 index 0000000..129d32d --- /dev/null +++ b/stringbuilderlint/doc.go @@ -0,0 +1,3 @@ +// Package stringbuilderlint reports non-trivial string concatenation and +// primitive fmt.Sprintf calls that should use strings.Builder. +package stringbuilderlint diff --git a/stringbuilderlint/go.mod b/stringbuilderlint/go.mod new file mode 100644 index 0000000..0f65536 --- /dev/null +++ b/stringbuilderlint/go.mod @@ -0,0 +1,12 @@ +module github.com/omniaura/go-kit/stringbuilderlint + +go 1.25.5 + +toolchain go1.26.1 + +require golang.org/x/tools v0.44.0 + +require ( + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sync v0.21.0 // indirect +) diff --git a/stringbuilderlint/go.sum b/stringbuilderlint/go.sum new file mode 100644 index 0000000..fb61b4f --- /dev/null +++ b/stringbuilderlint/go.sum @@ -0,0 +1,7 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= diff --git a/stringbuilderlint/testdata/src/a/a.go b/stringbuilderlint/testdata/src/a/a.go new file mode 100644 index 0000000..641a981 --- /dev/null +++ b/stringbuilderlint/testdata/src/a/a.go @@ -0,0 +1,30 @@ +package a + +func twoParts(a, b string) string { + return a + b +} + +func threeParts(a, b, c string) string { + return a + ":" + b + ":" + c +} + +func nested(a, b, c string) string { + return (a + b) + c +} + +func fiveDynamic(a, b, c, d, e string) string { + return a + ":" + b + ":" + c + ":" + d + ":" + e +} + +func thirtyTwoDynamic(a string, b string, c string, d string, e string, f string, g string, h string, i string, j string, k string, l string, m string, n string, o string, p string, q string, r string, s string, t string, u string, v string, w string, x string, y string, z string, aa string, ab string, ac string, ad string, ae string, af string) string { + return a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + q + r + s + t + u + v + w + x + y + z + aa + ab + ac + ad + ae + af // want "string concatenation with 32 dynamic parts should use strings.Builder" +} + +func constantOnly() string { + return "a" + "b" + "c" +} + +func ignored(a, b, c string) string { + //lint:ignore stringbuilderlint fixture verifies line suppression + return a + b + c +} diff --git a/stringbuilderlint/testdata/src/a/a.go.golden b/stringbuilderlint/testdata/src/a/a.go.golden new file mode 100644 index 0000000..9cb0623 --- /dev/null +++ b/stringbuilderlint/testdata/src/a/a.go.golden @@ -0,0 +1,102 @@ +package a + +import ( + "strings" +) + +func twoParts(a, b string) string { + return a + b +} + +func threeParts(a, b, c string) string { + return a + ":" + b + ":" + c +} + +func nested(a, b, c string) string { + return (a + b) + c +} + +func fiveDynamic(a, b, c, d, e string) string { + return a + ":" + b + ":" + c + ":" + d + ":" + e +} + +func thirtyTwoDynamic(a string, b string, c string, d string, e string, f string, g string, h string, i string, j string, k string, l string, m string, n string, o string, p string, q string, r string, s string, t string, u string, v string, w string, x string, y string, z string, aa string, ab string, ac string, ad string, ae string, af string) string { + return func() string { + builderPart0 := a + builderPart1 := b + builderPart2 := c + builderPart3 := d + builderPart4 := e + builderPart5 := f + builderPart6 := g + builderPart7 := h + builderPart8 := i + builderPart9 := j + builderPart10 := k + builderPart11 := l + builderPart12 := m + builderPart13 := n + builderPart14 := o + builderPart15 := p + builderPart16 := q + builderPart17 := r + builderPart18 := s + builderPart19 := t + builderPart20 := u + builderPart21 := v + builderPart22 := w + builderPart23 := x + builderPart24 := y + builderPart25 := z + builderPart26 := aa + builderPart27 := ab + builderPart28 := ac + builderPart29 := ad + builderPart30 := ae + builderPart31 := af + var sb strings.Builder + sb.Grow(len(builderPart0) + len(builderPart1) + len(builderPart2) + len(builderPart3) + len(builderPart4) + len(builderPart5) + len(builderPart6) + len(builderPart7) + len(builderPart8) + len(builderPart9) + len(builderPart10) + len(builderPart11) + len(builderPart12) + len(builderPart13) + len(builderPart14) + len(builderPart15) + len(builderPart16) + len(builderPart17) + len(builderPart18) + len(builderPart19) + len(builderPart20) + len(builderPart21) + len(builderPart22) + len(builderPart23) + len(builderPart24) + len(builderPart25) + len(builderPart26) + len(builderPart27) + len(builderPart28) + len(builderPart29) + len(builderPart30) + len(builderPart31)) + sb.WriteString(builderPart0) + sb.WriteString(builderPart1) + sb.WriteString(builderPart2) + sb.WriteString(builderPart3) + sb.WriteString(builderPart4) + sb.WriteString(builderPart5) + sb.WriteString(builderPart6) + sb.WriteString(builderPart7) + sb.WriteString(builderPart8) + sb.WriteString(builderPart9) + sb.WriteString(builderPart10) + sb.WriteString(builderPart11) + sb.WriteString(builderPart12) + sb.WriteString(builderPart13) + sb.WriteString(builderPart14) + sb.WriteString(builderPart15) + sb.WriteString(builderPart16) + sb.WriteString(builderPart17) + sb.WriteString(builderPart18) + sb.WriteString(builderPart19) + sb.WriteString(builderPart20) + sb.WriteString(builderPart21) + sb.WriteString(builderPart22) + sb.WriteString(builderPart23) + sb.WriteString(builderPart24) + sb.WriteString(builderPart25) + sb.WriteString(builderPart26) + sb.WriteString(builderPart27) + sb.WriteString(builderPart28) + sb.WriteString(builderPart29) + sb.WriteString(builderPart30) + sb.WriteString(builderPart31) + return sb.String() + }() // want "string concatenation with 32 dynamic parts should use strings.Builder" +} + +func constantOnly() string { + return "a" + "b" + "c" +} + +func ignored(a, b, c string) string { + //lint:ignore stringbuilderlint fixture verifies line suppression + return a + b + c +} diff --git a/stringbuilderlint/testdata/src/fileignore/fileignore.go b/stringbuilderlint/testdata/src/fileignore/fileignore.go new file mode 100644 index 0000000..86452c9 --- /dev/null +++ b/stringbuilderlint/testdata/src/fileignore/fileignore.go @@ -0,0 +1,6 @@ +//lint:file-ignore stringbuilderlint fixture verifies file suppression +package fileignore + +func ignoredFile(a, b, c string) string { + return a + b + c +} diff --git a/stringbuilderlint/testdata/src/sprintf/sprintf.go b/stringbuilderlint/testdata/src/sprintf/sprintf.go new file mode 100644 index 0000000..07b1592 --- /dev/null +++ b/stringbuilderlint/testdata/src/sprintf/sprintf.go @@ -0,0 +1,31 @@ +package sprintf + +import "fmt" + +type payload struct { + Name string +} + +func primitive(name string, count int, enabled bool) string { + return fmt.Sprintf("%s/%d/%t", name, count, enabled) // want "fmt.Sprintf with primitive arguments should use strings.Builder and strconv" +} + +func floatPrimitive(name string, ratio float64) string { + return fmt.Sprintf("%s=%.2f", name, ratio) // want "fmt.Sprintf with primitive arguments should use strings.Builder and strconv" +} + +func onePrimitive(count int) string { + return fmt.Sprintf("id=%d", count) // want "fmt.Sprintf with primitive arguments should use strings.Builder and strconv" +} + +func barePrimitive(count int) string { + return fmt.Sprintf("%d", count) +} + +func unsupportedStruct(value payload) string { + return fmt.Sprintf("%v", value) +} + +func unsupportedWidth(name string) string { + return fmt.Sprintf("%10s", name) +} diff --git a/stringbuilderlint/testdata/src/sprintf/sprintf.go.golden b/stringbuilderlint/testdata/src/sprintf/sprintf.go.golden new file mode 100644 index 0000000..fede2a3 --- /dev/null +++ b/stringbuilderlint/testdata/src/sprintf/sprintf.go.golden @@ -0,0 +1,63 @@ +package sprintf + +import ( + "fmt" + "strconv" + "strings" +) + +type payload struct { + Name string +} + +func primitive(name string, count int, enabled bool) string { + return func() string { + builderPart0 := name + builderPart2 := strconv.FormatInt(int64(count), 10) + builderPart4 := strconv.FormatBool(enabled) + var sb strings.Builder + sb.Grow(len(builderPart0) + 1 + len(builderPart2) + 1 + len(builderPart4)) + sb.WriteString(builderPart0) + sb.WriteString("/") + sb.WriteString(builderPart2) + sb.WriteString("/") + sb.WriteString(builderPart4) + return sb.String() + }() // want "fmt.Sprintf with primitive arguments should use strings.Builder and strconv" +} + +func floatPrimitive(name string, ratio float64) string { + return func() string { + builderPart0 := name + builderPart2 := strconv.FormatFloat(float64(ratio), 'f', 2, 64) + var sb strings.Builder + sb.Grow(len(builderPart0) + 1 + len(builderPart2)) + sb.WriteString(builderPart0) + sb.WriteString("=") + sb.WriteString(builderPart2) + return sb.String() + }() // want "fmt.Sprintf with primitive arguments should use strings.Builder and strconv" +} + +func onePrimitive(count int) string { + return func() string { + builderPart1 := strconv.FormatInt(int64(count), 10) + var sb strings.Builder + sb.Grow(3 + len(builderPart1)) + sb.WriteString("id=") + sb.WriteString(builderPart1) + return sb.String() + }() // want "fmt.Sprintf with primitive arguments should use strings.Builder and strconv" +} + +func barePrimitive(count int) string { + return fmt.Sprintf("%d", count) +} + +func unsupportedStruct(value payload) string { + return fmt.Sprintf("%v", value) +} + +func unsupportedWidth(name string) string { + return fmt.Sprintf("%10s", name) +}