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