class Cucumber::TagExpressions::Parser
def check(infix_expression, expected_token_type, token_type)
def check(infix_expression, expected_token_type, token_type) return if expected_token_type == token_type raise "Tag expression \"#{infix_expression}\" could not be parsed because of syntax error: Expected #{expected_token_type}." end
def handle_binary_operator(infix_expression, token, expected_token_type)
def handle_binary_operator(infix_expression, token, expected_token_type) check(infix_expression, expected_token_type, :operator) push_expression(infix_expression, @operators.pop) while @operators.any? && operator?(@operators.last) && lower_precedence?(token) @operators.push(token) :operand end
def handle_close_paren(infix_expression, _token, expected_token_type)
def handle_close_paren(infix_expression, _token, expected_token_type) check(infix_expression, expected_token_type, :operator) push_expression(infix_expression, @operators.pop) while @operators.any? && @operators.last != '(' raise "Tag expression \"#{infix_expression}\" could not be parsed because of syntax error: Unmatched )." if @operators.empty? @operators.pop if @operators.last == '(' :operator end
def handle_literal(infix_expression, token, expected_token_type)
def handle_literal(infix_expression, token, expected_token_type) check(infix_expression, expected_token_type, :operand) push_expression(infix_expression, token) :operator end
def handle_open_paren(infix_expression, token, expected_token_type)
def handle_open_paren(infix_expression, token, expected_token_type) check(infix_expression, expected_token_type, :operand) @operators.push(token) :operand end
def handle_sequential_tokens(token, infix_expression, expected_token_type)
def handle_sequential_tokens(token, infix_expression, expected_token_type) if operator_types[token] send("handle_#{operator_types.dig(token, :type)}", infix_expression, token, expected_token_type) else handle_literal(infix_expression, token, expected_token_type) end end
def handle_unary_operator(infix_expression, token, expected_token_type)
def handle_unary_operator(infix_expression, token, expected_token_type) check(infix_expression, expected_token_type, :operand) @operators.push(token) :operand end
def initialize
def initialize @expressions = [] @operators = [] end
def lower_precedence?(operation)
def lower_precedence?(operation) (operator_types.dig(operation, :assoc) == :left && precedence(operation) <= precedence(@operators.last)) || (operator_types.dig(operation, :assoc) == :right && precedence(operation) < precedence(@operators.last)) end
def operator?(token)
def operator?(token) %i[unary_operator binary_operator].include?(operator_types.dig(token, :type)) end
def operator_types
def operator_types { 'or' => { type: :binary_operator, precedence: 0, assoc: :left }, 'and' => { type: :binary_operator, precedence: 1, assoc: :left }, 'not' => { type: :unary_operator, precedence: 2, assoc: :right }, ')' => { type: :close_paren, precedence: -1 }, '(' => { type: :open_paren, precedence: 1 } } end
def parse(infix_expression)
def parse(infix_expression) expected_token_type = :operand tokens = tokenize(infix_expression) return True.new if tokens.empty? tokens.each { |token| expected_token_type = handle_sequential_tokens(token, infix_expression, expected_token_type) } while @operators.any? raise "Tag expression \"#{infix_expression}\" could not be parsed because of syntax error: Unmatched (." if @operators.last == '(' push_expression(infix_expression, @operators.pop) end @expressions.pop end
def popOperand(infix_expression, array, amount = 1)
def popOperand(infix_expression, array, amount = 1) result = array.pop(amount) raise "Tag expression \"#{infix_expression}\" could not be parsed because of syntax error: Expected operand." if result.length != amount amount == 1 ? result.first : result end
def precedence(token)
def precedence(token) operator_types.dig(token, :precedence) end
def push_expression(infix_expression, token)
def push_expression(infix_expression, token) case token when 'and' then @expressions.push(And.new(*popOperand(infix_expression, @expressions, 2))) when 'or' then @expressions.push(Or.new(*popOperand(infix_expression, @expressions, 2))) when 'not' then @expressions.push(Not.new(popOperand(infix_expression, @expressions))) else @expressions.push(Literal.new(token)) end end
def tokenize(infix_expression)
def tokenize(infix_expression) tokens = [] escaped = false token = +'' infix_expression.chars.each do |char| if escaped unless char == '(' || char == ')' || char == '\\' || whitespace?(char) raise "Tag expression \"#{infix_expression}\" could not be parsed because of syntax error: Illegal escape before \"#{char}\"." end token += char escaped = false elsif char == '\\' escaped = true elsif char == '(' || char == ')' || whitespace?(char) unless token.empty? tokens.push(token) token = +'' end tokens.push(char) unless whitespace?(char) else token += char end end tokens.push(token) unless token.empty? tokens end
def whitespace?(char)
def whitespace?(char) char.match(/\s/) end