1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
|
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(jba) deduce which functions wrap the log/slog functions, and use the
// fact mechanism to propagate this information, so we can provide diagnostics
// for user-supplied wrappers.
package slog
import (
_ "embed"
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
var doc string
var Analyzer = &analysis.Analyzer{
Name: "slog",
Doc: analysisutil.MustExtractDoc(doc, "slog"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/slog",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
var stringType = types.Universe.Lookup("string").Type()
// A position describes what is expected to appear in an argument position.
type position int
const (
// key is an argument position that should hold a string key or an Attr.
key position = iota
// value is an argument position that should hold a value.
value
// unknown represents that we do not know if position should hold a key or a value.
unknown
)
func run(pass *analysis.Pass) (any, error) {
var attrType types.Type // The type of slog.Attr
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.CallExpr)(nil),
}
inspect.Preorder(nodeFilter, func(node ast.Node) {
call := node.(*ast.CallExpr)
fn := typeutil.StaticCallee(pass.TypesInfo, call)
if fn == nil {
return // not a static call
}
if call.Ellipsis != token.NoPos {
return // skip calls with "..." args
}
skipArgs, ok := kvFuncSkipArgs(fn)
if !ok {
// Not a slog function that takes key-value pairs.
return
}
// Here we know that fn.Pkg() is "log/slog".
if attrType == nil {
attrType = fn.Pkg().Scope().Lookup("Attr").Type()
}
if isMethodExpr(pass.TypesInfo, call) {
// Call is to a method value. Skip the first argument.
skipArgs++
}
if len(call.Args) <= skipArgs {
// Too few args; perhaps there are no k-v pairs.
return
}
// Check this call.
// The first position should hold a key or Attr.
pos := key
var unknownArg ast.Expr // nil or the last unknown argument
for _, arg := range call.Args[skipArgs:] {
t := pass.TypesInfo.Types[arg].Type
switch pos {
case key:
// Expect a string or Attr.
switch {
case t == stringType:
pos = value
case isAttr(t):
pos = key
case types.IsInterface(t):
// As we do not do dataflow, we do not know what the dynamic type is.
// But we might be able to learn enough to make a decision.
if types.AssignableTo(stringType, t) {
// t must be an empty interface. So it can also be an Attr.
// We don't know enough to make an assumption.
pos = unknown
continue
} else if attrType != nil && types.AssignableTo(attrType, t) {
// Assume it is an Attr.
pos = key
continue
}
// Can't be either a string or Attr. Definitely an error.
fallthrough
default:
if unknownArg == nil {
pass.ReportRangef(arg, "%s arg %q should be a string or a slog.Attr (possible missing key or value)",
shortName(fn), analysisutil.Format(pass.Fset, arg))
} else {
pass.ReportRangef(arg, "%s arg %q should probably be a string or a slog.Attr (previous arg %q cannot be a key)",
shortName(fn), analysisutil.Format(pass.Fset, arg), analysisutil.Format(pass.Fset, unknownArg))
}
// Stop here so we report at most one missing key per call.
return
}
case value:
// Anything can appear in this position.
// The next position should be a key.
pos = key
case unknown:
// Once we encounter an unknown position, we can never be
// sure if a problem later or at the end of the call is due to a
// missing final value, or a non-key in key position.
// In both cases, unknownArg != nil.
unknownArg = arg
// We don't know what is expected about this position, but all hope is not lost.
if t != stringType && !isAttr(t) && !types.IsInterface(t) {
// This argument is definitely not a key.
//
// unknownArg cannot have been a key, in which case this is the
// corresponding value, and the next position should hold another key.
pos = key
}
}
}
if pos == value {
if unknownArg == nil {
pass.ReportRangef(call, "call to %s missing a final value", shortName(fn))
} else {
pass.ReportRangef(call, "call to %s has a missing or misplaced value", shortName(fn))
}
}
})
return nil, nil
}
func isAttr(t types.Type) bool {
return analysisutil.IsNamedType(t, "log/slog", "Attr")
}
// shortName returns a name for the function that is shorter than FullName.
// Examples:
//
// "slog.Info" (instead of "log/slog.Info")
// "slog.Logger.With" (instead of "(*log/slog.Logger).With")
func shortName(fn *types.Func) string {
var r string
if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
if _, named := typesinternal.ReceiverNamed(recv); named != nil {
r = named.Obj().Name()
} else {
r = recv.Type().String() // anon struct/interface
}
r += "."
}
return fmt.Sprintf("%s.%s%s", fn.Pkg().Name(), r, fn.Name())
}
// If fn is a slog function that has a ...any parameter for key-value pairs,
// kvFuncSkipArgs returns the number of arguments to skip over to reach the
// corresponding arguments, and true.
// Otherwise it returns (0, false).
func kvFuncSkipArgs(fn *types.Func) (int, bool) {
if pkg := fn.Pkg(); pkg == nil || pkg.Path() != "log/slog" {
return 0, false
}
var recvName string // by default a slog package function
if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
_, named := typesinternal.ReceiverNamed(recv)
if named == nil {
return 0, false // anon struct/interface
}
recvName = named.Obj().Name()
}
skip, ok := kvFuncs[recvName][fn.Name()]
return skip, ok
}
// The names of functions and methods in log/slog that take
// ...any for key-value pairs, mapped to the number of initial args to skip in
// order to get to the ones that match the ...any parameter.
// The first key is the dereferenced receiver type name, or "" for a function.
var kvFuncs = map[string]map[string]int{
"": map[string]int{
"Debug": 1,
"Info": 1,
"Warn": 1,
"Error": 1,
"DebugContext": 2,
"InfoContext": 2,
"WarnContext": 2,
"ErrorContext": 2,
"Log": 3,
"Group": 1,
},
"Logger": map[string]int{
"Debug": 1,
"Info": 1,
"Warn": 1,
"Error": 1,
"DebugContext": 2,
"InfoContext": 2,
"WarnContext": 2,
"ErrorContext": 2,
"Log": 3,
"With": 0,
},
"Record": map[string]int{
"Add": 0,
},
}
// isMethodExpr reports whether a call is to a MethodExpr.
func isMethodExpr(info *types.Info, c *ast.CallExpr) bool {
s, ok := c.Fun.(*ast.SelectorExpr)
if !ok {
return false
}
sel := info.Selections[s]
return sel != nil && sel.Kind() == types.MethodExpr
}
|