github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/testutils/lint/passes/fmtsafe/fmtsafe.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package fmtsafe
    12  
    13  import (
    14  	"fmt"
    15  	"go/ast"
    16  	"go/token"
    17  	"go/types"
    18  	"strings"
    19  
    20  	"github.com/cockroachdb/errors"
    21  	"golang.org/x/tools/go/analysis"
    22  	"golang.org/x/tools/go/analysis/passes/inspect"
    23  	"golang.org/x/tools/go/ast/inspector"
    24  	"golang.org/x/tools/go/types/typeutil"
    25  )
    26  
    27  // Doc documents this pass.
    28  var Doc = `checks that log and error functions don't leak PII.
    29  
    30  This linter checks the following:
    31  
    32  - that the format string in Infof(), Errorf() and similar calls is a
    33    constant string.
    34  
    35    This check is essential for correctness because format strings
    36    are assumed to be PII-free and always safe for reporting in
    37    telemetry or PII-free logs.
    38  
    39  - that the message strings in errors.New() and similar calls that
    40    construct error objects is a constant string.
    41  
    42    This check is provided to encourage hygiene: errors
    43    constructed using non-constant strings are better constructed using
    44    a formatting function instead, which makes the construction more
    45    readable and encourage the provision of PII-free reportable details.
    46  
    47  It is possible for a call site *in a test file* to opt the format/message
    48  string out of the linter using /* nolint:fmtsafe */ after the format
    49  argument. This escape hatch is not available in non-test code.
    50  `
    51  
    52  // Analyzer defines this pass.
    53  var Analyzer = &analysis.Analyzer{
    54  	Name:     "fmtsafe",
    55  	Doc:      Doc,
    56  	Requires: []*analysis.Analyzer{inspect.Analyzer},
    57  	Run:      run,
    58  }
    59  
    60  func run(pass *analysis.Pass) (interface{}, error) {
    61  	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    62  
    63  	// Our analyzer just wants to see function definitions
    64  	// and call points.
    65  	nodeFilter := []ast.Node{
    66  		(*ast.FuncDecl)(nil),
    67  		(*ast.CallExpr)(nil),
    68  	}
    69  
    70  	// fmtOrMsgStr, if non-nil, indicates an incoming
    71  	// format or message string in the argument list of the
    72  	// current function.
    73  	//
    74  	// The pointer is set at the beginning of every function declaration
    75  	// for a function recognized by this linter (= any of those listed
    76  	// in functions.go). In non-recognized function, it is set to nil to
    77  	// indicate there is no known format or message string.
    78  	var fmtOrMsgStr *types.Var
    79  	var enclosingFnName string
    80  
    81  	// Now traverse the ASTs. The preorder traversal visits each
    82  	// function declaration node before its body, so we always get to
    83  	// set fmtOrMsgStr before the call points inside the body are
    84  	// visited.
    85  	inspect.Preorder(nodeFilter, func(n ast.Node) {
    86  		// Catch-all for possible bugs in the linter code.
    87  		defer func() {
    88  			if r := recover(); r != nil {
    89  				if err, ok := r.(error); ok {
    90  					pass.Reportf(n.Pos(), "internal linter error: %v", err)
    91  					return
    92  				}
    93  				panic(r)
    94  			}
    95  		}()
    96  
    97  		if fd, ok := n.(*ast.FuncDecl); ok {
    98  			// This is the function declaration header. Obtain the formal
    99  			// parameter and the name of the function being defined.
   100  			// We use the name in subsequent error messages to provide
   101  			// more context, and to facilitate the definition
   102  			// of precise exceptions in lint_test.go.
   103  			enclosingFnName, fmtOrMsgStr = maybeGetConstStr(pass, fd)
   104  			return
   105  		}
   106  		// At a call site.
   107  		call := n.(*ast.CallExpr)
   108  		checkCallExpr(pass, enclosingFnName, call, fmtOrMsgStr)
   109  	})
   110  	return nil, nil
   111  }
   112  
   113  func maybeGetConstStr(
   114  	pass *analysis.Pass, fd *ast.FuncDecl,
   115  ) (enclosingFnName string, res *types.Var) {
   116  	if fd.Body == nil {
   117  		// No body. Since there won't be any callee, take
   118  		// an early return.
   119  		return "", nil
   120  	}
   121  
   122  	// What's the function being defined?
   123  	fn := pass.TypesInfo.Defs[fd.Name].(*types.Func)
   124  	if fn == nil {
   125  		return "", nil
   126  	}
   127  	fnName := stripVendor(fn.FullName())
   128  
   129  	var wantVariadic bool
   130  	var argIdx int
   131  
   132  	if requireConstFmt[fnName] {
   133  		// Expect a variadic function and the format parameter
   134  		// next-to-last in the parameter list.
   135  		wantVariadic = true
   136  		argIdx = -2
   137  	} else if requireConstMsg[fnName] {
   138  		// Expect a non-variadic function and the format parameter last in
   139  		// the parameter list.
   140  		wantVariadic = false
   141  		argIdx = -1
   142  	} else {
   143  		// Not a recognized function. Bail.
   144  		return fn.Name(), nil
   145  	}
   146  
   147  	sig := fn.Type().(*types.Signature)
   148  	if sig.Variadic() != wantVariadic {
   149  		panic(errors.Newf("expected variadic %v, got %v", wantVariadic, sig.Variadic()))
   150  	}
   151  
   152  	params := sig.Params()
   153  	nparams := params.Len()
   154  
   155  	// Is message or format param a string?
   156  	if nparams+argIdx < 0 {
   157  		panic(errors.New("not enough arguments"))
   158  	}
   159  	if p := params.At(nparams + argIdx); p.Type() == types.Typ[types.String] {
   160  		// Found it!
   161  		return fn.Name(), p
   162  	}
   163  	return fn.Name(), nil
   164  }
   165  
   166  func checkCallExpr(pass *analysis.Pass, enclosingFnName string, call *ast.CallExpr, fv *types.Var) {
   167  	// What's the function being called?
   168  	cfn := typeutil.Callee(pass.TypesInfo, call)
   169  	if cfn == nil {
   170  		// Not a call to a statically identified function.
   171  		// We can't lint.
   172  		return
   173  	}
   174  	fn, ok := cfn.(*types.Func)
   175  	if !ok {
   176  		// Not a function with a name. We can't lint either.
   177  		return
   178  	}
   179  
   180  	// What's the full name of the callee? This includes the package
   181  	// path and, for methods, the type of the supporting object.
   182  	fnName := stripVendor(fn.FullName())
   183  
   184  	var wantVariadic bool
   185  	var argIdx int
   186  	var argType string
   187  
   188  	// Do the analysis of the callee.
   189  	if requireConstFmt[fnName] {
   190  		// Expect a variadic function and the format parameter
   191  		// next-to-last in the parameter list.
   192  		wantVariadic = true
   193  		argIdx = -2
   194  		argType = "format"
   195  	} else if requireConstMsg[fnName] {
   196  		// Expect a non-variadic function and the format parameter last in
   197  		// the parameter list.
   198  		wantVariadic = false
   199  		argIdx = -1
   200  		argType = "message"
   201  	} else {
   202  		// Not a recognized function. Bail.
   203  		return
   204  	}
   205  
   206  	typ := pass.TypesInfo.Types[call.Fun].Type
   207  	if typ == nil {
   208  		panic(errors.New("can't find function type"))
   209  	}
   210  
   211  	sig, ok := typ.(*types.Signature)
   212  	if !ok {
   213  		panic(errors.New("can't derive signature"))
   214  	}
   215  	if sig.Variadic() != wantVariadic {
   216  		panic(errors.Newf("expected variadic %v, got %v", wantVariadic, sig.Variadic()))
   217  	}
   218  
   219  	idx := sig.Params().Len() + argIdx
   220  	if idx < 0 {
   221  		panic(errors.New("not enough parameters"))
   222  	}
   223  
   224  	lit := pass.TypesInfo.Types[call.Args[idx]].Value
   225  	if lit != nil {
   226  		// A literal constant! All is well.
   227  		return
   228  	}
   229  
   230  	// Not a constant. If it's a variable and the variable
   231  	// refers to the incoming format/message from the arg list,
   232  	// tolerate that.
   233  	if fv != nil {
   234  		if id, ok := call.Args[idx].(*ast.Ident); ok {
   235  			if pass.TypesInfo.ObjectOf(id) == fv {
   236  				// Same arg as incoming. All good.
   237  				return
   238  			}
   239  		}
   240  	}
   241  
   242  	// If the argument is opting out of the linter with a special
   243  	// comment, tolerate that.
   244  	if hasNoLintComment(pass, call, idx) {
   245  		return
   246  	}
   247  
   248  	pass.Reportf(call.Lparen, escNl("%s(): %s argument is not a constant expression"+Tip),
   249  		enclosingFnName, argType)
   250  }
   251  
   252  // Tip is exported for use in tests.
   253  var Tip = `
   254  Tip: use YourFuncf("descriptive prefix %s", ...) or list new formatting wrappers in pkg/testutils/lint/passes/fmtsafe/functions.go.`
   255  
   256  func hasNoLintComment(pass *analysis.Pass, call *ast.CallExpr, idx int) bool {
   257  	fPos, f := findContainingFile(pass, call)
   258  
   259  	if !strings.HasSuffix(fPos.Name(), "_test.go") {
   260  		// The nolint: escape hatch is only supported in test files.
   261  		return false
   262  	}
   263  
   264  	startPos := call.Args[idx].End()
   265  	endPos := call.Rparen
   266  	if idx < len(call.Args)-1 {
   267  		endPos = call.Args[idx+1].Pos()
   268  	}
   269  	for _, cg := range f.Comments {
   270  		if cg.Pos() > endPos || cg.End() < startPos {
   271  			continue
   272  		}
   273  		for _, c := range cg.List {
   274  			if strings.Contains(c.Text, "nolint:fmtsafe") {
   275  				return true
   276  			}
   277  		}
   278  	}
   279  	return false
   280  }
   281  
   282  func findContainingFile(pass *analysis.Pass, n ast.Node) (*token.File, *ast.File) {
   283  	fPos := pass.Fset.File(n.Pos())
   284  	for _, f := range pass.Files {
   285  		if pass.Fset.File(f.Pos()) == fPos {
   286  			return fPos, f
   287  		}
   288  	}
   289  	panic(fmt.Errorf("cannot file file for %v", n))
   290  }
   291  
   292  func stripVendor(s string) string {
   293  	if i := strings.Index(s, "/vendor/"); i != -1 {
   294  		s = s[i+len("/vendor/"):]
   295  	}
   296  	return s
   297  }
   298  
   299  func escNl(msg string) string {
   300  	return strings.ReplaceAll(msg, "\n", "\\n++")
   301  }