-
Notifications
You must be signed in to change notification settings - Fork 72
/
args.go
210 lines (178 loc) · 5.42 KB
/
args.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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Copyright 2016 Ryan Boehning. All rights reserved.
// Use of this source code is governed by the MIT
// license that can be found in the LICENSE file.
package q
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"runtime"
"strings"
"unicode/utf8"
"github.com/kr/pretty"
)
// 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:
switch {
case a.Obj == nil:
name = a.Name
case 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 q.Q() 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, q.Q(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: %w", 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 !isQCall(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 {
return string(c) + text + string(endColor)
}
// exprToString returns the source text underlying the given ast.Expr.
func exprToString(arg ast.Expr) string {
var buf strings.Builder
fset := token.NewFileSet()
if err := printer.Fprint(&buf, fset, arg); err != nil {
return ""
}
// CallExpr will be multi-line and indented with tabs. replace tabs with
// spaces so we can better control formatting during output().
return strings.ReplaceAll(buf.String(), "\t", " ")
}
// 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
// q.Q().
func getCallerInfo() (funcName, file string, line int, err error) {
pc, file, line, ok := runtime.Caller(CallDepth)
if !ok {
// This error is not exported. It is only used internally in the q
// package. The error message isn't even used by the caller. So, I've
// suppressed the goerr113 linter here, which catches nonidiomatic
// error handling post Go 1.13 errors.
return "", "", 0, errors.New("failed to get info about the function calling q.Q") // nolint: goerr113
}
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, value := range values {
name := ""
if i < len(names) {
name = names[i]
}
if name == "" {
prepended[i] = value
continue
}
name = colorize(name, bold)
prepended[i] = fmt.Sprintf("%s=%s", name, value)
}
return prepended
}
// isQCall returns true if the given function call expression is Q() or q.Q().
func isQCall(n *ast.CallExpr) bool {
return isQFunction(n) || isQPackage(n)
}
// isQFunction returns true if the given function call expression is Q().
func isQFunction(n *ast.CallExpr) bool {
ident, is := n.Fun.(*ast.Ident)
if !is {
return false
}
return ident.Name == "Q"
}
// isQPackage returns true if the given function call expression is in the q
// package. Since Q() is the only exported function from the q package, this is
// sufficient for determining that we've found Q() in the source text.
func isQPackage(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 == "q"
}