# frozen_string_literal: true require 'cucumber/cucumber_expressions/ast' require 'cucumber/cucumber_expressions/errors' require 'cucumber/cucumber_expressions/cucumber_expression_tokenizer' module Cucumber module CucumberExpressions class CucumberExpressionParser def parse(expression) # text := whitespace | ')' | '}' | . parse_text = lambda do |_, tokens, current| token = tokens[current] case token.type when TokenType::WHITE_SPACE, TokenType::TEXT, TokenType::END_PARAMETER, TokenType::END_OPTIONAL return [1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)]] when TokenType::ALTERNATION raise AlternationNotAllowedInOptional.new(expression, token) when TokenType::BEGIN_PARAMETER, TokenType::START_OF_LINE, TokenType::END_OF_LINE, TokenType::BEGIN_OPTIONAL else # If configured correctly this will never happen return [0, nil] end # If configured correctly this will never happen return [0, nil] end # name := whitespace | . parse_name = lambda do |_, tokens, current| token = tokens[current] case token.type when TokenType::WHITE_SPACE, TokenType::TEXT return [1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)]] when TokenType::BEGIN_PARAMETER, TokenType::END_PARAMETER, TokenType::BEGIN_OPTIONAL, TokenType::END_OPTIONAL, TokenType::ALTERNATION raise InvalidParameterTypeNameInNode.new(expression, token) when TokenType::START_OF_LINE, TokenType::END_OF_LINE # If configured correctly this will never happen return [0, nil] else # If configured correctly this will never happen return [0, nil] end end # parameter := '{' + name* + '}' parse_parameter = parse_between(NodeType::PARAMETER, TokenType::BEGIN_PARAMETER, TokenType::END_PARAMETER, [parse_name]) # optional := '(' + option* + ')' # option := optional | parameter | text optional_sub_parsers = [] parse_optional = parse_between(NodeType::OPTIONAL, TokenType::BEGIN_OPTIONAL, TokenType::END_OPTIONAL, optional_sub_parsers) optional_sub_parsers << parse_optional << parse_parameter << parse_text # alternation := alternative* + ( '/' + alternative* )+ parse_alternative_separator = lambda do |_, tokens, current| return [0, nil] unless looking_at(tokens, current, TokenType::ALTERNATION) token = tokens[current] return [1, [Node.new(NodeType::ALTERNATIVE, nil, token.text, token.start, token.end)]] end alternative_parsers = [ parse_alternative_separator, parse_optional, parse_parameter, parse_text, ] # alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary) # left-boundary := whitespace | } | ^ # right-boundary := whitespace | { | $ # alternative: = optional | parameter | text parse_alternation = lambda do |expr, tokens, current| previous = current - 1 return [0, nil] unless looking_at_any(tokens, previous, [TokenType::START_OF_LINE, TokenType::WHITE_SPACE, TokenType::END_PARAMETER]) consumed, ast = parse_tokens_until(expr, alternative_parsers, tokens, current, [TokenType::WHITE_SPACE, TokenType::END_OF_LINE, TokenType::BEGIN_PARAMETER]) sub_current = current + consumed return [0, nil] unless ast.map { |ast_node| ast_node.type }.include? NodeType::ALTERNATIVE start = tokens[current].start _end = tokens[sub_current].start # Does not consume right hand boundary token return [consumed, [Node.new(NodeType::ALTERNATION, split_alternatives(start, _end, ast), nil, start, _end)]] end # # cucumber-expression := ( alternation | optional | parameter | text )* # parse_cucumber_expression = parse_between( NodeType::EXPRESSION, TokenType::START_OF_LINE, TokenType::END_OF_LINE, [parse_alternation, parse_optional, parse_parameter, parse_text] ) tokenizer = CucumberExpressionTokenizer.new tokens = tokenizer.tokenize(expression) _, ast = parse_cucumber_expression.call(expression, tokens, 0) ast[0] end private def parse_between(type, begin_token, end_token, parsers) lambda do |expression, tokens, current| return [0, nil] unless looking_at(tokens, current, begin_token) sub_current = current + 1 consumed, ast = parse_tokens_until(expression, parsers, tokens, sub_current, [end_token, TokenType::END_OF_LINE]) sub_current += consumed # endToken not found raise MissingEndToken.new(expression, begin_token, end_token, tokens[current]) unless looking_at(tokens, sub_current, end_token) # consumes endToken start = tokens[current].start _end = tokens[sub_current].end consumed = sub_current + 1 - current ast = [Node.new(type, ast, nil, start, _end)] return [consumed, ast] end end def parse_token(expression, parsers, tokens, start_at) parsers.each do |parser| consumed, ast = parser.call(expression, tokens, start_at) return [consumed, ast] unless consumed == 0 end # If configured correctly this will never happen raise 'No eligible parsers for ' + tokens end def parse_tokens_until(expression, parsers, tokens, start_at, end_tokens) current = start_at size = tokens.length ast = [] while current < size do break if looking_at_any(tokens, current, end_tokens) consumed, sub_ast = parse_token(expression, parsers, tokens, current) if consumed == 0 # If configured correctly this will never happen # Keep to avoid infinite loops raise 'No eligible parsers for ' + tokens end current += consumed ast += sub_ast end [current - start_at, ast] end def looking_at_any(tokens, at, token_types) token_types.detect { |token_type| looking_at(tokens, at, token_type) } end def looking_at(tokens, at, token) if at < 0 # If configured correctly this will never happen # Keep for completeness return token == TokenType::START_OF_LINE end return token == TokenType::END_OF_LINE if at >= tokens.length tokens[at].type == token end def split_alternatives(start, _end, alternation) separators = [] alternatives = [] alternative = [] alternation.each do |n| if NodeType::ALTERNATIVE == n.type separators.push(n) alternatives.push(alternative) alternative = [] else alternative.push(n) end end alternatives.push(alternative) create_alternative_nodes(start, _end, separators, alternatives) end def create_alternative_nodes(start, _end, separators, alternatives) alternatives.each_with_index.map do |n, i| if i == 0 right_separator = separators[i] Node.new(NodeType::ALTERNATIVE, n, nil, start, right_separator.start) elsif i == alternatives.length - 1 left_separator = separators[i - 1] Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, _end) else left_separator = separators[i - 1] right_separator = separators[i] Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, right_separator.start) end end end end end end