1
2
3
4
5 package modernize
6
7 import (
8 "fmt"
9 "go/ast"
10 "go/constant"
11 "go/types"
12 "strings"
13
14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 "golang.org/x/tools/go/ast/edge"
17 "golang.org/x/tools/internal/analysis/analyzerutil"
18 typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
19 "golang.org/x/tools/internal/astutil"
20 "golang.org/x/tools/internal/fmtstr"
21 "golang.org/x/tools/internal/typesinternal/typeindex"
22 "golang.org/x/tools/internal/versions"
23 )
24
25 var FmtAppendfAnalyzer = &analysis.Analyzer{
26 Name: "fmtappendf",
27 Doc: analyzerutil.MustExtractDoc(doc, "fmtappendf"),
28 Requires: []*analysis.Analyzer{
29 inspect.Analyzer,
30 typeindexanalyzer.Analyzer,
31 },
32 Run: fmtappendf,
33 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#fmtappendf",
34 }
35
36
37
38 func fmtappendf(pass *analysis.Pass) (any, error) {
39 index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
40 for _, fn := range []types.Object{
41 index.Object("fmt", "Sprintf"),
42 index.Object("fmt", "Sprintln"),
43 index.Object("fmt", "Sprint"),
44 } {
45 for curCall := range index.Calls(fn) {
46 call := curCall.Node().(*ast.CallExpr)
47 if ek, idx := curCall.ParentEdge(); ek == edge.CallExpr_Args && idx == 0 {
48
49 conv := curCall.Parent().Node().(*ast.CallExpr)
50 info := pass.TypesInfo
51 tv := info.Types[conv.Fun]
52 if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
53
54 if len(call.Args) == 0 {
55 continue
56 }
57
58
59
60
61
62 if fn.Name() == "Sprint" || fn.Name() == "Sprintf" {
63 format := info.Types[call.Args[0]].Value
64 if format != nil && mayFormatEmpty(constant.StringVal(format)) {
65 continue
66 }
67 }
68
69
70 var id *ast.Ident
71 switch e := ast.Unparen(call.Fun).(type) {
72 case *ast.SelectorExpr:
73 id = e.Sel
74 case *ast.Ident:
75 id = e
76 }
77
78 old, new := fn.Name(), strings.Replace(fn.Name(), "Sprint", "Append", 1)
79 edits := []analysis.TextEdit{
80 {
81
82 Pos: conv.Pos(),
83 End: conv.Args[0].Pos(),
84 },
85 {
86
87
88
89
90
91
92 Pos: conv.Args[0].End(),
93 End: conv.Rparen + 1,
94 },
95 {
96 Pos: id.Pos(),
97 End: id.End(),
98 NewText: []byte(new),
99 },
100 {
101 Pos: call.Lparen + 1,
102 NewText: []byte("nil, "),
103 },
104 }
105 if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_19) {
106 continue
107 }
108 pass.Report(analysis.Diagnostic{
109 Pos: conv.Pos(),
110 End: conv.End(),
111 Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
112 SuggestedFixes: []analysis.SuggestedFix{{
113 Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
114 TextEdits: edits,
115 }},
116 })
117 }
118 }
119 }
120 }
121 return nil, nil
122 }
123
124
125
126
127
128
129
130
131 func mayFormatEmpty(formatStr string) bool {
132 if formatStr == "" {
133 return true
134 }
135 operations, err := fmtstr.Parse(formatStr, 0)
136 if err != nil {
137
138
139
140
141 return false
142 }
143 totalOpsLen := 0
144 for _, op := range operations {
145 totalOpsLen += len(op.Text)
146 if !strings.ContainsRune("svxX", rune(op.Verb.Verb)) && op.Prec.Fixed != 0 {
147
148
149 return false
150 }
151 }
152
153
154 if totalOpsLen != len(formatStr) {
155 return false
156 }
157
158
159
160
161 return true
162 }
163
View as plain text