244 lines
7.6 KiB
Go
244 lines
7.6 KiB
Go
|
package outline
|
||
|
|
||
|
import (
|
||
|
"go/ast"
|
||
|
"go/token"
|
||
|
"strconv"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// undefinedTextAlt is used if the spec/container text cannot be derived
|
||
|
undefinedTextAlt = "undefined"
|
||
|
)
|
||
|
|
||
|
// ginkgoMetadata holds useful bits of information for every entry in the outline
|
||
|
type ginkgoMetadata struct {
|
||
|
// Name is the spec or container function name, e.g. `Describe` or `It`
|
||
|
Name string `json:"name"`
|
||
|
|
||
|
// Text is the `text` argument passed to specs, and some containers
|
||
|
Text string `json:"text"`
|
||
|
|
||
|
// Start is the position of first character of the spec or container block
|
||
|
Start int `json:"start"`
|
||
|
|
||
|
// End is the position of first character immediately after the spec or container block
|
||
|
End int `json:"end"`
|
||
|
|
||
|
Spec bool `json:"spec"`
|
||
|
Focused bool `json:"focused"`
|
||
|
Pending bool `json:"pending"`
|
||
|
}
|
||
|
|
||
|
// ginkgoNode is used to construct the outline as a tree
|
||
|
type ginkgoNode struct {
|
||
|
ginkgoMetadata
|
||
|
Nodes []*ginkgoNode `json:"nodes"`
|
||
|
}
|
||
|
|
||
|
type walkFunc func(n *ginkgoNode)
|
||
|
|
||
|
func (n *ginkgoNode) PreOrder(f walkFunc) {
|
||
|
f(n)
|
||
|
for _, m := range n.Nodes {
|
||
|
m.PreOrder(f)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (n *ginkgoNode) PostOrder(f walkFunc) {
|
||
|
for _, m := range n.Nodes {
|
||
|
m.PostOrder(f)
|
||
|
}
|
||
|
f(n)
|
||
|
}
|
||
|
|
||
|
func (n *ginkgoNode) Walk(pre, post walkFunc) {
|
||
|
pre(n)
|
||
|
for _, m := range n.Nodes {
|
||
|
m.Walk(pre, post)
|
||
|
}
|
||
|
post(n)
|
||
|
}
|
||
|
|
||
|
// PropagateInheritedProperties propagates the Pending and Focused properties
|
||
|
// through the subtree rooted at n.
|
||
|
func (n *ginkgoNode) PropagateInheritedProperties() {
|
||
|
n.PreOrder(func(thisNode *ginkgoNode) {
|
||
|
for _, descendantNode := range thisNode.Nodes {
|
||
|
if thisNode.Pending {
|
||
|
descendantNode.Pending = true
|
||
|
descendantNode.Focused = false
|
||
|
}
|
||
|
if thisNode.Focused && !descendantNode.Pending {
|
||
|
descendantNode.Focused = true
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// BackpropagateUnfocus propagates the Focused property through the subtree
|
||
|
// rooted at n. It applies the rule described in the Ginkgo docs:
|
||
|
// > Nested programmatically focused specs follow a simple rule: if a
|
||
|
// > leaf-node is marked focused, any of its ancestor nodes that are marked
|
||
|
// > focus will be unfocused.
|
||
|
func (n *ginkgoNode) BackpropagateUnfocus() {
|
||
|
focusedSpecInSubtreeStack := []bool{}
|
||
|
n.PostOrder(func(thisNode *ginkgoNode) {
|
||
|
if thisNode.Spec {
|
||
|
focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, thisNode.Focused)
|
||
|
return
|
||
|
}
|
||
|
focusedSpecInSubtree := false
|
||
|
for range thisNode.Nodes {
|
||
|
focusedSpecInSubtree = focusedSpecInSubtree || focusedSpecInSubtreeStack[len(focusedSpecInSubtreeStack)-1]
|
||
|
focusedSpecInSubtreeStack = focusedSpecInSubtreeStack[0 : len(focusedSpecInSubtreeStack)-1]
|
||
|
}
|
||
|
focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, focusedSpecInSubtree)
|
||
|
if focusedSpecInSubtree {
|
||
|
thisNode.Focused = false
|
||
|
}
|
||
|
})
|
||
|
|
||
|
}
|
||
|
|
||
|
func packageAndIdentNamesFromCallExpr(ce *ast.CallExpr) (string, string, bool) {
|
||
|
switch ex := ce.Fun.(type) {
|
||
|
case *ast.Ident:
|
||
|
return "", ex.Name, true
|
||
|
case *ast.SelectorExpr:
|
||
|
pkgID, ok := ex.X.(*ast.Ident)
|
||
|
if !ok {
|
||
|
return "", "", false
|
||
|
}
|
||
|
// A package identifier is top-level, so Obj must be nil
|
||
|
if pkgID.Obj != nil {
|
||
|
return "", "", false
|
||
|
}
|
||
|
if ex.Sel == nil {
|
||
|
return "", "", false
|
||
|
}
|
||
|
return pkgID.Name, ex.Sel.Name, true
|
||
|
default:
|
||
|
return "", "", false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// absoluteOffsetsForNode derives the absolute character offsets of the node start and
|
||
|
// end positions.
|
||
|
func absoluteOffsetsForNode(fset *token.FileSet, n ast.Node) (start, end int) {
|
||
|
return fset.PositionFor(n.Pos(), false).Offset, fset.PositionFor(n.End(), false).Offset
|
||
|
}
|
||
|
|
||
|
// ginkgoNodeFromCallExpr derives an outline entry from a go AST subtree
|
||
|
// corresponding to a Ginkgo container or spec.
|
||
|
func ginkgoNodeFromCallExpr(fset *token.FileSet, ce *ast.CallExpr, ginkgoPackageName, tablePackageName *string) (*ginkgoNode, bool) {
|
||
|
packageName, identName, ok := packageAndIdentNamesFromCallExpr(ce)
|
||
|
if !ok {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
n := ginkgoNode{}
|
||
|
n.Name = identName
|
||
|
n.Start, n.End = absoluteOffsetsForNode(fset, ce)
|
||
|
n.Nodes = make([]*ginkgoNode, 0)
|
||
|
switch identName {
|
||
|
case "It", "Measure", "Specify":
|
||
|
n.Spec = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "Entry":
|
||
|
n.Spec = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, tablePackageName != nil && *tablePackageName == packageName
|
||
|
case "FIt", "FMeasure", "FSpecify":
|
||
|
n.Spec = true
|
||
|
n.Focused = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "FEntry":
|
||
|
n.Spec = true
|
||
|
n.Focused = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, tablePackageName != nil && *tablePackageName == packageName
|
||
|
case "PIt", "PMeasure", "PSpecify", "XIt", "XMeasure", "XSpecify":
|
||
|
n.Spec = true
|
||
|
n.Pending = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "PEntry", "XEntry":
|
||
|
n.Spec = true
|
||
|
n.Pending = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, tablePackageName != nil && *tablePackageName == packageName
|
||
|
case "Context", "Describe", "When":
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "DescribeTable":
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, tablePackageName != nil && *tablePackageName == packageName
|
||
|
case "FContext", "FDescribe", "FWhen":
|
||
|
n.Focused = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "FDescribeTable":
|
||
|
n.Focused = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, tablePackageName != nil && *tablePackageName == packageName
|
||
|
case "PContext", "PDescribe", "PWhen", "XContext", "XDescribe", "XWhen":
|
||
|
n.Pending = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "PDescribeTable", "XDescribeTable":
|
||
|
n.Pending = true
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, tablePackageName != nil && *tablePackageName == packageName
|
||
|
case "By":
|
||
|
n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt)
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "AfterEach", "BeforeEach":
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "JustAfterEach", "JustBeforeEach":
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "AfterSuite", "BeforeSuite":
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
case "SynchronizedAfterSuite", "SynchronizedBeforeSuite":
|
||
|
return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName
|
||
|
default:
|
||
|
return nil, false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// textOrAltFromCallExpr tries to derive the "text" of a Ginkgo spec or
|
||
|
// container. If it cannot derive it, it returns the alt text.
|
||
|
func textOrAltFromCallExpr(ce *ast.CallExpr, alt string) string {
|
||
|
text, defined := textFromCallExpr(ce)
|
||
|
if !defined {
|
||
|
return alt
|
||
|
}
|
||
|
return text
|
||
|
}
|
||
|
|
||
|
// textFromCallExpr tries to derive the "text" of a Ginkgo spec or container. If
|
||
|
// it cannot derive it, it returns false.
|
||
|
func textFromCallExpr(ce *ast.CallExpr) (string, bool) {
|
||
|
if len(ce.Args) < 1 {
|
||
|
return "", false
|
||
|
}
|
||
|
text, ok := ce.Args[0].(*ast.BasicLit)
|
||
|
if !ok {
|
||
|
return "", false
|
||
|
}
|
||
|
switch text.Kind {
|
||
|
case token.CHAR, token.STRING:
|
||
|
// For token.CHAR and token.STRING, Value is quoted
|
||
|
unquoted, err := strconv.Unquote(text.Value)
|
||
|
if err != nil {
|
||
|
// If unquoting fails, just use the raw Value
|
||
|
return text.Value, true
|
||
|
}
|
||
|
return unquoted, true
|
||
|
default:
|
||
|
return text.Value, true
|
||
|
}
|
||
|
}
|