Skip to content

Commit 431c6b7

Browse files
authored
feat: implement --forbidden-keys (#40)
1 parent f4014dd commit 431c6b7

File tree

4 files changed

+107
-25
lines changed

4 files changed

+107
-25
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ With `sloglint` you can enforce various rules for `log/slog` based on your prefe
2222
* Enforce using static log messages (optional)
2323
* Enforce using constants instead of raw keys (optional)
2424
* Enforce a single key naming convention (optional)
25+
* Enforce not using specific keys (optional)
2526
* Enforce putting arguments on separate lines (optional)
2627

2728
## 📦 Install
@@ -74,7 +75,7 @@ slog.Info("a user has logged in", "user_id", 42) // sloglint: key-value pairs sh
7475
### No global
7576

7677
Some projects prefer to pass loggers as explicit dependencies.
77-
The `no-global` option causes `sloglint` to report the usage of global loggers.
78+
The `no-global` option causes `sloglint` to report the use of global loggers.
7879

7980
```go
8081
slog.Info("a user has logged in", "user_id", 42) // sloglint: global logger should not be used
@@ -149,6 +150,18 @@ slog.Info("a user has logged in", "user-id", 42) // sloglint: keys should be wri
149150

150151
Possible values are `snake`, `kebab`, `camel`, or `pascal`.
151152

153+
### Forbidden keys
154+
155+
To prevent accidental use of reserved log keys, you may want to forbid specific keys altogether.
156+
The `forbidden-keys` option causes `sloglint` to report the use of forbidden keys:
157+
158+
```go
159+
slog.Info("a user has logged in", "reserved", 42) // sloglint: "reserved" key is forbidden and should not be used
160+
```
161+
162+
For example, when using the standard `slog.JSONHandler` and `slog.TextHandler`,
163+
you may want to forbid the `time`, `level`, `msg`, and `source` keys, as these are used by the handlers.
164+
152165
### Arguments on separate lines
153166

154167
To improve code readability, you may want to put arguments on separate lines, especially when using key-value pairs.

sloglint.go

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"go/ast"
99
"go/token"
1010
"go/types"
11+
"slices"
1112
"strconv"
1213
"strings"
1314

@@ -20,15 +21,16 @@ import (
2021

2122
// Options are options for the sloglint analyzer.
2223
type Options struct {
23-
NoMixedArgs bool // Enforce not mixing key-value pairs and attributes (default true).
24-
KVOnly bool // Enforce using key-value pairs only (overrides NoMixedArgs, incompatible with AttrOnly).
25-
AttrOnly bool // Enforce using attributes only (overrides NoMixedArgs, incompatible with KVOnly).
26-
NoGlobal string // Enforce not using global loggers ("all" or "default").
27-
ContextOnly string // Enforce using methods that accept a context ("all" or "scope").
28-
StaticMsg bool // Enforce using static log messages.
29-
NoRawKeys bool // Enforce using constants instead of raw keys.
30-
KeyNamingCase string // Enforce a single key naming convention ("snake", "kebab", "camel", or "pascal").
31-
ArgsOnSepLines bool // Enforce putting arguments on separate lines.
24+
NoMixedArgs bool // Enforce not mixing key-value pairs and attributes (default true).
25+
KVOnly bool // Enforce using key-value pairs only (overrides NoMixedArgs, incompatible with AttrOnly).
26+
AttrOnly bool // Enforce using attributes only (overrides NoMixedArgs, incompatible with KVOnly).
27+
NoGlobal string // Enforce not using global loggers ("all" or "default").
28+
ContextOnly string // Enforce using methods that accept a context ("all" or "scope").
29+
StaticMsg bool // Enforce using static log messages.
30+
NoRawKeys bool // Enforce using constants instead of raw keys.
31+
KeyNamingCase string // Enforce a single key naming convention ("snake", "kebab", "camel", or "pascal").
32+
ForbiddenKeys []string // Enforce not using specific keys.
33+
ArgsOnSepLines bool // Enforce putting arguments on separate lines.
3234
}
3335

3436
// New creates a new sloglint analyzer.
@@ -104,6 +106,11 @@ func flags(opts *Options) flag.FlagSet {
104106
strVar(&opts.KeyNamingCase, "key-naming-case", "enforce a single key naming convention (snake|kebab|camel|pascal)")
105107
boolVar(&opts.ArgsOnSepLines, "args-on-sep-lines", "enforce putting arguments on separate lines")
106108

109+
fset.Func("forbidden-keys", "enforce not using specific keys (comma-separated)", func(s string) error {
110+
opts.ForbiddenKeys = append(opts.ForbiddenKeys, strings.Split(s, ",")...)
111+
return nil
112+
})
113+
107114
return *fset
108115
}
109116

@@ -249,15 +256,41 @@ func visit(pass *analysis.Pass, opts *Options, node ast.Node, stack []ast.Node)
249256
pass.Reportf(call.Pos(), "arguments should be put on separate lines")
250257
}
251258

259+
if len(opts.ForbiddenKeys) > 0 {
260+
if name, found := badKeyNames(pass.TypesInfo, isForbiddenKey(opts.ForbiddenKeys), keys, attrs); found {
261+
pass.Reportf(call.Pos(), "%q key is forbidden and should not be used", name)
262+
}
263+
}
264+
252265
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")
266+
case opts.KeyNamingCase == snakeCase:
267+
if _, found := badKeyNames(pass.TypesInfo, valueChanged(strcase.ToSnake), keys, attrs); found {
268+
pass.Reportf(call.Pos(), "keys should be written in snake_case")
269+
}
270+
case opts.KeyNamingCase == kebabCase:
271+
if _, found := badKeyNames(pass.TypesInfo, valueChanged(strcase.ToKebab), keys, attrs); found {
272+
pass.Reportf(call.Pos(), "keys should be written in kebab-case")
273+
}
274+
case opts.KeyNamingCase == camelCase:
275+
if _, found := badKeyNames(pass.TypesInfo, valueChanged(strcase.ToCamel), keys, attrs); found {
276+
pass.Reportf(call.Pos(), "keys should be written in camelCase")
277+
}
278+
case opts.KeyNamingCase == pascalCase:
279+
if _, found := badKeyNames(pass.TypesInfo, valueChanged(strcase.ToPascal), keys, attrs); found {
280+
pass.Reportf(call.Pos(), "keys should be written in PascalCase")
281+
}
282+
}
283+
}
284+
285+
func isForbiddenKey(forbiddenKeys []string) func(string) bool {
286+
return func(name string) bool {
287+
return slices.Contains(forbiddenKeys, name)
288+
}
289+
}
290+
291+
func valueChanged(handler func(string) string) func(string) bool {
292+
return func(name string) bool {
293+
return handler(name) != name
261294
}
262295
}
263296

@@ -351,10 +384,10 @@ func rawKeysUsed(info *types.Info, keys, attrs []ast.Expr) bool {
351384
return false
352385
}
353386

354-
func badKeyNames(info *types.Info, caseFn func(string) string, keys, attrs []ast.Expr) bool {
387+
func badKeyNames(info *types.Info, validationFn func(string) bool, keys, attrs []ast.Expr) (string, bool) {
355388
for _, key := range keys {
356-
if name, ok := getKeyName(key); ok && name != caseFn(name) {
357-
return true
389+
if name, ok := getKeyName(key); ok && validationFn(name) {
390+
return name, true
358391
}
359392
}
360393

@@ -389,12 +422,12 @@ func badKeyNames(info *types.Info, caseFn func(string) string, keys, attrs []ast
389422
}
390423
}
391424

392-
if name, ok := getKeyName(expr); ok && name != caseFn(name) {
393-
return true
425+
if name, ok := getKeyName(expr); ok && validationFn(name) {
426+
return name, true
394427
}
395428
}
396429

397-
return false
430+
return "", false
398431
}
399432

400433
func getKeyName(expr ast.Expr) (string, bool) {
@@ -411,7 +444,12 @@ func getKeyName(expr ast.Expr) (string, bool) {
411444
}
412445
}
413446
if lit, ok := expr.(*ast.BasicLit); ok && lit.Kind == token.STRING {
414-
return lit.Value, true
447+
// string literals are always quoted.
448+
value, err := strconv.Unquote(lit.Value)
449+
if err != nil {
450+
panic("unreachable")
451+
}
452+
return value, true
415453
}
416454
return "", false
417455
}

sloglint_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,9 @@ func TestAnalyzer(t *testing.T) {
6464
analyzer := sloglint.New(&sloglint.Options{ArgsOnSepLines: true})
6565
analysistest.Run(t, testdata, analyzer, "args_on_sep_lines")
6666
})
67+
68+
t.Run("forbidden keys", func(t *testing.T) {
69+
analyzer := sloglint.New(&sloglint.Options{ForbiddenKeys: []string{"foo_bar"}})
70+
analysistest.Run(t, testdata, analyzer, "forbidden_keys")
71+
})
6772
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package forbidden_keys
2+
3+
import "log/slog"
4+
5+
const (
6+
snakeKey = "foo_bar"
7+
)
8+
9+
func tests() {
10+
slog.Info("msg")
11+
slog.Info("msg", "foo-bar", 1)
12+
slog.Info("msg", "foo_bar", 1) // want `"foo_bar" key is forbidden and should not be used`
13+
slog.Info("msg", snakeKey, 1) // want `"foo_bar" key is forbidden and should not be used`
14+
slog.Info("msg", slog.Int("foo_bar", 1)) // want `"foo_bar" key is forbidden and should not be used`
15+
slog.Info("msg", slog.Int(snakeKey, 1)) // want `"foo_bar" key is forbidden and should not be used`
16+
slog.Info("msg", slog.Attr{})
17+
slog.Info("msg", slog.Attr{"foo_bar", slog.IntValue(1)}) // want `"foo_bar" key is forbidden and should not be used`
18+
slog.Info("msg", slog.Attr{snakeKey, slog.IntValue(1)}) // want `"foo_bar" key is forbidden and should not be used`
19+
slog.Info("msg", slog.Attr{Key: "foo_bar"}) // want `"foo_bar" key is forbidden and should not be used`
20+
slog.Info("msg", slog.Attr{Key: snakeKey}) // want `"foo_bar" key is forbidden and should not be used`
21+
slog.Info("msg", slog.Attr{Key: "foo_bar", Value: slog.IntValue(1)}) // want `"foo_bar" key is forbidden and should not be used`
22+
slog.Info("msg", slog.Attr{Key: snakeKey, Value: slog.IntValue(1)}) // want `"foo_bar" key is forbidden and should not be used`
23+
slog.Info("msg", slog.Attr{Value: slog.IntValue(1), Key: "foo_bar"}) // want `"foo_bar" key is forbidden and should not be used`
24+
slog.Info("msg", slog.Attr{Value: slog.IntValue(1), Key: snakeKey}) // want `"foo_bar" key is forbidden and should not be used`
25+
slog.Info("msg", slog.Attr{Value: slog.IntValue(1), Key: `foo_bar`}) // want `"foo_bar" key is forbidden and should not be used`
26+
}

0 commit comments

Comments
 (0)