github.com/onsi/ginkgo@v1.16.6-0.20211118180735-4e1925ba4c95/types/label_filter.go (about)

     1  package types
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  )
     8  
     9  var DEBUG_LABEL_FILTER_PARSING = false
    10  
    11  type LabelFilter func([]string) bool
    12  
    13  func matchLabelAction(label string) LabelFilter {
    14  	expected := strings.ToLower(label)
    15  	return func(labels []string) bool {
    16  		for i := range labels {
    17  			if strings.ToLower(labels[i]) == expected {
    18  				return true
    19  			}
    20  		}
    21  		return false
    22  	}
    23  }
    24  
    25  func matchLabelRegexAction(regex *regexp.Regexp) LabelFilter {
    26  	return func(labels []string) bool {
    27  		for i := range labels {
    28  			if regex.MatchString(labels[i]) {
    29  				return true
    30  			}
    31  		}
    32  		return false
    33  	}
    34  }
    35  
    36  func notAction(filter LabelFilter) LabelFilter {
    37  	return func(labels []string) bool { return !filter(labels) }
    38  }
    39  
    40  func andAction(a, b LabelFilter) LabelFilter {
    41  	return func(labels []string) bool { return a(labels) && b(labels) }
    42  }
    43  
    44  func orAction(a, b LabelFilter) LabelFilter {
    45  	return func(labels []string) bool { return a(labels) || b(labels) }
    46  }
    47  
    48  type lfToken uint
    49  
    50  const (
    51  	lfTokenInvalid lfToken = iota
    52  
    53  	lfTokenRoot
    54  	lfTokenOpenGroup
    55  	lfTokenCloseGroup
    56  	lfTokenNot
    57  	lfTokenAnd
    58  	lfTokenOr
    59  	lfTokenRegexp
    60  	lfTokenLabel
    61  	lfTokenEOF
    62  )
    63  
    64  func (l lfToken) Precedence() int {
    65  	switch l {
    66  	case lfTokenRoot, lfTokenOpenGroup:
    67  		return 0
    68  	case lfTokenOr:
    69  		return 1
    70  	case lfTokenAnd:
    71  		return 2
    72  	case lfTokenNot:
    73  		return 3
    74  	}
    75  	return -1
    76  }
    77  
    78  func (l lfToken) String() string {
    79  	switch l {
    80  	case lfTokenRoot:
    81  		return "ROOT"
    82  	case lfTokenOpenGroup:
    83  		return "("
    84  	case lfTokenCloseGroup:
    85  		return ")"
    86  	case lfTokenNot:
    87  		return "!"
    88  	case lfTokenAnd:
    89  		return "&&"
    90  	case lfTokenOr:
    91  		return "||"
    92  	case lfTokenRegexp:
    93  		return "/regexp/"
    94  	case lfTokenLabel:
    95  		return "label"
    96  	case lfTokenEOF:
    97  		return "EOF"
    98  	}
    99  	return "INVALID"
   100  }
   101  
   102  type treeNode struct {
   103  	token    lfToken
   104  	location int
   105  	value    string
   106  
   107  	parent    *treeNode
   108  	leftNode  *treeNode
   109  	rightNode *treeNode
   110  }
   111  
   112  func (tn *treeNode) setRightNode(node *treeNode) {
   113  	tn.rightNode = node
   114  	node.parent = tn
   115  }
   116  
   117  func (tn *treeNode) setLeftNode(node *treeNode) {
   118  	tn.leftNode = node
   119  	node.parent = tn
   120  }
   121  
   122  func (tn *treeNode) firstAncestorWithPrecedenceLEQ(precedence int) *treeNode {
   123  	if tn.token.Precedence() <= precedence {
   124  		return tn
   125  	}
   126  	return tn.parent.firstAncestorWithPrecedenceLEQ(precedence)
   127  }
   128  
   129  func (tn *treeNode) firstUnmatchedOpenNode() *treeNode {
   130  	if tn.token == lfTokenOpenGroup {
   131  		return tn
   132  	}
   133  	if tn.parent == nil {
   134  		return nil
   135  	}
   136  	return tn.parent.firstUnmatchedOpenNode()
   137  }
   138  
   139  func (tn *treeNode) constructLabelFilter(input string) (LabelFilter, error) {
   140  	switch tn.token {
   141  	case lfTokenOpenGroup:
   142  		return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, tn.location, "Mismatched '(' - could not find matching ')'.")
   143  	case lfTokenLabel:
   144  		return matchLabelAction(tn.value), nil
   145  	case lfTokenRegexp:
   146  		re, err := regexp.Compile(tn.value)
   147  		if err != nil {
   148  			return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, tn.location, fmt.Sprintf("RegExp compilation error: %s", err))
   149  		}
   150  		return matchLabelRegexAction(re), nil
   151  	}
   152  
   153  	if tn.rightNode == nil {
   154  		return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, -1, "Unexpected EOF.")
   155  	}
   156  	rightLF, err := tn.rightNode.constructLabelFilter(input)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	switch tn.token {
   162  	case lfTokenRoot, lfTokenCloseGroup:
   163  		return rightLF, nil
   164  	case lfTokenNot:
   165  		return notAction(rightLF), nil
   166  	}
   167  
   168  	if tn.leftNode == nil {
   169  		return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, tn.location, fmt.Sprintf("Malformed tree - '%s' is missing left operand.", tn.token))
   170  	}
   171  	leftLF, err := tn.leftNode.constructLabelFilter(input)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	switch tn.token {
   177  	case lfTokenAnd:
   178  		return andAction(leftLF, rightLF), nil
   179  	case lfTokenOr:
   180  		return orAction(leftLF, rightLF), nil
   181  	}
   182  
   183  	return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, tn.location, fmt.Sprintf("Invalid token '%s'.", tn.token))
   184  }
   185  
   186  func (tn *treeNode) tokenString() string {
   187  	out := fmt.Sprintf("<%s", tn.token)
   188  	if tn.value != "" {
   189  		out += " | " + tn.value
   190  	}
   191  	out += ">"
   192  	return out
   193  }
   194  
   195  func (tn *treeNode) toString(indent int) string {
   196  	out := tn.tokenString() + "\n"
   197  	if tn.leftNode != nil {
   198  		out += fmt.Sprintf("%s  |_(L)_%s", strings.Repeat(" ", indent), tn.leftNode.toString(indent+1))
   199  	}
   200  	if tn.rightNode != nil {
   201  		out += fmt.Sprintf("%s  |_(R)_%s", strings.Repeat(" ", indent), tn.rightNode.toString(indent+1))
   202  	}
   203  	return out
   204  }
   205  
   206  func tokenize(input string) func() (*treeNode, error) {
   207  	runes, i := []rune(input), 0
   208  
   209  	peekIs := func(r rune) bool {
   210  		if i+1 < len(runes) {
   211  			return runes[i+1] == r
   212  		}
   213  		return false
   214  	}
   215  
   216  	consumeUntil := func(cutset string) (string, int) {
   217  		j := i
   218  		for ; j < len(runes); j++ {
   219  			if strings.IndexRune(cutset, runes[j]) >= 0 {
   220  				break
   221  			}
   222  		}
   223  		return string(runes[i:j]), j - i
   224  	}
   225  
   226  	return func() (*treeNode, error) {
   227  		for i < len(runes) && runes[i] == ' ' {
   228  			i += 1
   229  		}
   230  
   231  		if i >= len(runes) {
   232  			return &treeNode{token: lfTokenEOF}, nil
   233  		}
   234  
   235  		node := &treeNode{location: i}
   236  		switch runes[i] {
   237  		case '&':
   238  			if !peekIs('&') {
   239  				return &treeNode{}, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, i, "Invalid token '&'.  Did you mean '&&'?")
   240  			}
   241  			i += 2
   242  			node.token = lfTokenAnd
   243  		case '|':
   244  			if !peekIs('|') {
   245  				return &treeNode{}, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, i, "Invalid token '|'.  Did you mean '||'?")
   246  			}
   247  			i += 2
   248  			node.token = lfTokenOr
   249  		case '!':
   250  			i += 1
   251  			node.token = lfTokenNot
   252  		case ',':
   253  			i += 1
   254  			node.token = lfTokenOr
   255  		case '(':
   256  			i += 1
   257  			node.token = lfTokenOpenGroup
   258  		case ')':
   259  			i += 1
   260  			node.token = lfTokenCloseGroup
   261  		case '/':
   262  			i += 1
   263  			value, n := consumeUntil("/")
   264  			i += n + 1
   265  			node.token, node.value = lfTokenRegexp, value
   266  		default:
   267  			value, n := consumeUntil("&|!,()/")
   268  			i += n
   269  			node.token, node.value = lfTokenLabel, strings.TrimSpace(value)
   270  		}
   271  		return node, nil
   272  	}
   273  }
   274  
   275  func ParseLabelFilter(input string) (LabelFilter, error) {
   276  	if DEBUG_LABEL_FILTER_PARSING {
   277  		fmt.Println("\n==============")
   278  		fmt.Println("Input: ", input)
   279  		fmt.Print("Tokens: ")
   280  	}
   281  	nextToken := tokenize(input)
   282  
   283  	root := &treeNode{token: lfTokenRoot}
   284  	current := root
   285  LOOP:
   286  	for {
   287  		node, err := nextToken()
   288  		if err != nil {
   289  			return nil, err
   290  		}
   291  
   292  		if DEBUG_LABEL_FILTER_PARSING {
   293  			fmt.Print(node.tokenString() + " ")
   294  		}
   295  
   296  		switch node.token {
   297  		case lfTokenEOF:
   298  			break LOOP
   299  		case lfTokenLabel, lfTokenRegexp:
   300  			if current.rightNode != nil {
   301  				return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, node.location, "Found two adjacent labels.  You need an operator between them.")
   302  			}
   303  			current.setRightNode(node)
   304  		case lfTokenNot, lfTokenOpenGroup:
   305  			if current.rightNode != nil {
   306  				return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, node.location, fmt.Sprintf("Invalid token '%s'.", node.token))
   307  			}
   308  			current.setRightNode(node)
   309  			current = node
   310  		case lfTokenAnd, lfTokenOr:
   311  			if current.rightNode == nil {
   312  				return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, node.location, fmt.Sprintf("Operator '%s' missing left hand operand.", node.token))
   313  			}
   314  			nodeToStealFrom := current.firstAncestorWithPrecedenceLEQ(node.token.Precedence())
   315  			node.setLeftNode(nodeToStealFrom.rightNode)
   316  			nodeToStealFrom.setRightNode(node)
   317  			current = node
   318  		case lfTokenCloseGroup:
   319  			firstUnmatchedOpenNode := current.firstUnmatchedOpenNode()
   320  			if firstUnmatchedOpenNode == nil {
   321  				return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, node.location, "Mismatched ')' - could not find matching '('.")
   322  			}
   323  			if firstUnmatchedOpenNode == current && current.rightNode == nil {
   324  				return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, node.location, "Found empty '()' group.")
   325  			}
   326  			firstUnmatchedOpenNode.token = lfTokenCloseGroup //signify the group is now closed
   327  			current = firstUnmatchedOpenNode.parent
   328  		default:
   329  			return nil, GinkgoErrors.SyntaxErrorParsingLabelFilter(input, node.location, fmt.Sprintf("Unknown token '%s'.", node.token))
   330  		}
   331  	}
   332  	if DEBUG_LABEL_FILTER_PARSING {
   333  		fmt.Printf("\n Tree:\n%s", root.toString(0))
   334  	}
   335  	return root.constructLabelFilter(input)
   336  }
   337  
   338  func ValidateAndCleanupLabel(label string, cl CodeLocation) (string, error) {
   339  	out := strings.TrimSpace(label)
   340  	if out == "" {
   341  		return "", GinkgoErrors.InvalidEmptyLabel(cl)
   342  	}
   343  	if strings.ContainsAny(out, "&|!,()/") {
   344  		return "", GinkgoErrors.InvalidLabel(label, cl)
   345  	}
   346  	return out, nil
   347  }