1 | // Copyright 2012 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | // Package composite defines an Analyzer that checks for unkeyed |
6 | // composite literals. |
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 | // runUnkeyedLiteral checks if a composite literal is a struct literal with |
52 | // unkeyed fields. |
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 | // cannot determine composite literals' type, skip it |
65 | return |
66 | } |
67 | typeName := typ.String() |
68 | if whitelist && unkeyedLiteral[typeName] { |
69 | // skip whitelisted types |
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 // invalid type |
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 | // skip non-struct composite literals |
90 | continue |
91 | } |
92 | if isLocalType(pass, typ) { |
93 | // allow unkeyed locally defined composite literal |
94 | continue |
95 | } |
96 | |
97 | // check if the struct contains an unkeyed field |
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 | // Adding unexported field names for structs not defined |
110 | // locally will not work. |
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 | // all the struct fields are keyed |
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 | // struct literals are local types |
159 | return true |
160 | case *types.Pointer: |
161 | return isLocalType(pass, x.Elem()) |
162 | case *types.Named: |
163 | // names in package foo are local to foo_test too |
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 |
Members