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

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

View as plain text