github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/caller/resolver.go (about)

     1  // Copyright 2015 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 caller
    12  
    13  import (
    14  	"path"
    15  	"regexp"
    16  	"runtime"
    17  	"strings"
    18  
    19  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    20  )
    21  
    22  type cachedLookup struct {
    23  	file string
    24  	line int
    25  	fun  string
    26  }
    27  
    28  var dummyLookup = cachedLookup{file: "???", line: 1, fun: "???"}
    29  
    30  // A CallResolver is a helping hand around runtime.Caller() to look up file,
    31  // line and name of the calling function. CallResolver caches the results of
    32  // its lookups and strips the uninteresting prefix from both the caller's
    33  // location and name; see NewCallResolver().
    34  type CallResolver struct {
    35  	mu    syncutil.Mutex
    36  	cache map[uintptr]*cachedLookup
    37  	re    *regexp.Regexp
    38  }
    39  
    40  var reStripNothing = regexp.MustCompile(`^$`)
    41  
    42  // defaultRE strips src/github.com/org/project/(pkg/)module/submodule/file.go
    43  // down to module/submodule/file.go. It falls back to stripping nothing when
    44  // it's unable to look up its own location via runtime.Caller().
    45  var defaultRE = func() *regexp.Regexp {
    46  	_, file, _, ok := runtime.Caller(0)
    47  	if !ok {
    48  		return reStripNothing
    49  	}
    50  	const sep = "/"
    51  	root := path.Dir(file)
    52  	// Coverage tests report back as `[...]/util/caller/_test/_obj_test`;
    53  	// strip back to this package's directory.
    54  	for strings.Contains(root, sep) && !strings.HasSuffix(root, "caller") {
    55  		root = path.Dir(root)
    56  	}
    57  	// Strip to $GOPATH/src.
    58  	for i := 0; i < 6; i++ {
    59  		root = path.Dir(root)
    60  	}
    61  	qSep := regexp.QuoteMeta(sep)
    62  	// Part of the regexp that matches `/github.com/username/reponame/(pkg/)`.
    63  	pkgStrip := qSep + strings.Repeat(strings.Join([]string{"[^", "]+", ""}, qSep), 3) + "(?:pkg/)?(.*)"
    64  	if !strings.Contains(root, sep) {
    65  		// This is again the unusual case above. The actual callsites will have
    66  		// a "real" caller, so now we don't exactly know what to strip; going
    67  		// up to the rightmost "src" directory will be correct unless someone
    68  		// creates packages inside of a "src" directory within their GOPATH.
    69  		return regexp.MustCompile(".*" + qSep + "src" + pkgStrip)
    70  	}
    71  	if !strings.HasSuffix(root, sep+"src") && !strings.HasSuffix(root, sep+"vendor") &&
    72  		!strings.HasSuffix(root, sep+"pkg/mod") {
    73  		panic("unable to find base path for default call resolver, got " + root)
    74  	}
    75  	return regexp.MustCompile(regexp.QuoteMeta(root) + pkgStrip)
    76  }()
    77  
    78  var defaultCallResolver = NewCallResolver(defaultRE)
    79  
    80  // Lookup returns the (reduced) file, line and function of the caller at the
    81  // requested depth, using a default call resolver which drops the path of
    82  // the project repository.
    83  func Lookup(depth int) (file string, line int, fun string) {
    84  	return defaultCallResolver.Lookup(depth + 1)
    85  }
    86  
    87  // NewCallResolver returns a CallResolver. The supplied pattern must specify a
    88  // valid regular expression and is used to format the paths returned by
    89  // Lookup(): If submatches are specified, their concatenation forms the path,
    90  // otherwise the match of the whole expression is used. Paths which do not
    91  // match at all are left unchanged.
    92  // TODO(bdarnell): don't strip paths at lookup time, but at display time;
    93  // need better handling for callers such as x/tools/something.
    94  func NewCallResolver(re *regexp.Regexp) *CallResolver {
    95  	return &CallResolver{
    96  		cache: map[uintptr]*cachedLookup{},
    97  		re:    re,
    98  	}
    99  }
   100  
   101  // Lookup returns the (reduced) file, line and function of the caller at the
   102  // requested depth.
   103  func (cr *CallResolver) Lookup(depth int) (file string, line int, fun string) {
   104  	pc, file, line, ok := runtime.Caller(depth + 1)
   105  	if !ok || cr == nil {
   106  		return dummyLookup.file, dummyLookup.line, dummyLookup.fun
   107  	}
   108  	cr.mu.Lock()
   109  	defer cr.mu.Unlock()
   110  	if v, okCache := cr.cache[pc]; okCache {
   111  		return v.file, v.line, v.fun
   112  	}
   113  	if matches := cr.re.FindStringSubmatch(file); matches != nil {
   114  		if len(matches) == 1 {
   115  			file = matches[0]
   116  		} else {
   117  			// NB: "path" is used here (and elsewhere in this file) over
   118  			// "path/filepath" because runtime.Caller always returns unix paths.
   119  			file = path.Join(matches[1:]...)
   120  		}
   121  	}
   122  
   123  	cr.cache[pc] = &cachedLookup{file: file, line: line, fun: dummyLookup.fun}
   124  	if f := runtime.FuncForPC(pc); f != nil {
   125  		fun = f.Name()
   126  		if indDot := strings.LastIndexByte(fun, '.'); indDot != -1 {
   127  			fun = fun[indDot+1:]
   128  		}
   129  		cr.cache[pc].fun = fun
   130  	}
   131  	return file, line, fun
   132  }