Skip to content

Commit 43ec8d8

Browse files
authored
feat: implement -static-msg (#18)
1 parent 61cece7 commit 43ec8d8

File tree

4 files changed

+68
-0
lines changed

4 files changed

+68
-0
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The linter has several options, so you can adjust it to your own code style.
2525
* Forbid mixing key-value pairs and attributes within a single function call (default)
2626
* Enforce using either key-value pairs or attributes for the entire project (optional)
2727
* Enforce using methods that accept a context (optional)
28+
* Enforce using static log messages (optional)
2829
* Enforce using constants instead of raw keys (optional)
2930
* Enforce a single key naming convention (optional)
3031
* Enforce putting arguments on separate lines (optional)
@@ -82,6 +83,21 @@ This report can be fixed by using the equivalent method with the `Context` suffi
8283
slog.InfoContext(ctx, "a user has logged in")
8384
```
8485

86+
### Static messages
87+
88+
To get the most out of structured logging, you may want to require log messages to be static.
89+
The `static-msg` option causes `sloglint` to report non-static messages:
90+
91+
```go
92+
slog.Info(fmt.Sprintf("a user with id %d has logged in", 42)) // sloglint: message should be a string literal or a constant
93+
```
94+
95+
The report can be fixed by moving dynamic values to arguments:
96+
97+
```go
98+
slog.Info("a user has logged in", "user_id", 42)
99+
```
100+
85101
### No raw keys
86102

87103
To prevent typos, you may want to forbid the use of raw keys altogether.

sloglint.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Options struct {
2222
KVOnly bool // Enforce using key-value pairs only (incompatible with AttrOnly).
2323
AttrOnly bool // Enforce using attributes only (incompatible with KVOnly).
2424
ContextOnly bool // Enforce using methods that accept a context.
25+
StaticMsg bool // Enforce using static log messages.
2526
NoRawKeys bool // Enforce using constants instead of raw keys.
2627
KeyNamingCase string // Enforce a single key naming convention ("snake", "kebab", "camel", or "pascal").
2728
ArgsOnSepLines bool // Enforce putting arguments on separate lines.
@@ -71,6 +72,7 @@ func flags(opts *Options) flag.FlagSet {
7172
boolVar(&opts.KVOnly, "kv-only", "enforce using key-value pairs only (incompatible with -attr-only)")
7273
boolVar(&opts.AttrOnly, "attr-only", "enforce using attributes only (incompatible with -kv-only)")
7374
boolVar(&opts.ContextOnly, "context-only", "enforce using methods that accept a context")
75+
boolVar(&opts.StaticMsg, "static-msg", "enforce using static log messages")
7476
boolVar(&opts.NoRawKeys, "no-raw-keys", "enforce using constants instead of raw keys")
7577
boolVar(&opts.ArgsOnSepLines, "args-on-sep-lines", "enforce putting arguments on separate lines")
7678

@@ -146,6 +148,9 @@ func run(pass *analysis.Pass, opts *Options) {
146148
pass.Reportf(call.Pos(), "methods without a context should not be used")
147149
}
148150
}
151+
if opts.StaticMsg && !staticMsg(call.Args[argsPos-1]) {
152+
pass.Reportf(call.Pos(), "message should be a string literal or a constant")
153+
}
149154

150155
// NOTE: we assume that the arguments have already been validated by govet.
151156
args := call.Args[argsPos:]
@@ -199,6 +204,17 @@ func run(pass *analysis.Pass, opts *Options) {
199204
})
200205
}
201206

207+
func staticMsg(expr ast.Expr) bool {
208+
switch msg := expr.(type) {
209+
case *ast.BasicLit: // e.g. slog.Info("msg")
210+
return msg.Kind == token.STRING
211+
case *ast.Ident: // e.g. const msg = "msg"; slog.Info(msg)
212+
return msg.Obj != nil && msg.Obj.Kind == ast.Con
213+
default:
214+
return false
215+
}
216+
}
217+
202218
func rawKeysUsed(info *types.Info, keys, attrs []ast.Expr) bool {
203219
isConst := func(expr ast.Expr) bool {
204220
ident, ok := expr.(*ast.Ident)

sloglint_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ func TestAnalyzer(t *testing.T) {
3030
analysistest.Run(t, testdata, analyzer, "context_only")
3131
})
3232

33+
t.Run("static message", func(t *testing.T) {
34+
analyzer := sloglint.New(&sloglint.Options{StaticMsg: true})
35+
analysistest.Run(t, testdata, analyzer, "static_msg")
36+
})
37+
3338
t.Run("no raw keys", func(t *testing.T) {
3439
analyzer := sloglint.New(&sloglint.Options{NoRawKeys: true})
3540
analysistest.Run(t, testdata, analyzer, "no_raw_keys")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package static_msg
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
)
8+
9+
const constMsg = "msg"
10+
11+
var varMsg = "msg"
12+
13+
func tests() {
14+
ctx := context.Background()
15+
16+
slog.Info("msg")
17+
slog.InfoContext(ctx, "msg")
18+
slog.Log(ctx, slog.LevelInfo, "msg")
19+
20+
slog.Info(constMsg)
21+
slog.InfoContext(ctx, constMsg)
22+
slog.Log(ctx, slog.LevelInfo, constMsg)
23+
24+
slog.Info(fmt.Sprintf("msg")) // want `message should be a string literal or a constant`
25+
slog.InfoContext(ctx, fmt.Sprintf("msg")) // want `message should be a string literal or a constant`
26+
slog.Log(ctx, slog.LevelInfo, fmt.Sprintf("msg")) // want `message should be a string literal or a constant`
27+
28+
slog.Info(varMsg) // want `message should be a string literal or a constant`
29+
slog.InfoContext(ctx, varMsg) // want `message should be a string literal or a constant`
30+
slog.Log(ctx, slog.LevelInfo, varMsg) // want `message should be a string literal or a constant`
31+
}

0 commit comments

Comments
 (0)