1
2
3
4
5
6
7
8
9 package slog
10
11 import (
12 _ "embed"
13 "fmt"
14 "go/ast"
15 "go/token"
16 "go/types"
17
18 "golang.org/x/tools/go/analysis"
19 "golang.org/x/tools/go/analysis/passes/inspect"
20 "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
21 "golang.org/x/tools/go/ast/inspector"
22 "golang.org/x/tools/go/types/typeutil"
23 "golang.org/x/tools/internal/typesinternal"
24 )
25
26
27 var doc string
28
29 var Analyzer = &analysis.Analyzer{
30 Name: "slog",
31 Doc: analysisutil.MustExtractDoc(doc, "slog"),
32 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/slog",
33 Requires: []*analysis.Analyzer{inspect.Analyzer},
34 Run: run,
35 }
36
37 var stringType = types.Universe.Lookup("string").Type()
38
39
40 type position int
41
42 const (
43
44 key position = iota
45
46 value
47
48 unknown
49 )
50
51 func run(pass *analysis.Pass) (any, error) {
52 var attrType types.Type
53 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
54 nodeFilter := []ast.Node{
55 (*ast.CallExpr)(nil),
56 }
57 inspect.Preorder(nodeFilter, func(node ast.Node) {
58 call := node.(*ast.CallExpr)
59 fn := typeutil.StaticCallee(pass.TypesInfo, call)
60 if fn == nil {
61 return
62 }
63 if call.Ellipsis != token.NoPos {
64 return
65 }
66 skipArgs, ok := kvFuncSkipArgs(fn)
67 if !ok {
68
69 return
70 }
71
72 if attrType == nil {
73 attrType = fn.Pkg().Scope().Lookup("Attr").Type()
74 }
75
76 if isMethodExpr(pass.TypesInfo, call) {
77
78 skipArgs++
79 }
80 if len(call.Args) <= skipArgs {
81
82 return
83 }
84
85
86
87 pos := key
88 var unknownArg ast.Expr
89 for _, arg := range call.Args[skipArgs:] {
90 t := pass.TypesInfo.Types[arg].Type
91 switch pos {
92 case key:
93
94 switch {
95 case t == stringType:
96 pos = value
97 case isAttr(t):
98 pos = key
99 case types.IsInterface(t):
100
101
102 if types.AssignableTo(stringType, t) {
103
104
105 pos = unknown
106 continue
107 } else if attrType != nil && types.AssignableTo(attrType, t) {
108
109 pos = key
110 continue
111 }
112
113 fallthrough
114 default:
115 if unknownArg == nil {
116 pass.ReportRangef(arg, "%s arg %q should be a string or a slog.Attr (possible missing key or value)",
117 shortName(fn), analysisutil.Format(pass.Fset, arg))
118 } else {
119 pass.ReportRangef(arg, "%s arg %q should probably be a string or a slog.Attr (previous arg %q cannot be a key)",
120 shortName(fn), analysisutil.Format(pass.Fset, arg), analysisutil.Format(pass.Fset, unknownArg))
121 }
122
123 return
124 }
125
126 case value:
127
128
129 pos = key
130
131 case unknown:
132
133
134
135
136 unknownArg = arg
137
138
139 if t != stringType && !isAttr(t) && !types.IsInterface(t) {
140
141
142
143
144 pos = key
145 }
146 }
147 }
148 if pos == value {
149 if unknownArg == nil {
150 pass.ReportRangef(call, "call to %s missing a final value", shortName(fn))
151 } else {
152 pass.ReportRangef(call, "call to %s has a missing or misplaced value", shortName(fn))
153 }
154 }
155 })
156 return nil, nil
157 }
158
159 func isAttr(t types.Type) bool {
160 return analysisutil.IsNamedType(t, "log/slog", "Attr")
161 }
162
163
164
165
166
167
168 func shortName(fn *types.Func) string {
169 var r string
170 if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
171 if _, named := typesinternal.ReceiverNamed(recv); named != nil {
172 r = named.Obj().Name()
173 } else {
174 r = recv.Type().String()
175 }
176 r += "."
177 }
178 return fmt.Sprintf("%s.%s%s", fn.Pkg().Name(), r, fn.Name())
179 }
180
181
182
183
184
185 func kvFuncSkipArgs(fn *types.Func) (int, bool) {
186 if pkg := fn.Pkg(); pkg == nil || pkg.Path() != "log/slog" {
187 return 0, false
188 }
189 var recvName string
190 if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
191 _, named := typesinternal.ReceiverNamed(recv)
192 if named == nil {
193 return 0, false
194 }
195 recvName = named.Obj().Name()
196 }
197 skip, ok := kvFuncs[recvName][fn.Name()]
198 return skip, ok
199 }
200
201
202
203
204
205 var kvFuncs = map[string]map[string]int{
206 "": map[string]int{
207 "Debug": 1,
208 "Info": 1,
209 "Warn": 1,
210 "Error": 1,
211 "DebugContext": 2,
212 "InfoContext": 2,
213 "WarnContext": 2,
214 "ErrorContext": 2,
215 "Log": 3,
216 "Group": 1,
217 },
218 "Logger": map[string]int{
219 "Debug": 1,
220 "Info": 1,
221 "Warn": 1,
222 "Error": 1,
223 "DebugContext": 2,
224 "InfoContext": 2,
225 "WarnContext": 2,
226 "ErrorContext": 2,
227 "Log": 3,
228 "With": 0,
229 },
230 "Record": map[string]int{
231 "Add": 0,
232 },
233 }
234
235
236 func isMethodExpr(info *types.Info, c *ast.CallExpr) bool {
237 s, ok := c.Fun.(*ast.SelectorExpr)
238 if !ok {
239 return false
240 }
241 sel := info.Selections[s]
242 return sel != nil && sel.Kind() == types.MethodExpr
243 }
244
View as plain text