class Cucumber::CucumberExpressions::CucumberExpressionParser

Public Instance Methods

parse(expression) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 10
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 Instance Methods

create_alternative_nodes(start, _end, separators, alternatives) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 186
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
looking_at(tokens, at, token) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 158
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
looking_at_any(tokens, at, token_types) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 154
def looking_at_any(tokens, at, token_types)
  token_types.detect { |token_type| looking_at(tokens, at, token_type) }
end
parse_between(type, begin_token, end_token, parsers) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 105
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
parse_token(expression, parsers, tokens, start_at) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 125
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
parse_tokens_until(expression, parsers, tokens, start_at, end_tokens) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 134
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
split_alternatives(start, _end, alternation) click to toggle source
# File lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb, line 169
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