golang.org/x/tools@v0.21.0/go/analysis/passes/slog/slog.go (about) 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 // TODO(jba) deduce which functions wrap the log/slog functions, and use the 6 // fact mechanism to propagate this information, so we can provide diagnostics 7 // for user-supplied wrappers. 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 //go:embed doc.go 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 // A position describes what is expected to appear in an argument position. 40 type position int 41 42 const ( 43 // key is an argument position that should hold a string key or an Attr. 44 key position = iota 45 // value is an argument position that should hold a value. 46 value 47 // unknown represents that we do not know if position should hold a key or a value. 48 unknown 49 ) 50 51 func run(pass *analysis.Pass) (any, error) { 52 var attrType types.Type // The type of slog.Attr 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 // not a static call 62 } 63 if call.Ellipsis != token.NoPos { 64 return // skip calls with "..." args 65 } 66 skipArgs, ok := kvFuncSkipArgs(fn) 67 if !ok { 68 // Not a slog function that takes key-value pairs. 69 return 70 } 71 // Here we know that fn.Pkg() is "log/slog". 72 if attrType == nil { 73 attrType = fn.Pkg().Scope().Lookup("Attr").Type() 74 } 75 76 if isMethodExpr(pass.TypesInfo, call) { 77 // Call is to a method value. Skip the first argument. 78 skipArgs++ 79 } 80 if len(call.Args) <= skipArgs { 81 // Too few args; perhaps there are no k-v pairs. 82 return 83 } 84 85 // Check this call. 86 // The first position should hold a key or Attr. 87 pos := key 88 var unknownArg ast.Expr // nil or the last unknown argument 89 for _, arg := range call.Args[skipArgs:] { 90 t := pass.TypesInfo.Types[arg].Type 91 switch pos { 92 case key: 93 // Expect a string or Attr. 94 switch { 95 case t == stringType: 96 pos = value 97 case isAttr(t): 98 pos = key 99 case types.IsInterface(t): 100 // As we do not do dataflow, we do not know what the dynamic type is. 101 // But we might be able to learn enough to make a decision. 102 if types.AssignableTo(stringType, t) { 103 // t must be an empty interface. So it can also be an Attr. 104 // We don't know enough to make an assumption. 105 pos = unknown 106 continue 107 } else if attrType != nil && types.AssignableTo(attrType, t) { 108 // Assume it is an Attr. 109 pos = key 110 continue 111 } 112 // Can't be either a string or Attr. Definitely an error. 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 // Stop here so we report at most one missing key per call. 123 return 124 } 125 126 case value: 127 // Anything can appear in this position. 128 // The next position should be a key. 129 pos = key 130 131 case unknown: 132 // Once we encounter an unknown position, we can never be 133 // sure if a problem later or at the end of the call is due to a 134 // missing final value, or a non-key in key position. 135 // In both cases, unknownArg != nil. 136 unknownArg = arg 137 138 // We don't know what is expected about this position, but all hope is not lost. 139 if t != stringType && !isAttr(t) && !types.IsInterface(t) { 140 // This argument is definitely not a key. 141 // 142 // unknownArg cannot have been a key, in which case this is the 143 // corresponding value, and the next position should hold another key. 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 // shortName returns a name for the function that is shorter than FullName. 164 // Examples: 165 // 166 // "slog.Info" (instead of "log/slog.Info") 167 // "slog.Logger.With" (instead of "(*log/slog.Logger).With") 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() // anon struct/interface 175 } 176 r += "." 177 } 178 return fmt.Sprintf("%s.%s%s", fn.Pkg().Name(), r, fn.Name()) 179 } 180 181 // If fn is a slog function that has a ...any parameter for key-value pairs, 182 // kvFuncSkipArgs returns the number of arguments to skip over to reach the 183 // corresponding arguments, and true. 184 // Otherwise it returns (0, false). 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 // by default a slog package function 190 if recv := fn.Type().(*types.Signature).Recv(); recv != nil { 191 _, named := typesinternal.ReceiverNamed(recv) 192 if named == nil { 193 return 0, false // anon struct/interface 194 } 195 recvName = named.Obj().Name() 196 } 197 skip, ok := kvFuncs[recvName][fn.Name()] 198 return skip, ok 199 } 200 201 // The names of functions and methods in log/slog that take 202 // ...any for key-value pairs, mapped to the number of initial args to skip in 203 // order to get to the ones that match the ...any parameter. 204 // The first key is the dereferenced receiver type name, or "" for a function. 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 // isMethodExpr reports whether a call is to a MethodExpr. 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 }