@@ -57,8 +57,8 @@ var builtin = []Func{
57
57
{Name : "github.com/mitchellh/mapstructure.WeakDecodeMetadata" , Tag : "mapstructure" , ArgPos : 1 },
58
58
}
59
59
60
- // flags creates a flag set for the analyzer. The funcs slice will be filled
61
- // with custom functions passed via CLI flags.
60
+ // flags creates a flag set for the analyzer.
61
+ // The funcs slice will be filled with custom functions passed via CLI flags.
62
62
func flags (funcs * []Func ) flag.FlagSet {
63
63
fs := flag .NewFlagSet ("musttag" , flag .ContinueOnError )
64
64
fs .Func ("fn" , "report custom function (name:tag:argpos)" , func (s string ) error {
@@ -80,8 +80,9 @@ func flags(funcs *[]Func) flag.FlagSet {
80
80
return * fs
81
81
}
82
82
83
- // New creates a new musttag analyzer. To report a custom function provide its
84
- // description via Func, it will be added to the builtin ones.
83
+ // New creates a new musttag analyzer.
84
+ // To report a custom function provide its description via Func,
85
+ // it will be added to the builtin ones.
85
86
func New (funcs ... Func ) * analysis.Analyzer {
86
87
var flagFuncs []Func
87
88
return & analysis.Analyzer {
@@ -112,125 +113,135 @@ var (
112
113
113
114
// reportf is a wrapper for pass.Reportf (as a variable, so it could be mocked in tests).
114
115
reportf = func (pass * analysis.Pass , pos token.Pos , fn Func ) {
116
+ // TODO(junk1tm): print the name of the struct type as well?
115
117
pass .Reportf (pos , "exported fields should be annotated with the %q tag" , fn .Tag )
116
118
}
117
119
)
118
120
119
121
// run starts the analysis.
120
122
func run (pass * analysis.Pass , funcs map [string ]Func ) (any , error ) {
121
- insp := pass .ResultOf [inspect .Analyzer ].(* inspector.Inspector )
122
-
123
- filter := []ast.Node {
124
- (* ast .CallExpr )(nil ),
125
- }
126
-
127
123
type report struct {
128
- pos token.Pos
129
- tag string
124
+ pos token.Pos // the position for report.
125
+ tag string // the missing struct tag.
130
126
}
131
- reported := make (map [report ]struct {})
132
127
133
- insp .Preorder (filter , func (n ast.Node ) {
134
- call := n .(* ast.CallExpr )
128
+ // store previous reports to prevent reporting
129
+ // the same struct more than once (if reportOnce is true).
130
+ reports := make (map [report ]struct {})
131
+
132
+ walk := pass .ResultOf [inspect .Analyzer ].(* inspector.Inspector )
133
+ filter := []ast.Node {(* ast .CallExpr )(nil )}
134
+
135
+ walk .Preorder (filter , func (n ast.Node ) {
136
+ call , ok := n .(* ast.CallExpr )
137
+ if ! ok {
138
+ return // not a function call.
139
+ }
135
140
136
141
callee := typeutil .StaticCallee (pass .TypesInfo , call )
137
142
if callee == nil {
138
- return
143
+ return // not a static call.
139
144
}
140
145
141
146
fn , ok := funcs [callee .FullName ()]
142
147
if ! ok {
143
- return
148
+ return // the function is not supported.
149
+ }
150
+
151
+ if len (call .Args ) <= fn .ArgPos {
152
+ return // TODO(junk1tm): return a proper error.
153
+ }
154
+
155
+ arg := call .Args [fn .ArgPos ]
156
+ if unary , ok := arg .(* ast.UnaryExpr ); ok {
157
+ arg = unary .X // e.g. json.Marshal(&foo)
158
+ }
159
+
160
+ initialPos := token .NoPos
161
+ switch arg := arg .(type ) {
162
+ case * ast.Ident : // e.g. json.Marshal(foo)
163
+ initialPos = arg .Obj .Pos ()
164
+ case * ast.CompositeLit : // e.g. json.Marshal(struct{}{})
165
+ initialPos = arg .Pos ()
144
166
}
145
167
146
- s , pos , ok := structAndPos (pass , call .Args [fn .ArgPos ])
168
+ t := pass .TypesInfo .TypeOf (arg )
169
+ s , ok := parseStruct (t , initialPos )
147
170
if ! ok {
148
- return
171
+ return // not a struct argument.
149
172
}
150
173
151
- if ok := checkStruct (s , fn .Tag , & pos ); ok {
152
- return
174
+ reportPos , ok := checkStruct (s , fn .Tag )
175
+ if ok {
176
+ return // nothing to report.
153
177
}
154
178
155
- r := report {pos , fn .Tag }
156
- if _ , ok := reported [r ]; ok && reportOnce {
157
- return
179
+ r := report {reportPos , fn .Tag }
180
+ if _ , ok := reports [r ]; ok && reportOnce {
181
+ return // already reported.
158
182
}
159
183
160
- reportf (pass , pos , fn )
161
- reported [r ] = struct {}{}
184
+ reportf (pass , reportPos , fn )
185
+ reports [r ] = struct {}{}
162
186
})
163
187
164
188
return nil , nil
165
189
}
166
190
167
- // structAndPos analyses the given expression and returns the struct to check
168
- // and the position to report if needed.
169
- func structAndPos (pass * analysis.Pass , expr ast.Expr ) (* types.Struct , token.Pos , bool ) {
170
- t := pass .TypesInfo .TypeOf (expr )
171
- if ptr , ok := t .(* types.Pointer ); ok {
191
+ // structInfo expands types.Struct with its position in the source code.
192
+ // If the struct is anonymous, Pos points to the corresponding identifier.
193
+ type structInfo struct {
194
+ * types.Struct
195
+ Pos token.Pos
196
+ }
197
+
198
+ // parseStruct parses the given types.Type, returning the underlying struct type.
199
+ // If it's a named type, the result will contain the position of its declaration,
200
+ // or the given token.Pos otherwise.
201
+ func parseStruct (t types.Type , pos token.Pos ) (* structInfo , bool ) {
202
+ for {
203
+ // unwrap pointers (if any) first.
204
+ ptr , ok := t .(* types.Pointer )
205
+ if ! ok {
206
+ break
207
+ }
172
208
t = ptr .Elem ()
173
209
}
174
210
175
211
switch t := t .(type ) {
176
- case * types.Named : // named type
177
- s , ok := t .Underlying ().(* types.Struct )
178
- if ok {
179
- return s , t .Obj ().Pos (), true
180
- }
181
-
182
- case * types.Struct : // anonymous struct
183
- if unary , ok := expr .(* ast.UnaryExpr ); ok {
184
- expr = unary .X // &x
185
- }
186
- //nolint:gocritic // commentedOutCode: these are examples
187
- switch arg := expr .(type ) {
188
- case * ast.Ident : // var x struct{}; json.Marshal(x)
189
- return t , arg .Obj .Pos (), true
190
- case * ast.CompositeLit : // json.Marshal(struct{}{})
191
- return t , arg .Pos (), true
212
+ case * types.Named : // a struct of the named type.
213
+ if s , ok := t .Underlying ().(* types.Struct ); ok {
214
+ return & structInfo {Struct : s , Pos : t .Obj ().Pos ()}, true
192
215
}
216
+ case * types.Struct : // an anonymous struct.
217
+ return & structInfo {Struct : t , Pos : pos }, true
193
218
}
194
219
195
- return nil , 0 , false
220
+ return nil , false
196
221
}
197
222
198
- // checkStruct checks that exported fields of the given struct are annotated
199
- // with the tag and updates the position to report in case a nested struct of a
200
- // named type is found.
201
- func checkStruct (s * types.Struct , tag string , pos * token.Pos ) (ok bool ) {
223
+ // checkStruct recursively checks the given struct and returns the position for report,
224
+ // in case one of its fields is missing the tag.
225
+ func checkStruct (s * structInfo , tag string ) (token.Pos , bool ) {
202
226
for i := 0 ; i < s .NumFields (); i ++ {
203
227
if ! s .Field (i ).Exported () {
204
228
continue
205
229
}
206
230
207
231
st := reflect .StructTag (s .Tag (i ))
208
- if _ , ok := st .Lookup (tag ); ! ok {
209
- // it's ok for embedded types not to be tagged,
210
- // see https://github.com/junk1tm/musttag/issues/12
211
- if ! s .Field (i ).Embedded () {
212
- return false
213
- }
232
+ if _ , ok := st .Lookup (tag ); ! ok && ! s .Field (i ).Embedded () {
233
+ return s .Pos , false
214
234
}
215
235
216
- // check if the field is a nested struct.
217
236
t := s .Field (i ).Type ()
218
- if ptr , ok := t .(* types.Pointer ); ok {
219
- t = ptr .Elem ()
220
- }
221
- nested , ok := t .Underlying ().(* types.Struct )
237
+ nested , ok := parseStruct (t , s .Pos ) // TODO(junk1tm): or s.Field(i).Pos()?
222
238
if ! ok {
223
239
continue
224
240
}
225
- if ok := checkStruct (nested , tag , pos ); ok {
226
- continue
227
- }
228
- // update the position to point to the named type.
229
- if named , ok := t .(* types.Named ); ok {
230
- * pos = named .Obj ().Pos ()
241
+ if pos , ok := checkStruct (nested , tag ); ! ok {
242
+ return pos , false
231
243
}
232
- return false
233
244
}
234
245
235
- return true
246
+ return token . NoPos , true
236
247
}
0 commit comments