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 }