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