1
2
3
4
5 package modernize
6
7 import (
8 "fmt"
9 "go/ast"
10 "go/types"
11 "slices"
12 "strconv"
13
14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 "golang.org/x/tools/go/types/typeutil"
17 "golang.org/x/tools/internal/analysis/analyzerutil"
18 "golang.org/x/tools/internal/astutil"
19 "golang.org/x/tools/internal/refactor"
20 "golang.org/x/tools/internal/typesinternal"
21 "golang.org/x/tools/internal/versions"
22 )
23
24
25 var AppendClippedAnalyzer = &analysis.Analyzer{
26 Name: "appendclipped",
27 Doc: analyzerutil.MustExtractDoc(doc, "appendclipped"),
28 Requires: []*analysis.Analyzer{inspect.Analyzer},
29 Run: appendclipped,
30 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped",
31 }
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 func appendclipped(pass *analysis.Pass) (any, error) {
58
59
60 if within(pass, "slices", "bytes", "runtime") {
61 return nil, nil
62 }
63
64 info := pass.TypesInfo
65
66
67 simplifyAppendEllipsis := func(file *ast.File, call *ast.CallExpr, base ast.Expr, sliceArgs []ast.Expr) {
68
69
70
71 clipped, empty := clippedSlice(info, base)
72 if clipped == nil {
73 return
74 }
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94 baseType := info.TypeOf(base)
95 for _, arg := range sliceArgs {
96 if !types.Identical(info.TypeOf(arg), baseType) {
97 return
98 }
99 }
100
101
102
103
104
105
106
107
108 if !empty {
109 sliceArgs = append(sliceArgs, clipped)
110 }
111 slices.Reverse(sliceArgs)
112
113
114
115
116 if len(sliceArgs) == 1 {
117 s := sliceArgs[0]
118
119
120
121 if scall, ok := s.(*ast.CallExpr); ok {
122 obj := typeutil.Callee(info, scall)
123 if typesinternal.IsFunctionNamed(obj, "os", "Environ") {
124 pass.Report(analysis.Diagnostic{
125 Pos: call.Pos(),
126 End: call.End(),
127 Message: "Redundant clone of os.Environ()",
128 SuggestedFixes: []analysis.SuggestedFix{{
129 Message: "Eliminate redundant clone",
130 TextEdits: []analysis.TextEdit{{
131 Pos: call.Pos(),
132 End: call.End(),
133 NewText: []byte(astutil.Format(pass.Fset, s)),
134 }},
135 }},
136 })
137 return
138 }
139 }
140
141
142
143
144
145 fileImports := func(path string) bool {
146 return slices.ContainsFunc(file.Imports, func(spec *ast.ImportSpec) bool {
147 value, _ := strconv.Unquote(spec.Path.Value)
148 return value == path
149 })
150 }
151 clonepkg := cond(
152 types.Identical(info.TypeOf(call), byteSliceType) &&
153 !fileImports("slices") && fileImports("bytes"),
154 "bytes",
155 "slices")
156
157
158
159
160
161 prefix, importEdits := refactor.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos())
162 message := fmt.Sprintf("Replace append with %s.Clone", clonepkg)
163 pass.Report(analysis.Diagnostic{
164 Pos: call.Pos(),
165 End: call.End(),
166 Message: message,
167 SuggestedFixes: []analysis.SuggestedFix{{
168 Message: message,
169 TextEdits: append(importEdits, []analysis.TextEdit{{
170 Pos: call.Pos(),
171 End: call.End(),
172 NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, astutil.Format(pass.Fset, s)),
173 }}...),
174 }},
175 })
176 return
177 }
178
179
180
181
182 prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", "Concat", call.Pos())
183 pass.Report(analysis.Diagnostic{
184 Pos: call.Pos(),
185 End: call.End(),
186 Message: "Replace append with slices.Concat",
187 SuggestedFixes: []analysis.SuggestedFix{{
188 Message: "Replace append with slices.Concat",
189 TextEdits: append(importEdits, []analysis.TextEdit{{
190 Pos: call.Pos(),
191 End: call.End(),
192 NewText: fmt.Appendf(nil, "%sConcat(%s)", prefix, formatExprs(pass.Fset, sliceArgs)),
193 }}...),
194 }},
195 })
196 }
197
198
199 skip := make(map[*ast.CallExpr]bool)
200
201
202 for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
203 file := curFile.Node().(*ast.File)
204
205 for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
206 call := curCall.Node().(*ast.CallExpr)
207 if skip[call] {
208 continue
209 }
210
211
212
213
214 base, slices := ast.Expr(call), []ast.Expr(nil)
215 again:
216 if call, ok := base.(*ast.CallExpr); ok {
217 if id, ok := call.Fun.(*ast.Ident); ok &&
218 call.Ellipsis.IsValid() &&
219 len(call.Args) == 2 &&
220 info.Uses[id] == builtinAppend {
221
222
223 base, slices = call.Args[0], append(slices, call.Args[1])
224 skip[call] = true
225 goto again
226 }
227 }
228
229 if len(slices) > 0 {
230 simplifyAppendEllipsis(file, call, base, slices)
231 }
232 }
233 }
234 return nil, nil
235 }
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256 func clippedSlice(info *types.Info, e ast.Expr) (res ast.Expr, empty bool) {
257 switch e := e.(type) {
258 case *ast.SliceExpr:
259
260 if e.Slice3 && e.High != nil && e.Max != nil && astutil.EqualSyntax(e.High, e.Max) {
261 res = e
262 empty = isZeroIntConst(info, e.High)
263 if call, ok := e.High.(*ast.CallExpr); ok &&
264 typeutil.Callee(info, call) == builtinLen &&
265 astutil.EqualSyntax(call.Args[0], e.X) {
266 res = e.X
267 }
268 return
269 }
270 return
271
272 case *ast.CallExpr:
273
274 if info.Types[e.Fun].IsType() &&
275 is[*ast.Ident](e.Args[0]) &&
276 info.Uses[e.Args[0].(*ast.Ident)] == builtinNil {
277 return e, true
278 }
279
280
281 obj := typeutil.Callee(info, e)
282 if typesinternal.IsFunctionNamed(obj, "slices", "Clip") {
283 return e.Args[0], false
284 }
285
286 case *ast.CompositeLit:
287
288 if len(e.Elts) == 0 {
289 return e, true
290 }
291 }
292 return nil, false
293 }
294
View as plain text