Skip to content

Commit 6b7f937

Browse files
tmzaneldez
andauthored
feat: suggest to use slog.DiscardHandler (#81)
Co-authored-by: Ludovic Fernandez <[email protected]>
1 parent eae36ea commit 6b7f937

File tree

6 files changed

+105
-120
lines changed

6 files changed

+105
-120
lines changed

.github/workflows/checks.yml

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches: [ main ]
66
pull_request:
7-
branches: [ main ]
87
workflow_dispatch:
98

109
jobs:

sloglint.go

+32-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"go/ast"
99
"go/token"
1010
"go/types"
11+
"go/version"
1112
"slices"
1213
"strconv"
1314
"strings"
@@ -33,6 +34,8 @@ type Options struct {
3334
KeyNamingCase string // Enforce key naming convention ("snake", "kebab", "camel", or "pascal").
3435
ForbiddenKeys []string // Enforce not using specific keys.
3536
ArgsOnSepLines bool // Enforce putting arguments on separate lines.
37+
38+
go124 bool
3639
}
3740

3841
// New creates a new sloglint analyzer.
@@ -75,6 +78,10 @@ func New(opts *Options) *analysis.Analyzer {
7578
return nil, fmt.Errorf("sloglint: Options.KeyNamingCase=%s: %w", opts.KeyNamingCase, errInvalidValue)
7679
}
7780

81+
if version.Compare("go"+pass.Module.GoVersion, "go1.24") >= 0 {
82+
opts.go124 = true
83+
}
84+
7885
run(pass, opts)
7986
return nil, nil
8087
},
@@ -206,6 +213,27 @@ func visit(pass *analysis.Pass, opts *Options, node ast.Node, stack []ast.Node)
206213
}
207214

