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