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