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