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 return first(strconv.Unquote(spec.Path.Value)) == path
148 })
149 }
150 clonepkg := cond(
151 types.Identical(info.TypeOf(call), byteSliceType) &&
152 !fileImports("slices") && fileImports("bytes"),
153 "bytes",
154 "slices")
155
156
157
158
159
160 prefix, importEdits := refactor.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos())
161 message := fmt.Sprintf("Replace append with %s.Clone", clonepkg)
162 pass.Report(analysis.Diagnostic{
163 Pos: call.Pos(),
164 End: call.End(),
165 Message: message,
166 SuggestedFixes: []analysis.SuggestedFix{{
167 Message: message,
168 TextEdits: append(importEdits, []analysis.TextEdit{{
169 Pos: call.Pos(),
170 End: call.End(),
171 NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, astutil.Format(pass.Fset, s)),
172 }}...),
173 }},
174 })
175 return
176 }
177
178
179
180
181 prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", "Concat", call.Pos())
182 pass.Report(analysis.Diagnostic{
183 Pos: call.Pos(),
184 End: call.End(),
185 Message: "Replace append with slices.Concat",
186 SuggestedFixes: []analysis.SuggestedFix{{
187 Message: "Replace append with slices.Concat",
188 TextEdits: append(importEdits, []analysis.TextEdit{{
189 Pos: call.Pos(),
190 End: call.End(),
191 NewText: fmt.Appendf(nil, "%sConcat(%s)", prefix, formatExprs(pass.Fset, sliceArgs)),
192 }}...),
193 }},
194 })
195 }
196
197
198 skip := make(map[*ast.CallExpr]bool)
199
200
201 for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
202 file := curFile.Node().(*ast.File)
203
204 for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
205 call := curCall.Node().(*ast.CallExpr)
206 if skip[call] {
207 continue
208 }
209
210
211
212
213 base, slices := ast.Expr(call), []ast.Expr(nil)
214 again:
215 if call, ok := base.(*ast.CallExpr); ok {
216 if id, ok := call.Fun.(*ast.Ident); ok &&
217 call.Ellipsis.IsValid() &&
218 len(call.Args) == 2 &&
219 info.Uses[id] == builtinAppend {
220
221
222 base, slices = call.Args[0], append(slices, call.Args[1])
223 skip[call] = true
224 goto again
225 }
226 }
227
228 if len(slices) > 0 {
229 simplifyAppendEllipsis(file, call, base, slices)
230 }
231 }
232 }
233 return nil, nil
234 }
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255 func clippedSlice(info *types.Info, e ast.Expr) (res ast.Expr, empty bool) {
256 switch e := e.(type) {
257 case *ast.SliceExpr:
258
259 if e.Slice3 && e.High != nil && e.Max != nil && astutil.EqualSyntax(e.High, e.Max) {
260 res = e
261 empty = isZeroIntConst(info, e.High)
262 if call, ok := e.High.(*ast.CallExpr); ok &&
263 typeutil.Callee(info, call) == builtinLen &&
264 astutil.EqualSyntax(call.Args[0], e.X) {
265 res = e.X
266 }
267 return
268 }
269 return
270
271 case *ast.CallExpr:
272
273 if info.Types[e.Fun].IsType() &&
274 is[*ast.Ident](e.Args[0]) &&
275 info.Uses[e.Args[0].(*ast.Ident)] == builtinNil {
276 return e, true
277 }
278
279
280 obj := typeutil.Callee(info, e)
281 if typesinternal.IsFunctionNamed(obj, "slices", "Clip") {
282 return e.Args[0], false
283 }
284
285 case *ast.CompositeLit:
286
287 if len(e.Elts) == 0 {
288 return e, true
289 }
290 }
291 return nil, false
292 }
293
View as plain text