...
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 Requires: []*analysis.Analyzer{inspect.Analyzer},
41 RunDespiteErrors: true,
42 Run: run,
43 }
44
45 var whitelist = true
46
47 func init() {
48 Analyzer.Flags.BoolVar(&whitelist, "whitelist", whitelist, "use composite white list; for testing only")
49 }
50
51
52
53 func run(pass *analysis.Pass) (interface{}, error) {
54 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
55
56 nodeFilter := []ast.Node{
57 (*ast.CompositeLit)(nil),
58 }
59 inspect.Preorder(nodeFilter, func(n ast.Node) {
60 cl := n.(*ast.CompositeLit)
61
62 typ := pass.TypesInfo.Types[cl].Type
63 if typ == nil {
64
65 return
66 }
67 typeName := typ.String()
68 if whitelist && unkeyedLiteral[typeName] {
69
70 return
71 }
72 var structuralTypes []types.Type
73 switch typ := typ.(type) {
74 case *typeparams.TypeParam:
75 terms, err := typeparams.StructuralTerms(typ)
76 if err != nil {
77 return
78 }
79 for _, term := range terms {
80 structuralTypes = append(structuralTypes, term.Type())
81 }
82 default:
83 structuralTypes = append(structuralTypes, typ)
84 }
85 for _, typ := range structuralTypes {
86 under := deref(typ.Underlying())
87 strct, ok := under.(*types.Struct)
88 if !ok {
89
90 continue
91 }
92 if isLocalType(pass, typ) {
93
94 continue
95 }
96
97
98 allKeyValue := true
99 var suggestedFixAvailable = len(cl.Elts) == strct.NumFields()
100 var missingKeys []analysis.TextEdit
101 for i, e := range cl.Elts {
102 if _, ok := e.(*ast.KeyValueExpr); !ok {
103 allKeyValue = false
104 if i >= strct.NumFields() {
105 break
106 }
107 field := strct.Field(i)
108 if !field.Exported() {
109
110
111 suggestedFixAvailable = false
112 break
113 }
114 missingKeys = append(missingKeys, analysis.TextEdit{
115 Pos: e.Pos(),
116 End: e.Pos(),
117 NewText: []byte(fmt.Sprintf("%s: ", field.Name())),
118 })
119 }
120 }
121 if allKeyValue {
122
123 continue
124 }
125
126 diag := analysis.Diagnostic{
127 Pos: cl.Pos(),
128 End: cl.End(),
129 Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName),
130 }
131 if suggestedFixAvailable {
132 diag.SuggestedFixes = []analysis.SuggestedFix{{
133 Message: "Add field names to struct literal",
134 TextEdits: missingKeys,
135 }}
136 }
137 pass.Report(diag)
138 return
139 }
140 })
141 return nil, nil
142 }
143
144 func deref(typ types.Type) types.Type {
145 for {
146 ptr, ok := typ.(*types.Pointer)
147 if !ok {
148 break
149 }
150 typ = ptr.Elem().Underlying()
151 }
152 return typ
153 }
154
155 func isLocalType(pass *analysis.Pass, typ types.Type) bool {
156 switch x := typ.(type) {
157 case *types.Struct:
158
159 return true
160 case *types.Pointer:
161 return isLocalType(pass, x.Elem())
162 case *types.Named:
163
164 return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test")
165 case *typeparams.TypeParam:
166 return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test")
167 }
168 return false
169 }
170
View as plain text