@@ -24,7 +24,7 @@ type Options struct {
24
24
KVOnly bool // Enforce using key-value pairs only (overrides NoMixedArgs, incompatible with AttrOnly).
25
25
AttrOnly bool // Enforce using attributes only (overrides NoMixedArgs, incompatible with KVOnly).
26
26
NoGlobal string // Enforce not using global loggers ("all" or "default").
27
- ContextOnly bool // Enforce using methods that accept a context.
27
+ ContextOnly string // Enforce using methods that accept a context ("all" or "scope") .
28
28
StaticMsg bool // Enforce using static log messages.
29
29
NoRawKeys bool // Enforce using constants instead of raw keys.
30
30
KeyNamingCase string // Enforce a single key naming convention ("snake", "kebab", "camel", or "pascal").
@@ -36,6 +36,7 @@ func New(opts *Options) *analysis.Analyzer {
36
36
if opts == nil {
37
37
opts = & Options {NoMixedArgs : true }
38
38
}
39
+
39
40
return & analysis.Analyzer {
40
41
Name : "sloglint" ,
41
42
Doc : "ensure consistent code style when using log/slog" ,
@@ -52,6 +53,12 @@ func New(opts *Options) *analysis.Analyzer {
52
53
return nil , fmt .Errorf ("sloglint: Options.NoGlobal=%s: %w" , opts .NoGlobal , errInvalidValue )
53
54
}
54
55
56
+ switch opts .ContextOnly {
57
+ case "" , "all" , "scope" :
58
+ default :
59
+ return nil , fmt .Errorf ("sloglint: Options.ContextOnly=%s: %w" , opts .ContextOnly , errInvalidValue )
60
+ }
61
+
55
62
switch opts .KeyNamingCase {
56
63
case "" , snakeCase , kebabCase , camelCase , pascalCase :
57
64
default :
@@ -91,7 +98,7 @@ func flags(opts *Options) flag.FlagSet {
91
98
boolVar (& opts .KVOnly , "kv-only" , "enforce using key-value pairs only (overrides -no-mixed-args, incompatible with -attr-only)" )
92
99
boolVar (& opts .AttrOnly , "attr-only" , "enforce using attributes only (overrides -no-mixed-args, incompatible with -kv-only)" )
93
100
strVar (& opts .NoGlobal , "no-global" , "enforce not using global loggers (all|default)" )
94
- boolVar (& opts .ContextOnly , "context-only" , "enforce using methods that accept a context" )
101
+ strVar (& opts .ContextOnly , "context-only" , "enforce using methods that accept a context (all|scope) " )
95
102
boolVar (& opts .StaticMsg , "static-msg" , "enforce using static log messages" )
96
103
boolVar (& opts .NoRawKeys , "no-raw-keys" , "enforce using constants instead of raw keys" )
97
104
strVar (& opts .KeyNamingCase , "key-naming-case" , "enforce a single key naming convention (snake|kebab|camel|pascal)" )
@@ -142,96 +149,116 @@ const (
142
149
)
143
150
144
151
func run (pass * analysis.Pass , opts * Options ) {
145
- visit := pass .ResultOf [inspect .Analyzer ].(* inspector.Inspector )
152
+ visitor := pass .ResultOf [inspect .Analyzer ].(* inspector.Inspector )
146
153
filter := []ast.Node {(* ast .CallExpr )(nil )}
147
154
148
- visit .Preorder (filter , func (node ast.Node ) {
149
- call := node .(* ast.CallExpr )
155
+ // WithStack is ~2x slower than Preorder, use it only when stack is needed.
156
+ if opts .ContextOnly == "scope" {
157
+ visitor .WithStack (filter , func (node ast.Node , _ bool , stack []ast.Node ) bool {
158
+ visit (pass , opts , node , stack )
159
+ return false
160
+ })
161
+ return
162
+ }
150
163
151
- fn := typeutil . StaticCallee ( pass . TypesInfo , call )
152
- if fn == nil {
153
- return
154
- }
164
+ visitor . Preorder ( filter , func ( node ast. Node ) {
165
+ visit ( pass , opts , node , nil )
166
+ })
167
+ }
155
168
156
- name := fn .FullName ()
157
- argsPos , ok := slogFuncs [name ]
158
- if ! ok {
159
- return
160
- }
169
+ // NOTE: stack is nil if Preorder is used.
170
+ func visit (pass * analysis.Pass , opts * Options , node ast.Node , stack []ast.Node ) {
171
+ call := node .(* ast.CallExpr )
161
172
162
- switch opts .NoGlobal {
163
- case "all" :
164
- if strings .HasPrefix (name , "log/slog." ) || globalLoggerUsed (pass .TypesInfo , call .Fun ) {
165
- pass .Reportf (call .Pos (), "global logger should not be used" )
166
- }
167
- case "default" :
168
- if strings .HasPrefix (name , "log/slog." ) {
169
- pass .Reportf (call .Pos (), "default logger should not be used" )
170
- }
171
- }
173
+ fn := typeutil .StaticCallee (pass .TypesInfo , call )
174
+ if fn == nil {
175
+ return
176
+ }
172
177
173
- if opts .ContextOnly {
174
- typ := pass .TypesInfo .TypeOf (call .Args [0 ])
175
- if typ != nil && typ .String () != "context.Context" {
176
- pass .Reportf (call .Pos (), "methods without a context should not be used" )
177
- }
178
- }
178
+ name := fn .FullName ()
179
+ argsPos , ok := slogFuncs [name ]
180
+ if ! ok {
181
+ return
182
+ }
179
183
180
- if opts .StaticMsg && ! staticMsg (call .Args [argsPos - 1 ]) {
181
- pass .Reportf (call .Pos (), "message should be a string literal or a constant" )
184
+ switch opts .NoGlobal {
185
+ case "all" :
186
+ if strings .HasPrefix (name , "log/slog." ) || globalLoggerUsed (pass .TypesInfo , call .Fun ) {
187
+ pass .Reportf (call .Pos (), "global logger should not be used" )
182
188
}
189
+ case "default" :
190
+ if strings .HasPrefix (name , "log/slog." ) {
191
+ pass .Reportf (call .Pos (), "default logger should not be used" )
192
+ }
193
+ }
183
194
184
- // NOTE: we assume that the arguments have already been validated by govet.
185
- args := call .Args [argsPos :]
186
- if len (args ) == 0 {
187
- return
195
+ switch opts .ContextOnly {
196
+ case "all" :
197
+ typ := pass .TypesInfo .TypeOf (call .Args [0 ])
198
+ if typ != nil && typ .String () != "context.Context" {
199
+ pass .Reportf (call .Pos (), "%sContext should be used instead" , fn .Name ())
200
+ }
201
+ case "scope" :
202
+ typ := pass .TypesInfo .TypeOf (call .Args [0 ])
203
+ if typ != nil && typ .String () != "context.Context" && hasContextInScope (pass .TypesInfo , stack ) {
204
+ pass .Reportf (call .Pos (), "%sContext should be used instead" , fn .Name ())
188
205
}
206
+ }
189
207
190
- var keys []ast.Expr
191
- var attrs []ast.Expr
208
+ if opts .StaticMsg && ! staticMsg (call .Args [argsPos - 1 ]) {
209
+ pass .Reportf (call .Pos (), "message should be a string literal or a constant" )
210
+ }
192
211
193
- for i := 0 ; i < len (args ); i ++ {
194
- typ := pass .TypesInfo .TypeOf (args [i ])
195
- if typ == nil {
196
- continue
197
- }
198
- switch typ .String () {
199
- case "string" :
200
- keys = append (keys , args [i ])
201
- i ++ // skip the value.
202
- case "log/slog.Attr" :
203
- attrs = append (attrs , args [i ])
204
- }
205
- }
212
+ // NOTE: we assume that the arguments have already been validated by govet.
213
+ args := call .Args [argsPos :]
214
+ if len (args ) == 0 {
215
+ return
216
+ }
206
217
207
- switch {
208
- case opts .KVOnly && len (attrs ) > 0 :
209
- pass .Reportf (call .Pos (), "attributes should not be used" )
210
- case opts .AttrOnly && len (attrs ) < len (args ):
211
- pass .Reportf (call .Pos (), "key-value pairs should not be used" )
212
- case opts .NoMixedArgs && 0 < len (attrs ) && len (attrs ) < len (args ):
213
- pass .Reportf (call .Pos (), "key-value pairs and attributes should not be mixed" )
214
- }
218
+ var keys []ast.Expr
219
+ var attrs []ast.Expr
215
220
216
- if opts .NoRawKeys && rawKeysUsed (pass .TypesInfo , keys , attrs ) {
217
- pass .Reportf (call .Pos (), "raw keys should not be used" )
221
+ for i := 0 ; i < len (args ); i ++ {
222
+ typ := pass .TypesInfo .TypeOf (args [i ])
223
+ if typ == nil {
224
+ continue
218
225
}
219
-
220
- if opts .ArgsOnSepLines && argsOnSameLine (pass .Fset , call , keys , attrs ) {
221
- pass .Reportf (call .Pos (), "arguments should be put on separate lines" )
226
+ switch typ .String () {
227
+ case "string" :
228
+ keys = append (keys , args [i ])
229
+ i ++ // skip the value.
230
+ case "log/slog.Attr" :
231
+ attrs = append (attrs , args [i ])
222
232
}
233
+ }
223
234
224
- switch {
225
- case opts .KeyNamingCase == snakeCase && badKeyNames (pass .TypesInfo , strcase .ToSnake , keys , attrs ):
226
- pass .Reportf (call .Pos (), "keys should be written in snake_case" )
227
- case opts .KeyNamingCase == kebabCase && badKeyNames (pass .TypesInfo , strcase .ToKebab , keys , attrs ):
228
- pass .Reportf (call .Pos (), "keys should be written in kebab-case" )
229
- case opts .KeyNamingCase == camelCase && badKeyNames (pass .TypesInfo , strcase .ToCamel , keys , attrs ):
230
- pass .Reportf (call .Pos (), "keys should be written in camelCase" )
231
- case opts .KeyNamingCase == pascalCase && badKeyNames (pass .TypesInfo , strcase .ToPascal , keys , attrs ):
232
- pass .Reportf (call .Pos (), "keys should be written in PascalCase" )
233
- }
234
- })
235
+ switch {
236
+ case opts .KVOnly && len (attrs ) > 0 :
237
+ pass .Reportf (call .Pos (), "attributes should not be used" )
238
+ case opts .AttrOnly && len (attrs ) < len (args ):
239
+ pass .Reportf (call .Pos (), "key-value pairs should not be used" )
240
+ case opts .NoMixedArgs && 0 < len (attrs ) && len (attrs ) < len (args ):
241
+ pass .Reportf (call .Pos (), "key-value pairs and attributes should not be mixed" )
242
+ }
243
+
244
+ if opts .NoRawKeys && rawKeysUsed (pass .TypesInfo , keys , attrs ) {
245
+ pass .Reportf (call .Pos (), "raw keys should not be used" )
246
+ }
247
+
248
+ if opts .ArgsOnSepLines && argsOnSameLine (pass .Fset , call , keys , attrs ) {
249
+ pass .Reportf (call .Pos (), "arguments should be put on separate lines" )
250
+ }
251
+
252
+ switch {
253
+ case opts .KeyNamingCase == snakeCase && badKeyNames (pass .TypesInfo , strcase .ToSnake , keys , attrs ):
254
+ pass .Reportf (call .Pos (), "keys should be written in snake_case" )
255
+ case opts .KeyNamingCase == kebabCase && badKeyNames (pass .TypesInfo , strcase .ToKebab , keys , attrs ):
256
+ pass .Reportf (call .Pos (), "keys should be written in kebab-case" )
257
+ case opts .KeyNamingCase == camelCase && badKeyNames (pass .TypesInfo , strcase .ToCamel , keys , attrs ):
258
+ pass .Reportf (call .Pos (), "keys should be written in camelCase" )
259
+ case opts .KeyNamingCase == pascalCase && badKeyNames (pass .TypesInfo , strcase .ToPascal , keys , attrs ):
260
+ pass .Reportf (call .Pos (), "keys should be written in PascalCase" )
261
+ }
235
262
}
236
263
237
264
func globalLoggerUsed (info * types.Info , expr ast.Expr ) bool {
@@ -247,6 +274,24 @@ func globalLoggerUsed(info *types.Info, expr ast.Expr) bool {
247
274
return obj .Parent () == obj .Pkg ().Scope ()
248
275
}
249
276
277
+ func hasContextInScope (info * types.Info , stack []ast.Node ) bool {
278
+ for i := len (stack ) - 1 ; i >= 0 ; i -- {
279
+ decl , ok := stack [i ].(* ast.FuncDecl )
280
+ if ! ok {
281
+ continue
282
+ }
283
+ params := decl .Type .Params
284
+ if len (params .List ) == 0 || len (params .List [0 ].Names ) == 0 {
285
+ continue
286
+ }
287
+ typ := info .TypeOf (params .List [0 ].Names [0 ])
288
+ if typ != nil && typ .String () == "context.Context" {
289
+ return true
290
+ }
291
+ }
292
+ return false
293
+ }
294
+
250
295
func staticMsg (expr ast.Expr ) bool {
251
296
switch msg := expr .(type ) {
252
297
case * ast.BasicLit : // e.g. slog.Info("msg")
0 commit comments