1
2
3
4
5 package modernize
6
7 import (
8 "bytes"
9 "fmt"
10 "go/ast"
11 "go/token"
12 "go/types"
13 "slices"
14 "strings"
15
16 "golang.org/x/tools/go/analysis"
17 "golang.org/x/tools/go/analysis/passes/inspect"
18 "golang.org/x/tools/go/ast/edge"
19 "golang.org/x/tools/go/ast/inspector"
20 "golang.org/x/tools/internal/analysis/analyzerutil"
21 typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
22 "golang.org/x/tools/internal/astutil"
23 "golang.org/x/tools/internal/moreiters"
24 "golang.org/x/tools/internal/typesinternal/typeindex"
25 "golang.org/x/tools/internal/versions"
26 )
27
28 var EmbedLitAnalyzer = &analysis.Analyzer{
29 Name: "embedlit",
30 Doc: analyzerutil.MustExtractDoc(doc, "embedlit"),
31 Requires: []*analysis.Analyzer{
32 inspect.Analyzer,
33 typeindexanalyzer.Analyzer,
34 },
35 Run: runEmbedLit,
36 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_embedlit",
37 }
38
39
40
41
42
43
44 func runEmbedLit(pass *analysis.Pass) (any, error) {
45 var (
46 inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
47 index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
48 info = pass.TypesInfo
49 )
50 for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
51 if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value {
52
53
54
55
56 if !embedlitUnnest(pass, info, curLit) {
57 err := embedlitCombine(pass, index, info, curLit)
58 if err != nil {
59 return nil, err
60 }
61 }
62 }
63 }
64 return nil, nil
65 }
66
67
68
69
70
71 func embedlitUnnest(pass *analysis.Pass, info *types.Info, curLit inspector.Cursor) bool {
72 var (
73 edits []analysis.TextEdit
74 names []string
75 lit = curLit.Node().(*ast.CompositeLit)
76 compLitType = info.TypeOf(lit)
77 )
78
79
80
81 var checkLit func(lit *ast.CompositeLit)
82 checkLit = func(lit *ast.CompositeLit) {
83 for i, elt := range lit.Elts {
84
85 if kv, ok := elt.(*ast.KeyValueExpr); ok {
86 if innerLit := isEmbeddedFieldLit(info, compLitType, kv); innerLit != nil {
87
88
89
90
91
92 closingPos := innerLit.Elts[len(innerLit.Elts)-1].End()
93 file := astutil.EnclosingFile(curLit)
94
95 if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
96 return
97 }
98
99 if !moreiters.Empty(astutil.Comments(file, kv.Pos(), innerLit.Lbrace+1)) ||
100 !moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) {
101 continue
102 }
103
104
105
106
107 startPos := kv.Pos()
108 endPos := innerLit.Lbrace + 1
109
110
111
112
113
114
115
116
117
118
119 tokFile := pass.Fset.File(kv.Pos())
120 lineOf := func(pos token.Pos) int {
121 return tokFile.PositionFor(pos, false).Line
122 }
123 curLine := lineOf(kv.Pos())
124 var prevLine int
125 if i == 0 {
126
127 prevLine = lineOf(lit.Lbrace)
128 } else {
129 prevLine = lineOf(lit.Elts[i-1].End())
130 }
131
132
133
134
135
136 if prevLine < curLine && curLine < tokFile.LineCount() &&
137 lineOf(innerLit.Elts[0].Pos()) > curLine {
138 lineStart := tokFile.LineStart(curLine)
139 nextLineStart := tokFile.LineStart(curLine + 1)
140
141 if moreiters.Empty(astutil.Comments(file, lineStart, nextLineStart)) {
142 startPos = tokFile.LineStart(curLine)
143 endPos = nextLineStart
144 }
145 }
146
147 edits = append(edits, []analysis.TextEdit{
148
149
150 {
151
152 Pos: startPos,
153 End: endPos,
154 },
155 {
156
157
158
159 Pos: closingPos,
160 End: innerLit.Rbrace + 1,
161 },
162 }...)
163 names = append(names, kv.Key.(*ast.Ident).Name)
164 checkLit(innerLit)
165 }
166 }
167 }
168 }
169 checkLit(lit)
170 if len(edits) > 0 {
171 pass.Report(analysis.Diagnostic{
172 Pos: curLit.Node().Pos(),
173 End: curLit.Node().End(),
174 Message: "embedded field type can be removed from struct literal",
175 SuggestedFixes: []analysis.SuggestedFix{
176 {
177 Message: fmt.Sprintf("Remove embedded field type%s %s", cond(len(names) == 1, "", "s"), strings.Join(names, ", ")),
178 TextEdits: edits,
179 },
180 },
181 })
182 return true
183 }
184 return false
185 }
186
187
188
189
190
191 func embedlitCombine(pass *analysis.Pass, index *typeindex.Index, info *types.Info, curLit inspector.Cursor) error {
192 compLit := curLit.Node().(*ast.CompositeLit)
193 if !moreiters.Every(slices.Values(compLit.Elts), func(e ast.Expr) bool {
194 return is[*ast.KeyValueExpr](e)
195 }) {
196
197
198 return nil
199 }
200 var (
201
202 lhs *ast.Ident
203
204
205
206 curStmt inspector.Cursor
207 )
208 switch curLit.ParentEdgeKind() {
209 case edge.AssignStmt_Rhs:
210 assign := curLit.Parent().Node().(*ast.AssignStmt)
211
212
213 if len(assign.Lhs) != 1 {
214 return nil
215 }
216 if id, ok := assign.Lhs[0].(*ast.Ident); ok {
217 lhs = id
218 curStmt = curLit.Parent()
219 }
220 case edge.ValueSpec_Values:
221 spec := curLit.Parent().Node().(*ast.ValueSpec)
222
223 if len(spec.Names) != 1 {
224 return nil
225 }
226 lhs = spec.Names[0]
227 if decl, ok := moreiters.First(curLit.Enclosing((*ast.DeclStmt)(nil))); ok {
228 if gdecl, ok := decl.Node().(*ast.DeclStmt).Decl.(*ast.GenDecl); ok && len(gdecl.Specs) == 1 {
229 curStmt = decl
230 }
231 }
232 default:
233 return nil
234 }
235
236 if lhs == nil || !curStmt.Valid() {
237 return nil
238 }
239
240 var (
241 tObj = info.ObjectOf(lhs)
242
243
244 firstStmt, lastStmt inspector.Cursor
245 hasEmbeddedSelection bool
246 )
247 stmtloop:
248 for {
249 var ok bool
250 curStmt, ok = curStmt.NextSibling()
251 if !ok {
252 break
253 }
254
255
256 assign, ok := curStmt.Node().(*ast.AssignStmt)
257 if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) {
258
259 break
260 }
261 expr := assign.Lhs[0]
262 sel, ok := expr.(*ast.SelectorExpr)
263 if !ok {
264 break
265 }
266
267 selXId, ok := sel.X.(*ast.Ident)
268 if !ok {
269
270 break
271 }
272 obj := info.ObjectOf(selXId)
273 if obj != tObj {
274 break
275 }
276
277
278
279 seln := info.Selections[sel]
280 if v, ok := seln.Obj().(*types.Var); ok && v.Embedded() ||
281 len(seln.Index()) > 1 {
282 hasEmbeddedSelection = true
283 }
284
285 rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0)
286 if uses(index, rhsCur, tObj) {
287 break
288 }
289 for c := range rhsCur.Preorder((*ast.Ident)(nil)) {
290 id := c.Node().(*ast.Ident)
291
292
293 if info.ObjectOf(id) == tObj {
294 break stmtloop
295 }
296
297
298
299
300 }
301 if !firstStmt.Valid() {
302 firstStmt = curStmt
303 }
304 lastStmt = curStmt
305 }
306
307 if !firstStmt.Valid() || !hasEmbeddedSelection {
308
309 return nil
310 }
311
312 file := astutil.EnclosingFile(curLit)
313
314 if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
315 return nil
316 }
317
318
319
320 tokFile := pass.Fset.File(compLit.Rbrace)
321 filename := tokFile.Name()
322 src, err := pass.ReadFile(filename)
323 if err != nil {
324 return err
325 }
326
327 hasTrailingComma := false
328 if len(compLit.Elts) > 0 {
329 lastElt := compLit.Elts[len(compLit.Elts)-1]
330 lastEltOffset := tokFile.Offset(lastElt.End())
331 rbraceOffset := tokFile.Offset(compLit.Rbrace)
332 hasTrailingComma = bytes.Contains(src[lastEltOffset:rbraceOffset], []byte(","))
333 }
334 var edits []analysis.TextEdit
335
336
337
338
339
340
341
342
343
344
345
346 if len(compLit.Elts) > 0 && !hasTrailingComma {
347 edits = append(edits, analysis.TextEdit{
348 Pos: compLit.Rbrace,
349 End: compLit.Rbrace + 1,
350 NewText: []byte(","),
351 })
352 } else {
353 edits = append(edits, analysis.TextEdit{
354 Pos: compLit.Rbrace,
355 End: compLit.Rbrace + 1,
356 })
357 }
358
359
360
361
362
363 curStmt = firstStmt
364 var prevStmt inspector.Cursor
365 for {
366 assign := curStmt.Node().(*ast.AssignStmt)
367 expr := assign.Lhs[0]
368 sel := expr.(*ast.SelectorExpr)
369
370 edits = append(edits, analysis.TextEdit{
371 Pos: assign.Pos(),
372 End: sel.Sel.Pos(),
373 })
374
375 edits = append(edits, analysis.TextEdit{
376 Pos: expr.End(),
377 End: assign.TokPos + 1,
378 NewText: []byte(":"),
379 })
380
381
382 if prevStmt.Valid() {
383 edits = append(edits, analysis.TextEdit{
384 Pos: prevStmt.Node().End(),
385 NewText: []byte(","),
386 })
387 }
388
389
390 if curStmt == lastStmt {
391 edits = append(edits, analysis.TextEdit{
392 Pos: assign.End(),
393 NewText: []byte("}"),
394 })
395 break
396 }
397 prevStmt = curStmt
398 curStmt, _ = curStmt.NextSibling()
399 }
400
401 pass.Report(analysis.Diagnostic{
402 Pos: curLit.Node().Pos(),
403 End: curLit.Node().End(),
404 Message: "embedded field assignment can be moved to struct literal",
405 SuggestedFixes: []analysis.SuggestedFix{
406 {
407 Message: "Move embedded field assignment to struct literal",
408 TextEdits: edits,
409 },
410 },
411 })
412 return nil
413 }
414
415
416
417
418
419 func isEmbeddedFieldLit(info *types.Info, topLevelType types.Type, kv *ast.KeyValueExpr) *ast.CompositeLit {
420 obj := keyedField(info, kv)
421 if obj == nil || !obj.Embedded() {
422 return nil
423 }
424 lit, ok := kv.Value.(*ast.CompositeLit)
425 if !ok || len(lit.Elts) == 0 {
426
427 return nil
428 }
429
430
431
432 for _, elt := range lit.Elts {
433 kv, ok := elt.(*ast.KeyValueExpr)
434 if !ok {
435 return nil
436 }
437 obj := keyedField(info, kv)
438 if obj == nil {
439 return nil
440 }
441 k := kv.Key.(*ast.Ident)
442
443
444
445
446
447
448
449 parentObj, _, _ := types.LookupFieldOrMethod(topLevelType, true, obj.Pkg(), k.Name)
450 if parentObj != obj {
451 return nil
452 }
453 }
454 return lit
455 }
456
457
458
459 func keyedField(info *types.Info, kv *ast.KeyValueExpr) *types.Var {
460 k, ok := kv.Key.(*ast.Ident)
461 if !ok {
462 return nil
463 }
464 obj, ok := info.ObjectOf(k).(*types.Var)
465 if !ok || !obj.IsField() {
466 return nil
467 }
468 return obj
469 }
470
View as plain text