This repository has been archived by the owner on Dec 26, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
helpers.go
189 lines (168 loc) · 5 KB
/
helpers.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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package d
import (
"bytes"
"errors"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"runtime"
"strings"
"unicode/utf8"
"github.com/kr/pretty"
)
var colorizeEnabled = true
// argName returns the source text of the given argument if it's a variable or
// an expression. If the argument is something else, like a literal, argName
// returns an empty string.
func argName(arg ast.Expr) string {
name := ""
switch a := arg.(type) {
case *ast.Ident:
if a.Obj.Kind == ast.Var || a.Obj.Kind == ast.Con {
name = a.Obj.Name
}
case *ast.BinaryExpr,
*ast.CallExpr,
*ast.IndexExpr,
*ast.KeyValueExpr,
*ast.ParenExpr,
*ast.SelectorExpr,
*ast.SliceExpr,
*ast.TypeAssertExpr,
*ast.UnaryExpr:
name = exprToString(arg)
}
return name
}
// argNames finds the d.D() call at the given filename/line number and
// returns its arguments as a slice of strings. If the argument is a literal,
// argNames will return an empty string at the index position of that argument.
// For example, d.D(ip, port, 5432) would return []string{"ip", "port", ""}.
// argNames returns an error if the source text cannot be parsed.
func argNames(filename string, line int) ([]string, error) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, filename, nil, 0)
if err != nil {
return nil, fmt.Errorf("failed to parse %q: %v", filename, err)
}
var names []string
ast.Inspect(f, func(n ast.Node) bool {
call, is := n.(*ast.CallExpr)
if !is {
// The node is not a function call.
return true // visit next node
}
if fset.Position(call.End()).Line != line {
// The node is a function call, but it's on the wrong line.
return true
}
if !isDCall(call) {
// The node is a function call on correct line, but it's not a Q()
// function.
return true
}
for _, arg := range call.Args {
names = append(names, argName(arg))
}
return true
})
return names, nil
}
// argWidth returns the number of characters that will be seen when the given
// argument is printed at the terminal.
func argWidth(arg string) int {
// Strip zero-width characters.
replacer := strings.NewReplacer(
"\n", "",
"\t", "",
"\r", "",
"\f", "",
"\v", "",
string(bold), "",
string(yellow), "",
string(cyan), "",
string(endColor), "",
)
s := replacer.Replace(arg)
return utf8.RuneCountInString(s)
}
// colorize returns the given text encapsulated in ANSI escape codes that
// give the text color in the terminal.
func colorize(text string, c color) string {
if !colorizeEnabled {
return text
}
return string(c) + text + string(endColor)
}
// exprToString returns the source text underlying the given ast.Expr.
func exprToString(arg ast.Expr) string {
var buf bytes.Buffer
fset := token.NewFileSet()
printer.Fprint(&buf, fset, arg)
// CallExpr will be multi-line and indented with tabs. replace tabs with
// spaces so we can better control formatting during output().
return strings.Replace(buf.String(), "\t", " ", -1)
}
// formatArgs converts the given args to pretty-printed, colorized strings.
func formatArgs(args ...interface{}) []string {
formatted := make([]string, 0, len(args))
for _, a := range args {
s := colorize(pretty.Sprint(a), cyan)
formatted = append(formatted, s)
}
return formatted
}
// getCallerInfo returns the name, file, and line number of the function calling
// d.D().
func getCallerInfo() (funcName, file string, line int, err error) {
const callDepth = 2 // user code calls d.D() which calls std.log().
pc, file, line, ok := runtime.Caller(callDepth)
if !ok {
return "", "", 0, errors.New("failed to get info about the function calling d.D")
}
funcName = runtime.FuncForPC(pc).Name()
return funcName, file, line, nil
}
// prependArgName turns argument names and values into name=value strings, e.g.
// "port=443", "3+2=5". If the name is given, it will be bolded using ANSI
// color codes. If no name is given, just the value will be returned.
func prependArgName(names, values []string) []string {
prepended := make([]string, len(values))
for i, name := range names {
if name == "" {
prepended[i] = values[i]
continue
}
name = colorize(name, bold)
prepended[i] = fmt.Sprintf("%s=%s", name, values[i])
}
return prepended
}
// isDCall returns true if the given function call expression is D() or d.D().
func isDCall(n *ast.CallExpr) bool {
return isDFunction(n) || isDPackage(n)
}
// isDFunction returns true if the given function call expression is D().
func isDFunction(n *ast.CallExpr) bool {
ident, is := n.Fun.(*ast.Ident)
if !is {
return false
}
return ident.Name == "D"
}
// isDPackage returns true if the given function call expression is in the d
// package. Since D() is the only exported function from the d package, this is
// sufficient for determining that we've found D() in the source text.
func isDPackage(n *ast.CallExpr) bool {
sel, is := n.Fun.(*ast.SelectorExpr) // SelectorExpr example: a.B()
if !is {
return false
}
ident, is := sel.X.(*ast.Ident) // sel.X is the part that precedes the .
if !is {
return false
}
return ident.Name == "d"
}