github.com/saucelabs/saucectl@v0.175.1/internal/cypress/grep/parser.go (about) 1 package grep 2 3 import ( 4 "regexp" 5 "strings" 6 ) 7 8 // Functions to parse cypress-grep expressions. 9 // cypress-grep expressions can include simple logical operations (e.g. 10 // NOT, AND, OR). These expressions are parsed into Expressions that are 11 // logical predicates that evaluate to true or false depending if the expression matches 12 // a given input string. 13 14 var reDelimiter = regexp.MustCompile(`[ ,]`) 15 16 // Expression is an interface that represents a logical statement. 17 type Expression interface { 18 // Eval evaluates the Expression against the input string. 19 Eval(input string) bool 20 } 21 22 // Partial represents an expression that wraps a string to be matched 23 // and evaluates to true if that string is a partial substring of the input. 24 type Partial struct { 25 Query string 26 Invert bool 27 } 28 29 // Exact represents an expression that wraps a string to be matched 30 // and evalutates to true if that string is an exact substring of the input. 31 type Exact struct { 32 Query string 33 Invert bool 34 } 35 36 // Any represents a composite expression that evaluates to true if any 37 // child expression evaluates to true. 38 type Any struct { 39 Expressions []Expression 40 } 41 42 // All represents a composite expression that evaluates to true if all 43 // child expressions evaluate to true. 44 type All struct { 45 Expressions []Expression 46 } 47 48 // Literal represents an expression that always evaluates to a literal truth value. 49 type Literal struct { 50 value bool 51 } 52 53 func (l Literal) Eval(input string) bool { 54 return l.value 55 } 56 57 func (p Partial) Eval(input string) bool { 58 contains := strings.Contains(input, p.Query) 59 if p.Invert { 60 return !contains 61 } 62 return contains 63 } 64 65 func (e Exact) Eval(input string) bool { 66 words := strings.Split(input, " ") 67 contains := false 68 69 for _, w := range words { 70 if w == e.Query { 71 contains = true 72 break 73 } 74 } 75 76 if e.Invert { 77 return !contains 78 } 79 return contains 80 } 81 82 func (a *Any) Eval(input string) bool { 83 for _, p := range a.Expressions { 84 if p.Eval(input) { 85 return true 86 } 87 } 88 return false 89 } 90 91 func (a *Any) add(p Expression) { 92 a.Expressions = append(a.Expressions, p) 93 } 94 95 func (a *All) Eval(input string) bool { 96 for _, p := range a.Expressions { 97 if !p.Eval(input) { 98 return false 99 } 100 } 101 return true 102 } 103 104 func (a *All) add(p Expression) { 105 a.Expressions = append(a.Expressions, p) 106 } 107 108 // ParseGrepTitleExp parses a cypress-grep expression and returns an Expression. 109 // 110 // The returned Expression can be used to evaluate whether a given string 111 // can match the expression. 112 func ParseGrepTitleExp(expr string) Expression { 113 // Treat an empty expression as if grepping were disabled 114 if expr == "" { 115 return Literal{ 116 value: true, 117 } 118 } 119 strs := strings.Split(expr, ";") 120 strs = normalize(strs) 121 122 substringMatches := Any{} 123 invertedMatches := All{} 124 for _, s := range strs { 125 if strings.HasPrefix(s, "-") { 126 invertedMatches.add(Partial{ 127 Query: s[1:], 128 Invert: true, 129 }) 130 continue 131 } 132 substringMatches.add(Partial{ 133 Query: s, 134 }) 135 } 136 137 parsed := All{} 138 parsed.add(&invertedMatches) 139 if len(substringMatches.Expressions) > 0 { 140 parsed.add(&substringMatches) 141 } 142 return &parsed 143 } 144 145 // ParseGrepTagsExp parses a cypress-grep tag expression. 146 // 147 // The returned Expression can be used to evaluate whether a given string 148 // can match the expression. 149 func ParseGrepTagsExp(expr string) Expression { 150 // Treat an empty expression as if grepping were disabled 151 if expr == "" { 152 return Literal{ 153 value: true, 154 } 155 } 156 exprs := reDelimiter.Split(expr, -1) 157 exprs = normalize(exprs) 158 159 var parsed Any 160 var not []Expression 161 162 // Find any global inverted expressions first 163 for _, e := range exprs { 164 if strings.HasPrefix(e, "--") { 165 not = append(not, Exact{ 166 Query: e[2:], 167 Invert: true, 168 }) 169 } 170 } 171 172 for _, e := range exprs { 173 if strings.HasPrefix(e, "--") { 174 continue 175 } 176 177 matcher := All{} 178 patterns := strings.Split(e, "+") 179 patterns = normalize(patterns) 180 for _, p := range patterns { 181 invert := false 182 if strings.HasPrefix(p, "-") { 183 invert = true 184 p = p[1:] 185 } 186 187 matcher.add(Exact{ 188 Query: p, 189 Invert: invert, 190 }) 191 } 192 193 // Add any globally inverted expressions found above 194 for _, n := range not { 195 matcher.add(n) 196 } 197 198 parsed.add(&matcher) 199 } 200 201 return &parsed 202 } 203 204 // normalize trims leading and trailing whitespace from a slice of strings 205 // and filters out any strings that contain only whitespace. 206 func normalize(strs []string) []string { 207 var all []string 208 for _, s := range strs { 209 trimmed := strings.TrimSpace(s) 210 if trimmed == "" { 211 continue 212 } 213 all = append(all, strings.TrimSpace(s)) 214 } 215 return all 216 }