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  }