Skip to content

Commit 2a9398d

Browse files
committed
go/analysis/passes/slog: do not report multiple incorrect keys
Stops the slog analyzer from reporting multiple issues for the same call. Adds additional unit tests. Change-Id: I6eec033a5c88fafc792a1d7cc7eb50986bf260a6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/487475 Commit-Queue: Tim King <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Tim King <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 4baa3dc commit 2a9398d

File tree

5 files changed

+135
-27
lines changed

5 files changed

+135
-27
lines changed

go/analysis/passes/internal/analysisutil/util.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,12 @@ func Imports(pkg *types.Package, path string) bool {
118118
}
119119
return false
120120
}
121+
122+
// IsNamed reports whether t is exactly a named type in a package with a given path.
123+
func IsNamed(t types.Type, path, name string) bool {
124+
if n, ok := t.(*types.Named); ok {
125+
obj := n.Obj()
126+
return obj.Pkg().Path() == path && obj.Name() == name
127+
}
128+
return false
129+
}

go/analysis/passes/slog/slog.go

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ func run(pass *analysis.Pass) (any, error) {
6666
// Not a slog function that takes key-value pairs.
6767
return
6868
}
69+
if isMethodExpr(pass.TypesInfo, call) {
70+
// Call is to a method value. Skip the first argument.
71+
skipArgs++
72+
}
6973
if len(call.Args) <= skipArgs {
7074
// Too few args; perhaps there are no k-v pairs.
7175
return
@@ -74,7 +78,7 @@ func run(pass *analysis.Pass) (any, error) {
7478
// Check this call.
7579
// The first position should hold a key or Attr.
7680
pos := key
77-
sawUnknown := false
81+
var unknownArg ast.Expr // nil or the last unknown argument
7882
for _, arg := range call.Args[skipArgs:] {
7983
t := pass.TypesInfo.Types[arg].Type
8084
switch pos {
@@ -86,17 +90,19 @@ func run(pass *analysis.Pass) (any, error) {
8690
case isAttr(t):
8791
pos = key
8892
case types.IsInterface(t):
89-
// We don't know if this arg is a string or an Attr, so we don't know what to expect next.
90-
// (We could see if one of interface's methods isn't a method of Attr, and thus know
91-
// for sure that this type is definitely not a string or Attr, but it doesn't seem
92-
// worth the effort for such an unlikely case.)
93+
// As we do not do dataflow, we do not know what the dynamic type is.
94+
// It could be a string or an Attr so we don't know what to expect next.
9395
pos = unknown
9496
default:
95-
// Definitely not a key.
96-
pass.ReportRangef(call, "%s arg %q should be a string or a slog.Attr (possible missing key or value)",
97-
shortName(fn), analysisutil.Format(pass.Fset, arg))
98-
// Assume this was supposed to be a value, and expect a key next.
99-
pos = key
97+
if unknownArg == nil {
98+
pass.ReportRangef(arg, "%s arg %q should be a string or a slog.Attr (possible missing key or value)",
99+
shortName(fn), analysisutil.Format(pass.Fset, arg))
100+
} else {
101+
pass.ReportRangef(arg, "%s arg %q should probably be a string or a slog.Attr (previous arg %q cannot be a key)",
102+
shortName(fn), analysisutil.Format(pass.Fset, arg), analysisutil.Format(pass.Fset, unknownArg))
103+
}
104+
// Stop here so we report at most one missing key per call.
105+
return
100106
}
101107

102108
case value:
@@ -105,39 +111,35 @@ func run(pass *analysis.Pass) (any, error) {
105111
pos = key
106112

107113
case unknown:
108-
// We don't know anything about this position, but all hope is not lost.
114+
// Once we encounter an unknown position, we can never be
115+
// sure if a problem later or at the end of the call is due to a
116+
// missing final value, or a non-key in key position.
117+
// In both cases, unknownArg != nil.
118+
unknownArg = arg
119+
120+
// We don't know what is expected about this position, but all hope is not lost.
109121
if t != stringType && !isAttr(t) && !types.IsInterface(t) {
110122
// This argument is definitely not a key.
111123
//
112-
// The previous argument could have been a key, in which case this is the
124+
// unknownArg cannot have been a key, in which case this is the
113125
// corresponding value, and the next position should hold another key.
114-
// We will assume that.
115126
pos = key
116-
// Another possibility: the previous argument was an Attr, and this is
117-
// a value incorrectly placed in a key position.
118-
// If we assumed this case instead, we might produce a false positive
119-
// (since the first case might actually hold).
120-
121-
// Once we encounter an unknown position, we can never be
122-
// sure if a problem at the end of the call is due to a
123-
// missing final value, or a non-key in key position.
124-
sawUnknown = true
125127
}
126128
}
127129
}
128130
if pos == value {
129-
if sawUnknown {
130-
pass.ReportRangef(call, "call to %s has a missing or misplaced value", shortName(fn))
131-
} else {
131+
if unknownArg == nil {
132132
pass.ReportRangef(call, "call to %s missing a final value", shortName(fn))
133+
} else {
134+
pass.ReportRangef(call, "call to %s has a missing or misplaced value", shortName(fn))
133135
}
134136
}
135137
})
136138
return nil, nil
137139
}
138140

139141
func isAttr(t types.Type) bool {
140-
return t.String() == "log/slog.Attr"
142+
return analysisutil.IsNamed(t, "log/slog", "Attr")
141143
}
142144

143145
// shortName returns a name for the function that is shorter than FullName.
@@ -216,3 +218,13 @@ var slogOutputFuncs = map[string]int{
216218
"ErrorCtx": 2,
217219
"Log": 3,
218220
}
221+
222+
// isMethodExpr reports whether a call is to a MethodExpr.
223+
func isMethodExpr(info *types.Info, c *ast.CallExpr) bool {
224+
s, ok := c.Fun.(*ast.SelectorExpr)
225+
if !ok {
226+
return false
227+
}
228+
sel := info.Selections[s]
229+
return sel != nil && sel.Kind() == types.MethodExpr
230+
}

go/analysis/passes/slog/slog_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ import (
1515
func Test(t *testing.T) {
1616
testenv.NeedsGo1Point(t, 21)
1717
testdata := analysistest.TestData()
18-
analysistest.Run(t, testdata, slog.Analyzer, "a")
18+
analysistest.Run(t, testdata, slog.Analyzer, "a", "b")
1919
}

go/analysis/passes/slog/testdata/src/a/a.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package a
1010

1111
import (
12+
"context"
1213
"fmt"
1314
"log/slog"
1415
)
@@ -25,11 +26,18 @@ func F() {
2526
// Valid calls.
2627
slog.Info("msg")
2728
slog.Info("msg", "a", 1)
29+
slog.Info("", "a", 1, "b", "two")
2830
l.Debug("msg", "a", 1)
2931
l.With("a", 1)
32+
slog.Warn("msg", slog.Int("a", 1))
3033
slog.Warn("msg", slog.Int("a", 1), "k", 2)
3134
l.WarnCtx(nil, "msg", "a", 1, slog.Int("b", 2), slog.Int("c", 3), "d", 4)
35+
l.DebugCtx(nil, "msg", "a", 1, slog.Int("b", 2), slog.Int("c", 3), "d", 4, slog.Int("e", 5))
3236
r.Add("a", 1, "b", 2)
37+
(*slog.Logger).Debug(l, "msg", "a", 1, "b", 2)
38+
39+
var key string
40+
r.Add(key, 1)
3341

3442
// bad
3543
slog.Info("msg", 1) // want `slog.Info arg "1" should be a string or a slog.Attr`
@@ -40,12 +48,24 @@ func F() {
4048
r.Add("K", "v", "k") // want `call to slog.Record.Add missing a final value`
4149
l.With("a", "b", 2) // want `slog.Logger.With arg "2" should be a string or a slog.Attr`
4250

51+
// Report the first problem if there are multiple bad keys.
52+
slog.Debug("msg", "a", 1, 2, 3, 4) // want `slog.Debug arg "2" should be a string or a slog.Attr`
53+
slog.Debug("msg", "a", 1, 2, 3, 4) // want `slog.Debug arg "2" should be a string or a slog.Attr`
54+
4355
slog.Log(nil, slog.LevelWarn, "msg", "a", "b", 2) // want `slog.Log arg "2" should be a string or a slog.Attr`
4456

57+
// Test method expression call.
58+
(*slog.Logger).Debug(l, "msg", "a", 1, 2, 3) // want `slog.Logger.Debug arg "2" should be a string or a slog.Attr`
59+
4560
// Skip calls with spread args.
4661
var args []any
4762
slog.Info("msg", args...)
4863

64+
// Report keys that are statically not exactly "string".
65+
type MyString string
66+
myKey := MyString("a") // any(x) looks like <MyString, "a">.
67+
slog.Info("", myKey, 1) // want `slog.Info arg "myKey" should be a string or a slog.Attr`
68+
4969
// The variadic part of all the calls below begins with an argument of
5070
// static type any, followed by an integer.
5171
// Even though the we don't know the dynamic type of the first arg, and thus
@@ -74,4 +94,56 @@ func F() {
7494
// Another invalid call we can't detect. Here the first argument is wrong.
7595
a = 1
7696
slog.Info("msg", a, 7, "b", 5)
97+
98+
// We can detect the first case as the type of key is UntypedNil,
99+
// e.g. not yet assigned to any and not yet an interface.
100+
// We cannot detect the second.
101+
slog.Debug("msg", nil, 2) // want `slog.Debug arg "nil" should be a string or a slog.Attr`
102+
slog.Debug("msg", any(nil), 2)
103+
104+
// Recovery from unknown value.
105+
slog.Debug("msg", any(nil), "a")
106+
slog.Debug("msg", any(nil), "a", 2)
107+
slog.Debug("msg", any(nil), "a", 2, "b") // want `call to slog.Debug has a missing or misplaced value`
108+
slog.Debug("msg", any(nil), 2, 3, 4) // want "slog.Debug arg \\\"3\\\" should probably be a string or a slog.Attr \\(previous arg \\\"2\\\" cannot be a key\\)"
109+
}
110+
111+
func All() {
112+
// Test all functions and methods at least once.
113+
var (
114+
l *slog.Logger
115+
r slog.Record
116+
ctx context.Context
117+
)
118+
slog.Debug("msg", 1, 2) // want `slog.Debug arg "1" should be a string or a slog.Attr`
119+
slog.Error("msg", 1, 2) // want `slog.Error arg "1" should be a string or a slog.Attr`
120+
slog.Info("msg", 1, 2) // want `slog.Info arg "1" should be a string or a slog.Attr`
121+
slog.Warn("msg", 1, 2) // want `slog.Warn arg "1" should be a string or a slog.Attr`
122+
123+
slog.DebugCtx(ctx, "msg", 1, 2) // want `slog.DebugCtx arg "1" should be a string or a slog.Attr`
124+
slog.ErrorCtx(ctx, "msg", 1, 2) // want `slog.ErrorCtx arg "1" should be a string or a slog.Attr`
125+
slog.InfoCtx(ctx, "msg", 1, 2) // want `slog.InfoCtx arg "1" should be a string or a slog.Attr`
126+
slog.WarnCtx(ctx, "msg", 1, 2) // want `slog.WarnCtx arg "1" should be a string or a slog.Attr`
127+
128+
slog.Log(ctx, slog.LevelDebug, "msg", 1, 2) // want `slog.Log arg "1" should be a string or a slog.Attr`
129+
130+
l.Debug("msg", 1, 2) // want `slog.Logger.Debug arg "1" should be a string or a slog.Attr`
131+
l.Error("msg", 1, 2) // want `slog.Logger.Error arg "1" should be a string or a slog.Attr`
132+
l.Info("msg", 1, 2) // want `slog.Logger.Info arg "1" should be a string or a slog.Attr`
133+
l.Warn("msg", 1, 2) // want `slog.Logger.Warn arg "1" should be a string or a slog.Attr`
134+
135+
l.DebugCtx(ctx, "msg", 1, 2) // want `slog.Logger.DebugCtx arg "1" should be a string or a slog.Attr`
136+
l.ErrorCtx(ctx, "msg", 1, 2) // want `slog.Logger.ErrorCtx arg "1" should be a string or a slog.Attr`
137+
l.InfoCtx(ctx, "msg", 1, 2) // want `slog.Logger.InfoCtx arg "1" should be a string or a slog.Attr`
138+
l.WarnCtx(ctx, "msg", 1, 2) // want `slog.Logger.WarnCtx arg "1" should be a string or a slog.Attr`
139+
140+
l.Log(ctx, slog.LevelDebug, "msg", 1, 2) // want `slog.Logger.Log arg "1" should be a string or a slog.Attr`
141+
142+
_ = l.With(1, 2) // want `slog.Logger.With arg "1" should be a string or a slog.Attr`
143+
144+
r.Add(1, 2) // want `slog.Record.Add arg "1" should be a string or a slog.Attr`
145+
77146
}
147+
148+
// Used in tests by package b.
149+
var MyLogger = slog.Default()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file contains tests for the slog checker.
6+
7+
//go:build go1.21
8+
9+
package b
10+
11+
import "a"
12+
13+
func Imported() {
14+
_ = a.MyLogger.With("a", 1, 2, 3) // want `slog.Logger.With arg "2" should be a string or a slog.Attr`
15+
}

0 commit comments

Comments
 (0)