@@ -11,6 +11,7 @@ import (
11
11
"slices"
12
12
"strconv"
13
13
"strings"
14
+ "unicode"
14
15
15
16
"github.com/ettle/strcase"
16
17
"golang.org/x/tools/go/analysis"
@@ -26,9 +27,10 @@ type Options struct {
26
27
AttrOnly bool // Enforce using attributes only (overrides NoMixedArgs, incompatible with KVOnly).
27
28
NoGlobal string // Enforce not using global loggers ("all" or "default").
28
29
ContextOnly string // Enforce using methods that accept a context ("all" or "scope").
29
- StaticMsg bool // Enforce using static log messages.
30
+ StaticMsg bool // Enforce using static messages.
31
+ MsgStyle string // Enforce message style ("lowercased" or "capitalized").
30
32
NoRawKeys bool // Enforce using constants instead of raw keys.
31
- KeyNamingCase string // Enforce a single key naming convention ("snake", "kebab", "camel", or "pascal").
33
+ KeyNamingCase string // Enforce key naming convention ("snake", "kebab", "camel", or "pascal").
32
34
ForbiddenKeys []string // Enforce not using specific keys.
33
35
ArgsOnSepLines bool // Enforce putting arguments on separate lines.
34
36
}
@@ -61,6 +63,12 @@ func New(opts *Options) *analysis.Analyzer {
61
63
return nil , fmt .Errorf ("sloglint: Options.ContextOnly=%s: %w" , opts .ContextOnly , errInvalidValue )
62
64
}
63
65
66
+ switch opts .MsgStyle {
67
+ case "" , styleLowercased , styleCapitalized :
68
+ default :
69
+ return nil , fmt .Errorf ("sloglint: Options.MsgStyle=%s: %w" , opts .MsgStyle , errInvalidValue )
70
+ }
71
+
64
72
switch opts .KeyNamingCase {
65
73
case "" , snakeCase , kebabCase , camelCase , pascalCase :
66
74
default :
@@ -101,9 +109,10 @@ func flags(opts *Options) flag.FlagSet {
101
109
boolVar (& opts .AttrOnly , "attr-only" , "enforce using attributes only (overrides -no-mixed-args, incompatible with -kv-only)" )
102
110
strVar (& opts .NoGlobal , "no-global" , "enforce not using global loggers (all|default)" )
103
111
strVar (& opts .ContextOnly , "context-only" , "enforce using methods that accept a context (all|scope)" )
104
- boolVar (& opts .StaticMsg , "static-msg" , "enforce using static log messages" )
112
+ boolVar (& opts .StaticMsg , "static-msg" , "enforce using static messages" )
113
+ strVar (& opts .MsgStyle , "msg-style" , "enforce message style (lowercased|capitalized)" )
105
114
boolVar (& opts .NoRawKeys , "no-raw-keys" , "enforce using constants instead of raw keys" )
106
- strVar (& opts .KeyNamingCase , "key-naming-case" , "enforce a single key naming convention (snake|kebab|camel|pascal)" )
115
+ strVar (& opts .KeyNamingCase , "key-naming-case" , "enforce key naming convention (snake|kebab|camel|pascal)" )
107
116
boolVar (& opts .ArgsOnSepLines , "args-on-sep-lines" , "enforce putting arguments on separate lines" )
108
117
109
118
fset .Func ("forbidden-keys" , "enforce not using specific keys (comma-separated)" , func (s string ) error {
@@ -155,6 +164,13 @@ var attrFuncs = map[string]struct{}{
155
164
"log/slog.Any" : {},
156
165
}
157
166
167
+ // message styles.
168
+ const (
169
+ styleLowercased = "lowercased"
170
+ styleCapitalized = "capitalized"
171
+ )
172
+
173
+ // key naming conventions.
158
174
const (
159
175
snakeCase = "snake"
160
176
kebabCase = "kebab"
@@ -228,6 +244,15 @@ func visit(pass *analysis.Pass, opts *Options, node ast.Node, stack []ast.Node)
228
244
pass .Reportf (call .Pos (), "message should be a string literal or a constant" )
229
245
}
230
246
247
+ if opts .MsgStyle != "" && msgPos >= 0 {
248
+ if msg , ok := call .Args [msgPos ].(* ast.BasicLit ); ok && msg .Kind == token .STRING {
249
+ msg .Value = msg .Value [1 : len (msg .Value )- 1 ] // trim quotes/backticks.
250
+ if ok := isValidMsgStyle (msg .Value , opts .MsgStyle ); ! ok {
251
+ pass .Reportf (call .Pos (), "message should be %s" , opts .MsgStyle )
252
+ }
253
+ }
254
+ }
255
+
231
256
// NOTE: we assume that the arguments have already been validated by govet.
232
257
args := call .Args [funcInfo .argsPos :]
233
258
if len (args ) == 0 {
@@ -356,6 +381,33 @@ func isStaticMsg(msg ast.Expr) bool {
356
381
}
357
382
}
358
383
384
+ func isValidMsgStyle (msg , style string ) bool {
385
+ runes := []rune (msg )
386
+ if len (runes ) < 2 {
387
+ return true
388
+ }
389
+
390
+ first , second := runes [0 ], runes [1 ]
391
+
392
+ switch style {
393
+ case styleLowercased :
394
+ if unicode .IsLower (first ) {
395
+ return true
396
+ }
397
+ if unicode .IsPunct (second ) {
398
+ return true // e.g. "U.S.A."
399
+ }
400
+ return unicode .IsUpper (second ) // e.g. "HTTP"
401
+ case styleCapitalized :
402
+ if unicode .IsUpper (first ) {
403
+ return true
404
+ }
405
+ return unicode .IsUpper (second ) // e.g. "iPhone"
406
+ default :
407
+ panic ("unreachable" )
408
+ }
409
+ }
410
+
359
411
func forEachKey (info * types.Info , keys , attrs []ast.Expr , fn func (key ast.Expr )) {
360
412
for _ , key := range keys {
361
413
fn (key )
0 commit comments