208215
name := fn.FullName()
216+
217+
if opts.go124 && (name == "log/slog.NewTextHandler" || name == "log/slog.NewJSONHandler") {
218+
if sel, ok := call.Args[0].(*ast.SelectorExpr); ok {
219+
if obj := pass.TypesInfo.ObjectOf(sel.Sel); obj != nil {
220+
if obj.Pkg().Name() == "io" && obj.Name() == "Discard" {
221+
pass.Report(analysis.Diagnostic{
222+
Pos: call.Pos(),
223+
Message: "use slog.DiscardHandler instead",
224+
SuggestedFixes: []analysis.SuggestedFix{{
225+
TextEdits: []analysis.TextEdit{{
226+
Pos: call.Pos(),
227+
End: call.End(),
228+
NewText: []byte("slog.DiscardHandler"),
229+
}},
230+
}},
231+
})
232+
}
233+
}
234+
}
235+
}
236+
209237
funcInfo, ok := slogFuncs[name]
210238
if !ok {
211239
return
@@ -293,8 +321,8 @@ func visit(pass *analysis.Pass, opts *Options, node ast.Node, stack []ast.Node)
293321

294322
if opts.NoRawKeys {
295323
forEachKey(pass.TypesInfo, keys, attrs, func(key ast.Expr) {
296-
if selector, ok := key.(*ast.SelectorExpr); ok {
297-
key = selector.Sel // the key is defined in another package, e.g. pkg.ConstKey.
324+
if sel, ok := key.(*ast.SelectorExpr); ok {
325+
key = sel.Sel // the key is defined in another package, e.g. pkg.ConstKey.
298326
}
299327
isConst := false
300328
if ident, ok := key.(*ast.Ident); ok {
@@ -343,11 +371,11 @@ func visit(pass *analysis.Pass, opts *Options, node ast.Node, stack []ast.Node)
343371
}
344372

345373
func isGlobalLoggerUsed(info *types.Info, call ast.Expr) bool {
346-
selector, ok := call.(*ast.SelectorExpr)
374+
sel, ok := call.(*ast.SelectorExpr)
347375
if !ok {
348376
return false
349377
}
350-
ident, ok := selector.X.(*ast.Ident)
378+
ident, ok := sel.X.(*ast.Ident)
351379
if !ok {
352380
return false
353381
}

sloglint_internal_test.go

-43
This file was deleted.

sloglint_test.go

+51-72
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,61 @@
1-
package sloglint_test
1+
package sloglint
22

33
import (
4+
"errors"
45
"testing"
56

6-
"go-simpler.org/sloglint"
77
"golang.org/x/tools/go/analysis/analysistest"
88
)
99

1010
func TestAnalyzer(t *testing.T) {
11-
testdata := analysistest.TestData()
12-
13-
t.Run("no mixed arguments", func(t *testing.T) {
14-
analyzer := sloglint.New(nil)
15-
analysistest.Run(t, testdata, analyzer, "no_mixed_args")
16-
})
17-
18-
t.Run("key-value pairs only", func(t *testing.T) {
19-
analyzer := sloglint.New(&sloglint.Options{KVOnly: true})
20-
analysistest.Run(t, testdata, analyzer, "kv_only")
21-
})
22-
23-
t.Run("attributes only", func(t *testing.T) {
24-
analyzer := sloglint.New(&sloglint.Options{AttrOnly: true})
25-
analysistest.Run(t, testdata, analyzer, "attr_only")
26-
})
27-
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-
38-
t.Run("context only (all)", func(t *testing.T) {
39-
analyzer := sloglint.New(&sloglint.Options{ContextOnly: "all"})
40-
analysistest.Run(t, testdata, analyzer, "context_only_all")
41-
})
42-
43-
t.Run("context only (scope)", func(t *testing.T) {
44-
analyzer := sloglint.New(&sloglint.Options{ContextOnly: "scope"})
45-
analysistest.Run(t, testdata, analyzer, "context_only_scope")
46-
})
47-
48-
t.Run("static message", func(t *testing.T) {
49-
analyzer := sloglint.New(&sloglint.Options{StaticMsg: true})
50-
analysistest.Run(t, testdata, analyzer, "static_msg")
51-
})
52-
53-
t.Run("no raw keys", func(t *testing.T) {
54-
analyzer := sloglint.New(&sloglint.Options{NoRawKeys: true})
55-
analysistest.Run(t, testdata, analyzer, "no_raw_keys")
56-
})
57-
58-
t.Run("key naming case", func(t *testing.T) {
59-
analyzer := sloglint.New(&sloglint.Options{KeyNamingCase: "snake"})
60-
analysistest.Run(t, testdata, analyzer, "key_naming_case")
61-
})
62-
63-
t.Run("arguments on separate lines", func(t *testing.T) {
64-
analyzer := sloglint.New(&sloglint.Options{ArgsOnSepLines: true})
65-
analysistest.Run(t, testdata, analyzer, "args_on_sep_lines")
66-
})
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-
})
72-
73-
t.Run("message style (lowercased)", func(t *testing.T) {
74-
analyzer := sloglint.New(&sloglint.Options{MsgStyle: "lowercased"})
75-
analysistest.Run(t, testdata, analyzer, "msg_style_lowercased")
76-
})
11+
tests := map[string]struct {
12+
opts Options
13+
dir string
14+
}{
15+
"no mixed arguments": {Options{NoMixedArgs: true}, "no_mixed_args"},
16+
"key-value pairs only": {Options{KVOnly: true}, "kv_only"},
17+
"attributes only": {Options{AttrOnly: true}, "attr_only"},
18+
"no global (all)": {Options{NoGlobal: "all"}, "no_global_all"},
19+
"no global (default)": {Options{NoGlobal: "default"}, "no_global_default"},
20+
"context only (all)": {Options{ContextOnly: "all"}, "context_only_all"},
21+
"context only (scope)": {Options{ContextOnly: "scope"}, "context_only_scope"},
22+
"static message": {Options{StaticMsg: true}, "static_msg"},
23+
"no raw keys": {Options{NoRawKeys: true}, "no_raw_keys"},
24+
"key naming case": {Options{KeyNamingCase: "snake"}, "key_naming_case"},
25+
"arguments on separate lines": {Options{ArgsOnSepLines: true}, "args_on_sep_lines"},
26+
"forbidden keys": {Options{ForbiddenKeys: []string{"foo_bar"}}, "forbidden_keys"},
27+
"message style (lowercased)": {Options{MsgStyle: "lowercased"}, "msg_style_lowercased"},
28+
"message style (capitalized)": {Options{MsgStyle: "capitalized"}, "msg_style_capitalized"},
29+
"slog.DiscardHandler": {Options{go124: true}, "discard_handler"},
30+
}
31+
32+
for name, tt := range tests {
33+
t.Run(name, func(t *testing.T) {
34+
analyzer := New(&tt.opts)
35+
testdata := analysistest.TestData()
36+
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, tt.dir)
37+
})
38+
}
39+
}
7740

78-
t.Run("message style (capitalized)", func(t *testing.T) {
79-
analyzer := sloglint.New(&sloglint.Options{MsgStyle: "capitalized"})
80-
analysistest.Run(t, testdata, analyzer, "msg_style_capitalized")
81-
})
41+
func TestOptions(t *testing.T) {
42+
tests := map[string]struct {
43+
opts Options
44+
err error
45+
}{
46+
"KVOnly+AttrOnly: incompatible": {Options{KVOnly: true, AttrOnly: true}, errIncompatible},
47+
"NoGlobal: invalid value": {Options{NoGlobal: "-"}, errInvalidValue},
48+
"ContextOnly: invalid value": {Options{ContextOnly: "-"}, errInvalidValue},
49+
"MsgStyle: invalid value": {Options{MsgStyle: "-"}, errInvalidValue},
50+
"KeyNamingCase: invalid value": {Options{KeyNamingCase: "-"}, errInvalidValue},
51+
}
52+
53+
for name, test := range tests {
54+
t.Run(name, func(t *testing.T) {
55+
analyzer := New(&test.opts)
56+
if _, err := analyzer.Run(nil); !errors.Is(err, test.err) {
57+
t.Errorf("errors.Is() mismatch\ngot: %v\nwant: %v", err, test.err)
58+
}
59+
})
60+
}
8261
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package discard_handler
2+
3+
import (
4+
"io"
5+
"log/slog"
6+
)
7+
8+
func _() {
9+
_ = slog.NewTextHandler(io.Discard, nil) // want `use slog.DiscardHandler instead`
10+
_ = slog.NewJSONHandler(io.Discard, nil) // want `use slog.DiscardHandler instead`
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package discard_handler
2+
3+
import (
4+
"io"
5+
"log/slog"
6+
)
7+
8+
func _() {
9+
_ = slog.DiscardHandler // want `use slog.DiscardHandler instead`
10+
_ = slog.DiscardHandler // want `use slog.DiscardHandler instead`
11+
}

0 commit comments

Comments
 (0)