Skip to content

Commit f87bbdf

Browse files
authored
feat: implement -no-global (#32)
1 parent b258907 commit f87bbdf

File tree

6 files changed

+98
-11
lines changed

6 files changed

+98
-11
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ With `sloglint` you can enforce various rules for `log/slog` based on your prefe
1717

1818
* Enforce not mixing key-value pairs and attributes (default)
1919
* Enforce using either key-value pairs only or attributes only (optional)
20+
* Enforce not using global loggers (optional)
2021
* Enforce using methods that accept a context (optional)
2122
* Enforce using static log messages (optional)
2223
* Enforce using constants instead of raw keys (optional)
@@ -70,6 +71,17 @@ In contrast, the `attr-only` option causes `sloglint` to report any use of key-v
7071
slog.Info("a user has logged in", "user_id", 42) // sloglint: key-value pairs should not be used
7172
```
7273

74+
### No global
75+
76+
Some projects prefer to pass loggers as explicit dependencies.
77+
The `no-global` option causes `sloglint` to report the usage of global loggers.
78+
79+
```go
80+
slog.Info("a user has logged in", "user_id", 42) // sloglint: global logger should not be used
81+
```
82+
83+
Possible values are `all` (report all global loggers) and `default` (report only the default `slog` logger).
84+
7385
### Context only
7486

7587
Some `slog.Handler` implementations make use of the given `context.Context` (e.g. to access context values).

sloglint.go

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"go/token"
1010
"go/types"
1111
"strconv"
12+
"strings"
1213

1314
"github.com/ettle/strcase"
1415
"golang.org/x/tools/go/analysis"
@@ -22,6 +23,7 @@ type Options struct {
2223
NoMixedArgs bool // Enforce not mixing key-value pairs and attributes (default true).
2324
KVOnly bool // Enforce using key-value pairs only (overrides NoMixedArgs, incompatible with AttrOnly).
2425
AttrOnly bool // Enforce using attributes only (overrides NoMixedArgs, incompatible with KVOnly).
26+
NoGlobal string // Enforce not using global loggers ("all" or "default").
2527
ContextOnly bool // Enforce using methods that accept a context.
2628
StaticMsg bool // Enforce using static log messages.
2729
NoRawKeys bool // Enforce using constants instead of raw keys.
@@ -43,11 +45,19 @@ func New(opts *Options) *analysis.Analyzer {
4345
if opts.KVOnly && opts.AttrOnly {
4446
return nil, fmt.Errorf("sloglint: Options.KVOnly and Options.AttrOnly: %w", errIncompatible)
4547
}
48+
49+
switch opts.NoGlobal {
50+
case "", "all", "default":
51+
default:
52+
return nil, fmt.Errorf("sloglint: Options.NoGlobal=%s: %w", opts.NoGlobal, errInvalidValue)
53+
}
54+
4655
switch opts.KeyNamingCase {
4756
case "", snakeCase, kebabCase, camelCase, pascalCase:
4857
default:
4958
return nil, fmt.Errorf("sloglint: Options.KeyNamingCase=%s: %w", opts.KeyNamingCase, errInvalidValue)
5059
}
60+
5161
run(pass, opts)
5262
return nil, nil
5363
},
@@ -60,30 +70,34 @@ var (
6070
)
6171

6272
func flags(opts *Options) flag.FlagSet {
63-
fs := flag.NewFlagSet("sloglint", flag.ContinueOnError)
73+
fset := flag.NewFlagSet("sloglint", flag.ContinueOnError)
6474

6575
boolVar := func(value *bool, name, usage string) {
66-
fs.Func(name, usage, func(s string) error {
76+
fset.Func(name, usage, func(s string) error {
6777
v, err := strconv.ParseBool(s)
6878
*value = v
6979
return err
7080
})
7181
}
7282

83+
strVar := func(value *string, name, usage string) {
84+
fset.Func(name, usage, func(s string) error {
85+
*value = s
86+
return nil
87+
})
88+
}
89+
7390
boolVar(&opts.NoMixedArgs, "no-mixed-args", "enforce not mixing key-value pairs and attributes (default true)")
7491
boolVar(&opts.KVOnly, "kv-only", "enforce using key-value pairs only (overrides -no-mixed-args, incompatible with -attr-only)")
7592
boolVar(&opts.AttrOnly, "attr-only", "enforce using attributes only (overrides -no-mixed-args, incompatible with -kv-only)")
93+
strVar(&opts.NoGlobal, "no-global", "enforce not using global loggers (all|default)")
7694
boolVar(&opts.ContextOnly, "context-only", "enforce using methods that accept a context")
7795
boolVar(&opts.StaticMsg, "static-msg", "enforce using static log messages")
7896
boolVar(&opts.NoRawKeys, "no-raw-keys", "enforce using constants instead of raw keys")
97+
strVar(&opts.KeyNamingCase, "key-naming-case", "enforce a single key naming convention (snake|kebab|camel|pascal)")
7998
boolVar(&opts.ArgsOnSepLines, "args-on-sep-lines", "enforce putting arguments on separate lines")
8099

81-
fs.Func("key-naming-case", "enforce a single key naming convention (snake|kebab|camel|pascal)", func(s string) error {
82-
opts.KeyNamingCase = s
83-
return nil
84-
})
85-
86-
return *fs
100+
return *fset
87101
}
88102

89103
var slogFuncs = map[string]int{ // funcName:argsPos
@@ -139,17 +153,30 @@ func run(pass *analysis.Pass, opts *Options) {
139153
return
140154
}
141155

142-
argsPos, ok := slogFuncs[fn.FullName()]
156+
name := fn.FullName()
157+
argsPos, ok := slogFuncs[name]
143158
if !ok {
144159
return
145160
}
146161

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+
}
172+
147173
if opts.ContextOnly {
148174
typ := pass.TypesInfo.TypeOf(call.Args[0])
149175
if typ != nil && typ.String() != "context.Context" {
150176
pass.Reportf(call.Pos(), "methods without a context should not be used")
151177
}
152178
}
179+
153180
if opts.StaticMsg && !staticMsg(call.Args[argsPos-1]) {
154181
pass.Reportf(call.Pos(), "message should be a string literal or a constant")
155182
}
@@ -189,6 +216,7 @@ func run(pass *analysis.Pass, opts *Options) {
189216
if opts.NoRawKeys && rawKeysUsed(pass.TypesInfo, keys, attrs) {
190217
pass.Reportf(call.Pos(), "raw keys should not be used")
191218
}
219+
192220
if opts.ArgsOnSepLines && argsOnSameLine(pass.Fset, call, keys, attrs) {
193221
pass.Reportf(call.Pos(), "arguments should be put on separate lines")
194222
}
@@ -206,6 +234,19 @@ func run(pass *analysis.Pass, opts *Options) {
206234
})
207235
}
208236

237+
func globalLoggerUsed(info *types.Info, expr ast.Expr) bool {
238+
selector, ok := expr.(*ast.SelectorExpr)
239+
if !ok {
240+
return false
241+
}
242+
ident, ok := selector.X.(*ast.Ident)
243+
if !ok {
244+
return false
245+
}
246+
obj := info.ObjectOf(ident)
247+
return obj.Parent() == obj.Pkg().Scope()
248+
}
249+
209250
func staticMsg(expr ast.Expr) bool {
210251
switch msg := expr.(type) {
211252
case *ast.BasicLit: // e.g. slog.Info("msg")

sloglint_internal_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ func TestOptions(t *testing.T) {
1010
opts Options
1111
err error
1212
}{
13-
"incompatible": {
13+
"KVOnly+AttrOnly: incompatible": {
1414
opts: Options{KVOnly: true, AttrOnly: true},
1515
err: errIncompatible,
1616
},
17-
"invalid value": {
17+
"NoGlobal: invalid value": {
18+
opts: Options{NoGlobal: "-"},
19+
err: errInvalidValue,
20+
},
21+
"KeyNamingCase: invalid value": {
1822
opts: Options{KeyNamingCase: "-"},
1923
err: errInvalidValue,
2024
},

sloglint_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ func TestAnalyzer(t *testing.T) {
2525
analysistest.Run(t, testdata, analyzer, "attr_only")
2626
})
2727

28+
t.Run("no global (all)", func(t *testing.T) {
29+
analyzer := sloglint.New(&sloglint.Options{NoGlobal: "all"})
30+
analysistest.Run(t, testdata, analyzer, "no_global_all")
31+
})
32+
33+
t.Run("no global (default)", func(t *testing.T) {
34+
analyzer := sloglint.New(&sloglint.Options{NoGlobal: "default"})
35+
analysistest.Run(t, testdata, analyzer, "no_global_default")
36+
})
37+
2838
t.Run("context only", func(t *testing.T) {
2939
analyzer := sloglint.New(&sloglint.Options{ContextOnly: true})
3040
analysistest.Run(t, testdata, analyzer, "context_only")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package no_global_all
2+
3+
import "log/slog"
4+
5+
var logger = slog.New(nil)
6+
7+
func tests() {
8+
slog.Info("msg") // want `global logger should not be used`
9+
logger.Info("msg") // want `global logger should not be used`
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package no_global_default
2+
3+
import "log/slog"
4+
5+
var logger = slog.New(nil)
6+
7+
func tests() {
8+
slog.Info("msg") // want `default logger should not be used`
9+
logger.Info("msg")
10+
}

0 commit comments

Comments
 (0)