forked from gotestyourself/gotest.tools
/
call.go
201 lines (177 loc) · 5.38 KB
/
call.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
package main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/token"
)
// call wraps a testify/assert ast.CallExpr and exposes properties of the
// expression to facilitate migrating the expression to a gotest.tools/v3/assert
type call struct {
fileset *token.FileSet
expr *ast.CallExpr
xIdent *ast.Ident
selExpr *ast.SelectorExpr
// assert is Assert (if the testify package was require), or Check (if the
// testify package was assert).
assert string
}
func (c call) String() string {
buf := new(bytes.Buffer)
// nolint: errcheck
format.Node(buf, token.NewFileSet(), c.expr)
return buf.String()
}
func (c call) StringWithFileInfo() string {
if c.fileset.File(c.expr.Pos()) == nil {
return fmt.Sprintf("%s at unknown file", c)
}
return fmt.Sprintf("%s at %s:%d", c,
relativePath(c.fileset.File(c.expr.Pos()).Name()),
c.fileset.Position(c.expr.Pos()).Line)
}
// testingT returns the first argument of the call, which is assumed to be a
// *ast.Ident of *testing.T or a compatible interface.
func (c call) testingT() ast.Expr {
if len(c.expr.Args) == 0 {
return nil
}
return c.expr.Args[0]
}
// extraArgs returns the arguments of the expression starting from index
func (c call) extraArgs(index int) []ast.Expr {
if len(c.expr.Args) <= index {
return nil
}
return c.expr.Args[index:]
}
// args returns a range of arguments from the expression
func (c call) args(from, to int) []ast.Expr {
return c.expr.Args[from:to]
}
// arg returns a single argument from the expression
func (c call) arg(index int) ast.Expr {
return c.expr.Args[index]
}
// newCallFromCallExpr returns a new call build from a ast.CallExpr. Returns false
// if the call expression does not have the expected ast nodes.
func newCallFromCallExpr(callExpr *ast.CallExpr, migration migration) (call, bool) {
c := call{}
selector, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return c, false
}
ident, ok := selector.X.(*ast.Ident)
if !ok {
return c, false
}
return call{
fileset: migration.fileset,
xIdent: ident,
selExpr: selector,
expr: callExpr,
}, true
}
// newTestifyCallFromNode returns a call that wraps a valid testify assertion.
// Returns false if the call expression is not a testify assertion.
func newTestifyCallFromNode(callExpr *ast.CallExpr, migration migration) (call, bool) {
tcall, ok := newCallFromCallExpr(callExpr, migration)
if !ok {
return tcall, false
}
testifyNewAssignStmt := testifyAssertionsAssignment(tcall, migration)
switch {
case testifyNewAssignStmt != nil:
return updateCallForTestifyNew(tcall, testifyNewAssignStmt, migration)
case isTestifyPkgCall(tcall, migration):
tcall.assert = migration.importNames.funcNameFromTestifyName(tcall.xIdent.Name)
return tcall, true
}
return tcall, false
}
// isTestifyPkgCall returns true if the call is a testify package-level assertion
// (as apposed to an assertion method on the Assertions type)
//
// TODO: check if the xIdent.Obj.Decl is an import declaration instead of
// assuming that a name matching the import name is always an import. Some code
// may shadow import names, which could lead to incorrect results.
func isTestifyPkgCall(tcall call, migration migration) bool {
return migration.importNames.matchesTestify(tcall.xIdent)
}
// testifyAssertionsAssignment returns an ast.AssignStmt if the call is a testify
// call from an Assertions object returned from assert.New(t) (not a package level
// assert). Otherwise returns nil.
func testifyAssertionsAssignment(tcall call, migration migration) *ast.AssignStmt {
if tcall.xIdent.Obj == nil {
return nil
}
assignStmt, ok := tcall.xIdent.Obj.Decl.(*ast.AssignStmt)
if !ok {
return nil
}
if isAssignmentFromAssertNew(assignStmt, migration) {
return assignStmt
}
return nil
}
func updateCallForTestifyNew(
tcall call,
testifyNewAssignStmt *ast.AssignStmt,
migration migration,
) (call, bool) {
testifyNewCallExpr := callExprFromAssignment(testifyNewAssignStmt)
if testifyNewCallExpr == nil {
return tcall, false
}
testifyNewCall, ok := newCallFromCallExpr(testifyNewCallExpr, migration)
if !ok {
return tcall, false
}
tcall.assert = migration.importNames.funcNameFromTestifyName(testifyNewCall.xIdent.Name)
tcall.expr = addMissingTestingTArgToCallExpr(tcall.expr, testifyNewCall.testingT())
return tcall, true
}
// addMissingTestingTArgToCallExpr adds a testingT arg as the first arg of the
// ast.CallExpr and returns a copy of the ast.CallExpr
func addMissingTestingTArgToCallExpr(callExpr *ast.CallExpr, testingT ast.Expr) *ast.CallExpr {
return &ast.CallExpr{
Fun: callExpr.Fun,
Args: append([]ast.Expr{removePos(testingT)}, callExpr.Args...),
}
}
func removePos(node ast.Expr) ast.Expr {
switch typed := node.(type) {
case *ast.Ident:
return &ast.Ident{Name: typed.Name}
}
return node
}
// TODO: use pkgInfo and walkForType instead?
func isAssignmentFromAssertNew(assign *ast.AssignStmt, migration migration) bool {
callExpr := callExprFromAssignment(assign)
if callExpr == nil {
return false
}
tcall, ok := newCallFromCallExpr(callExpr, migration)
if !ok {
return false
}
if !migration.importNames.matchesTestify(tcall.xIdent) {
return false
}
if len(tcall.expr.Args) != 1 {
return false
}
return tcall.selExpr.Sel.Name == "New"
}
func callExprFromAssignment(assign *ast.AssignStmt) *ast.CallExpr {
if len(assign.Rhs) != 1 {
return nil
}
callExpr, ok := assign.Rhs[0].(*ast.CallExpr)
if !ok {
return nil
}
return callExpr
}