...

Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/slog/slog.go

Documentation: cmd/vendor/golang.org/x/tools/go/analysis/passes/slog

     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  )
    24  
    25  //go:embed doc.go
    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  // A position describes what is expected to appear in an argument position.
    39  type position int
    40  
    41  const (
    42  	// key is an argument position that should hold a string key or an Attr.
    43  	key position = iota
    44  	// value is an argument position that should hold a value.
    45  	value
    46  	// unknown represents that we do not know if position should hold a key or a value.
    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 // not a static call
    60  		}
    61  		if call.Ellipsis != token.NoPos {
    62  			return // skip calls with "..." args
    63  		}
    64  		skipArgs, ok := kvFuncSkipArgs(fn)
    65  		if !ok {
    66  			// Not a slog function that takes key-value pairs.
    67  			return
    68  		}
    69  		if isMethodExpr(pass.TypesInfo, call) {
    70  			// Call is to a method value. Skip the first argument.
    71  			skipArgs++
    72  		}
    73  		if len(call.Args) <= skipArgs {
    74  			// Too few args; perhaps there are no k-v pairs.
    75  			return
    76  		}
    77  
    78  		// Check this call.
    79  		// The first position should hold a key or Attr.
    80  		pos := key
    81  		var unknownArg ast.Expr // nil or the last unknown argument
    82  		for _, arg := range call.Args[skipArgs:] {
    83  			t := pass.TypesInfo.Types[arg].Type
    84  			switch pos {
    85  			case key:
    86  				// Expect a string or Attr.
    87  				switch {
    88  				case t == stringType:
    89  					pos = value
    90  				case isAttr(t):
    91  					pos = key
    92  				case types.IsInterface(t):
    93  					// As we do not do dataflow, we do not know what the dynamic type is.
    94  					// It could be a string or an Attr so we don't know what to expect next.
    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  					// Stop here so we report at most one missing key per call.
   105  					return
   106  				}
   107  
   108  			case value:
   109  				// Anything can appear in this position.
   110  				// The next position should be a key.
   111  				pos = key
   112  
   113  			case unknown:
   114  				// Once we encounter an unknown position, we can never be
   115  				// sure if a problem later or at the end of the call is due to a
   116  				// missing final value, or a non-key in key position.
   117  				// In both cases, unknownArg != nil.
   118  				unknownArg = arg
   119  
   120  				// We don't know what is expected about this position, but all hope is not lost.
   121  				if t != stringType && !isAttr(t) && !types.IsInterface(t) {
   122  					// This argument is definitely not a key.
   123  					//
   124  					// unknownArg cannot have been a key, in which case this is the
   125  					// corresponding value, and the next position should hold another key.
   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  // shortName returns a name for the function that is shorter than FullName.
   146  // Examples:
   147  //
   148  //	"slog.Info" (instead of "log/slog.Info")
   149  //	"slog.Logger.With" (instead of "(*log/slog.Logger).With")
   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  // If fn is a slog function that has a ...any parameter for key-value pairs,
   168  // kvFuncSkipArgs returns the number of arguments to skip over to reach the
   169  // corresponding arguments, and true.
   170  // Otherwise it returns (0, false).
   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 // by default a slog package function
   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  // The names of functions and methods in log/slog that take
   193  // ...any for key-value pairs, mapped to the number of initial args to skip in
   194  // order to get to the ones that match the ...any parameter.
   195  // The first key is the dereferenced receiver type name, or "" for a function.
   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  // isMethodExpr reports whether a call is to a MethodExpr.
   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