lib/cucumber/cucumber_expressions/cucumber_expression.rb



# frozen_string_literal: true

require 'cucumber/cucumber_expressions/argument'
require 'cucumber/cucumber_expressions/tree_regexp'
require 'cucumber/cucumber_expressions/errors'
require 'cucumber/cucumber_expressions/cucumber_expression_parser'

module Cucumber
  module CucumberExpressions
    class CucumberExpression
      ESCAPE_PATTERN = /([\\^\[({$.|?*+})\]])/.freeze

      def initialize(expression, parameter_type_registry)
        @expression = expression
        @parameter_type_registry = parameter_type_registry
        @parameter_types = []
        parser = CucumberExpressionParser.new
        ast = parser.parse(expression)
        pattern = rewrite_to_regex(ast)
        @tree_regexp = TreeRegexp.new(pattern)
      end

      def match(text)
        Argument.build(@tree_regexp, text, @parameter_types)
      end

      def source
        @expression
      end

      def regexp
        @tree_regexp.regexp
      end

      def to_s
        source.inspect
      end

      private

      def rewrite_to_regex(node)
        case node.type
        when NodeType::TEXT
          return escape_regex(node.text)
        when NodeType::OPTIONAL
          return rewrite_optional(node)
        when NodeType::ALTERNATION
          return rewrite_alternation(node)
        when NodeType::ALTERNATIVE
          return rewrite_alternative(node)
        when NodeType::PARAMETER
          return rewrite_parameter(node)
        when NodeType::EXPRESSION
          return rewrite_expression(node)
        else
          # Can't happen as long as the switch case is exhaustive
          raise "#{node.type}"
        end
      end

      def escape_regex(expression)
        expression.gsub(ESCAPE_PATTERN, '\\\\\1')
      end

      def rewrite_optional(node)
        assert_no_parameters(node) { |ast_node| raise ParameterIsNotAllowedInOptional.new(ast_node, @expression) }
        assert_no_optionals(node) { |ast_node| raise OptionalIsNotAllowedInOptional.new(ast_node, @expression) }
        assert_not_empty(node) { |ast_node| raise OptionalMayNotBeEmpty.new(ast_node, @expression) }
        regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('')
        "(?:#{regex})?"
      end

      def rewrite_alternation(node)
        # Make sure the alternative parts aren't empty and don't contain parameter types
        node.nodes.each { |alternative|
          raise AlternativeMayNotBeEmpty.new(alternative, @expression) if alternative.nodes.length == 0

          assert_not_empty(alternative) { |ast_node| raise AlternativeMayNotExclusivelyContainOptionals.new(ast_node, @expression) }
        }
        regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('|')
        "(?:#{regex})"
      end

      def rewrite_alternative(node)
        node.nodes.map { |last_node| rewrite_to_regex(last_node) }.join('')
      end

      def rewrite_parameter(node)
        name = node.text
        parameter_type = @parameter_type_registry.lookup_by_type_name(name)
        raise UndefinedParameterTypeError.new(node, @expression, name) if parameter_type.nil?

        @parameter_types.push(parameter_type)
        regexps = parameter_type.regexps
        return "(#{regexps[0]})" if regexps.length == 1

        "((?:#{regexps.join(')|(?:')}))"
      end

      def rewrite_expression(node)
        regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('')
        "^#{regex}$"
      end

      def assert_not_empty(node, &raise_error)
        text_nodes = node.nodes.select { |ast_node| NodeType::TEXT == ast_node.type }
        raise_error.call(node) if text_nodes.length == 0
      end

      def assert_no_parameters(node, &raise_error)
        nodes = node.nodes.select { |ast_node| NodeType::PARAMETER == ast_node.type }
        raise_error.call(nodes[0]) if nodes.length > 0
      end

      def assert_no_optionals(node, &raise_error)
        nodes = node.nodes.select { |ast_node| NodeType::OPTIONAL == ast_node.type }
        raise_error.call(nodes[0]) if nodes.length > 0
      end
    end
  end
end