Current Path : /usr/local/go/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/slog/ |
Current File : //usr/local/go/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/slog/slog.go |
// 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" ) //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) { 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 } 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. // It could be a string or an Attr so we don't know what to expect next. pos = unknown 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 isNamed(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 { t := recv.Type() if pt, ok := t.(*types.Pointer); ok { t = pt.Elem() } if nt, ok := t.(*types.Named); ok { r = nt.Obj().Name() } else { r = recv.Type().String() } 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 recv := fn.Type().(*types.Signature).Recv() if recv != nil { t := recv.Type() if pt, ok := t.(*types.Pointer); ok { t = pt.Elem() } if nt, ok := t.(*types.Named); !ok { return 0, false } else { recvName = nt.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 } // isNamed reports whether t is exactly a named type in a package with a given path. func isNamed(t types.Type, path, name string) bool { if n, ok := t.(*types.Named); ok { obj := n.Obj() return obj.Pkg() != nil && obj.Pkg().Path() == path && obj.Name() == name } return false }