...
1
2
3
4
5
6
7 package composite
8
9 import (
10 "fmt"
11 "go/ast"
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/inspector"
19 "golang.org/x/tools/internal/typeparams"
20 )
21
22 const Doc = `check for unkeyed composite literals
23
24 This analyzer reports a diagnostic for composite literals of struct
25 types imported from another package that do not use the field-keyed
26 syntax. Such literals are fragile because the addition of a new field
27 (even if unexported) to the struct will cause compilation to fail.
28
29 As an example,
30
31 err = &net.DNSConfigError{err}
32
33 should be replaced by:
34
35 err = &net.DNSConfigError{Err: err}
36 `
37
38 var Analyzer = &analysis.Analyzer{
39 Name: "composites",
40 Doc: Doc,
41 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/composite",
42 Requires: []*analysis.Analyzer{inspect.Analyzer},
43 RunDespiteErrors: true,
44 Run: run,
45 }
46
47 var whitelist = true
48
49 func init() {
50 Analyzer.Flags.BoolVar(&whitelist, "whitelist", whitelist, "use composite white list; for testing only")
51 }
52
53 func run(pass *analysis.Pass) (any, error) {
54 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
55
56 for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
57 complit := curLit.Node().(*ast.CompositeLit)
58
59
60 if len(complit.Elts) == 0 ||
61 slices.ContainsFunc(complit.Elts, func(e ast.Expr) bool { return is[*ast.KeyValueExpr](e) }) {
62 continue
63 }
64
65
66
67 typ := pass.TypesInfo.Types[complit].Type
68 if typ == nil {
69 continue
70 }
71 terms, err := typeparams.NormalTerms(typ)
72 if err != nil || len(terms) == 0 {
73 continue
74 }
75 t := terms[0].Type()
76 strct, ok := typeparams.Deref(t).Underlying().(*types.Struct)
77 if !ok {
78 continue
79 }
80 if isSamePackageType(pass, t) {
81 continue
82 }
83
84
85 typeName := typ.String()
86 if whitelist && unkeyedLiteral[typeName] {
87 continue
88 }
89
90
91
92 var fixes []analysis.SuggestedFix
93 if len(complit.Elts) == strct.NumFields() {
94 var edits []analysis.TextEdit
95 for i, elt := range complit.Elts {
96 field := strct.Field(i)
97
98
99 if !field.Exported() {
100 edits = nil
101 break
102 }
103 edits = append(edits, analysis.TextEdit{
104 Pos: elt.Pos(),
105 End: elt.Pos(),
106 NewText: fmt.Appendf(nil, "%s: ", field.Name()),
107 })
108 }
109 if edits != nil {
110 fixes = []analysis.SuggestedFix{{
111 Message: "Add field names to struct literal",
112 TextEdits: edits,
113 }}
114 }
115 }
116
117 pass.Report(analysis.Diagnostic{
118 Pos: complit.Pos(),
119 End: complit.End(),
120 Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName),
121 SuggestedFixes: fixes,
122 })
123 }
124 return nil, nil
125 }
126
127
128 func isSamePackageType(pass *analysis.Pass, typ types.Type) bool {
129 switch x := types.Unalias(typ).(type) {
130 case *types.Struct:
131
132 return true
133 case *types.Pointer:
134 return isSamePackageType(pass, x.Elem())
135 case interface{ Obj() *types.TypeName }:
136
137 return x.Obj().Pkg() != nil &&
138 strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test")
139 }
140 return false
141 }
142
143 func is[T any](x any) bool {
144 _, ok := x.(T)
145 return ok
146 }
147
View as plain text