github.com/onsi/ginkgo@v1.16.6-0.20211118180735-4e1925ba4c95/ginkgo/outline/ginkgo.go (about) 1 package outline 2 3 import ( 4 "go/ast" 5 "go/token" 6 "strconv" 7 ) 8 9 const ( 10 // undefinedTextAlt is used if the spec/container text cannot be derived 11 undefinedTextAlt = "undefined" 12 ) 13 14 // ginkgoMetadata holds useful bits of information for every entry in the outline 15 type ginkgoMetadata struct { 16 // Name is the spec or container function name, e.g. `Describe` or `It` 17 Name string `json:"name"` 18 19 // Text is the `text` argument passed to specs, and some containers 20 Text string `json:"text"` 21 22 // Start is the position of first character of the spec or container block 23 Start int `json:"start"` 24 25 // End is the position of first character immediately after the spec or container block 26 End int `json:"end"` 27 28 Spec bool `json:"spec"` 29 Focused bool `json:"focused"` 30 Pending bool `json:"pending"` 31 } 32 33 // ginkgoNode is used to construct the outline as a tree 34 type ginkgoNode struct { 35 ginkgoMetadata 36 Nodes []*ginkgoNode `json:"nodes"` 37 } 38 39 type walkFunc func(n *ginkgoNode) 40 41 func (n *ginkgoNode) PreOrder(f walkFunc) { 42 f(n) 43 for _, m := range n.Nodes { 44 m.PreOrder(f) 45 } 46 } 47 48 func (n *ginkgoNode) PostOrder(f walkFunc) { 49 for _, m := range n.Nodes { 50 m.PostOrder(f) 51 } 52 f(n) 53 } 54 55 func (n *ginkgoNode) Walk(pre, post walkFunc) { 56 pre(n) 57 for _, m := range n.Nodes { 58 m.Walk(pre, post) 59 } 60 post(n) 61 } 62 63 // PropagateInheritedProperties propagates the Pending and Focused properties 64 // through the subtree rooted at n. 65 func (n *ginkgoNode) PropagateInheritedProperties() { 66 n.PreOrder(func(thisNode *ginkgoNode) { 67 for _, descendantNode := range thisNode.Nodes { 68 if thisNode.Pending { 69 descendantNode.Pending = true 70 descendantNode.Focused = false 71 } 72 if thisNode.Focused && !descendantNode.Pending { 73 descendantNode.Focused = true 74 } 75 } 76 }) 77 } 78 79 // BackpropagateUnfocus propagates the Focused property through the subtree 80 // rooted at n. It applies the rule described in the Ginkgo docs: 81 // > Nested programmatically focused specs follow a simple rule: if a 82 // > leaf-node is marked focused, any of its ancestor nodes that are marked 83 // > focus will be unfocused. 84 func (n *ginkgoNode) BackpropagateUnfocus() { 85 focusedSpecInSubtreeStack := []bool{} 86 n.PostOrder(func(thisNode *ginkgoNode) { 87 if thisNode.Spec { 88 focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, thisNode.Focused) 89 return 90 } 91 focusedSpecInSubtree := false 92 for range thisNode.Nodes { 93 focusedSpecInSubtree = focusedSpecInSubtree || focusedSpecInSubtreeStack[len(focusedSpecInSubtreeStack)-1] 94 focusedSpecInSubtreeStack = focusedSpecInSubtreeStack[0 : len(focusedSpecInSubtreeStack)-1] 95 } 96 focusedSpecInSubtreeStack = append(focusedSpecInSubtreeStack, focusedSpecInSubtree) 97 if focusedSpecInSubtree { 98 thisNode.Focused = false 99 } 100 }) 101 102 } 103 104 func packageAndIdentNamesFromCallExpr(ce *ast.CallExpr) (string, string, bool) { 105 switch ex := ce.Fun.(type) { 106 case *ast.Ident: 107 return "", ex.Name, true 108 case *ast.SelectorExpr: 109 pkgID, ok := ex.X.(*ast.Ident) 110 if !ok { 111 return "", "", false 112 } 113 // A package identifier is top-level, so Obj must be nil 114 if pkgID.Obj != nil { 115 return "", "", false 116 } 117 if ex.Sel == nil { 118 return "", "", false 119 } 120 return pkgID.Name, ex.Sel.Name, true 121 default: 122 return "", "", false 123 } 124 } 125 126 // absoluteOffsetsForNode derives the absolute character offsets of the node start and 127 // end positions. 128 func absoluteOffsetsForNode(fset *token.FileSet, n ast.Node) (start, end int) { 129 return fset.PositionFor(n.Pos(), false).Offset, fset.PositionFor(n.End(), false).Offset 130 } 131 132 // ginkgoNodeFromCallExpr derives an outline entry from a go AST subtree 133 // corresponding to a Ginkgo container or spec. 134 func ginkgoNodeFromCallExpr(fset *token.FileSet, ce *ast.CallExpr, ginkgoPackageName *string) (*ginkgoNode, bool) { 135 packageName, identName, ok := packageAndIdentNamesFromCallExpr(ce) 136 if !ok { 137 return nil, false 138 } 139 140 n := ginkgoNode{} 141 n.Name = identName 142 n.Start, n.End = absoluteOffsetsForNode(fset, ce) 143 n.Nodes = make([]*ginkgoNode, 0) 144 switch identName { 145 case "It", "Specify", "Entry": 146 n.Spec = true 147 n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) 148 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 149 case "FIt", "FSpecify", "FEntry": 150 n.Spec = true 151 n.Focused = true 152 n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) 153 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 154 case "PIt", "PSpecify", "XIt", "XSpecify", "PEntry", "XEntry": 155 n.Spec = true 156 n.Pending = true 157 n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) 158 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 159 case "Context", "Describe", "When", "DescribeTable": 160 n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) 161 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 162 case "FContext", "FDescribe", "FWhen", "FDescribeTable": 163 n.Focused = true 164 n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) 165 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 166 case "PContext", "PDescribe", "PWhen", "XContext", "XDescribe", "XWhen", "PDescribeTable", "XDescribeTable": 167 n.Pending = true 168 n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) 169 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 170 case "By": 171 n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) 172 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 173 case "AfterEach", "BeforeEach": 174 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 175 case "JustAfterEach", "JustBeforeEach": 176 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 177 case "AfterSuite", "BeforeSuite": 178 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 179 case "SynchronizedAfterSuite", "SynchronizedBeforeSuite": 180 return &n, ginkgoPackageName != nil && *ginkgoPackageName == packageName 181 default: 182 return nil, false 183 } 184 } 185 186 // textOrAltFromCallExpr tries to derive the "text" of a Ginkgo spec or 187 // container. If it cannot derive it, it returns the alt text. 188 func textOrAltFromCallExpr(ce *ast.CallExpr, alt string) string { 189 text, defined := textFromCallExpr(ce) 190 if !defined { 191 return alt 192 } 193 return text 194 } 195 196 // textFromCallExpr tries to derive the "text" of a Ginkgo spec or container. If 197 // it cannot derive it, it returns false. 198 func textFromCallExpr(ce *ast.CallExpr) (string, bool) { 199 if len(ce.Args) < 1 { 200 return "", false 201 } 202 text, ok := ce.Args[0].(*ast.BasicLit) 203 if !ok { 204 return "", false 205 } 206 switch text.Kind { 207 case token.CHAR, token.STRING: 208 // For token.CHAR and token.STRING, Value is quoted 209 unquoted, err := strconv.Unquote(text.Value) 210 if err != nil { 211 // If unquoting fails, just use the raw Value 212 return text.Value, true 213 } 214 return unquoted, true 215 default: 216 return text.Value, true 217 } 218 }