Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
199 lines (175 sloc) 5.27 KB
package main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/token"
)
// call wraps an testify assert ast.CallExpr and exposes properties of the
// expression to facilitate migrating the expression to a gotest.tools/assert
type call struct {
fileset *token.FileSet
expr *ast.CallExpr
xIdent *ast.Ident
selExpr *ast.SelectorExpr
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
}
You can’t perform that action at this time.