cucumber-cucumber-expressions-19.0.0/0000755000004100000410000000000015152064305017622 5ustar www-datawww-datacucumber-cucumber-expressions-19.0.0/cucumber-cucumber-expressions.gemspec0000644000004100000410000000621415152064305027162 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: cucumber-cucumber-expressions 19.0.0 ruby lib Gem::Specification.new do |s| s.name = "cucumber-cucumber-expressions".freeze s.version = "19.0.0".freeze s.required_rubygems_version = Gem::Requirement.new(">= 3.2.8".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/cucumber/cucumber/issues", "changelog_uri" => "https://github.com/cucumber/cucumber-expressions/blob/main/CHANGELOG.md", "documentation_uri" => "https://github.com/cucumber/cucumber-expressions#readme", "mailing_list_uri" => "https://community.smartbear.com/category/cucumber/discussions/cucumberos", "source_code_uri" => "https://github.com/cucumber/cucumber-expressions/tree/main/ruby" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Aslak Helles\u00F8y".freeze] s.date = "1980-01-02" s.description = "Cucumber Expressions - a simpler alternative to Regular Expressions".freeze s.email = "cukes@googlegroups.com".freeze s.files = ["LICENSE".freeze, "lib/cucumber/cucumber_expressions/argument.rb".freeze, "lib/cucumber/cucumber_expressions/ast.rb".freeze, "lib/cucumber/cucumber_expressions/combinatorial_generated_expression_factory.rb".freeze, "lib/cucumber/cucumber_expressions/cucumber_expression.rb".freeze, "lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb".freeze, "lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb".freeze, "lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rb".freeze, "lib/cucumber/cucumber_expressions/errors.rb".freeze, "lib/cucumber/cucumber_expressions/expression_factory.rb".freeze, "lib/cucumber/cucumber_expressions/generated_expression.rb".freeze, "lib/cucumber/cucumber_expressions/group.rb".freeze, "lib/cucumber/cucumber_expressions/group_builder.rb".freeze, "lib/cucumber/cucumber_expressions/parameter_type.rb".freeze, "lib/cucumber/cucumber_expressions/parameter_type_matcher.rb".freeze, "lib/cucumber/cucumber_expressions/parameter_type_registry.rb".freeze, "lib/cucumber/cucumber_expressions/regular_expression.rb".freeze, "lib/cucumber/cucumber_expressions/tree_regexp.rb".freeze] s.homepage = "https://github.com/cucumber/cucumber-expressions".freeze s.licenses = ["MIT".freeze] s.rdoc_options = ["--charset=UTF-8".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.7".freeze) s.rubygems_version = "3.6.9".freeze s.summary = "cucumber-expressions-19.0.0".freeze s.specification_version = 4 s.add_runtime_dependency(%q.freeze, [">= 0".freeze]) s.add_development_dependency(%q.freeze, ["~> 13.3".freeze]) s.add_development_dependency(%q.freeze, ["~> 3.13".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.55.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.21.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 0.6.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 3.0.0".freeze]) end cucumber-cucumber-expressions-19.0.0/lib/0000755000004100000410000000000015152064305020370 5ustar www-datawww-datacucumber-cucumber-expressions-19.0.0/lib/cucumber/0000755000004100000410000000000015152064305022175 5ustar www-datawww-datacucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/0000755000004100000410000000000015152064305026444 5ustar www-datawww-datacucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/group.rb0000644000004100000410000000070215152064305030124 0ustar www-datawww-data# frozen_string_literal: true module Cucumber module CucumberExpressions class Group attr_reader :value, :start, :end, :children def initialize(value, start, _end, children) @value = value @start = start @end = _end @children = children end def values if children.nil? [self.value] else children.map(&:value) end end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/ast.rb0000644000004100000410000001014415152064305027560 0ustar www-datawww-data# frozen_string_literal: true module Cucumber module CucumberExpressions ESCAPE_CHARACTER = '\\' ALTERNATION_CHARACTER = '/' BEGIN_PARAMETER_CHARACTER = '{' END_PARAMETER_CHARACTER = '}' BEGIN_OPTIONAL_CHARACTER = '(' END_OPTIONAL_CHARACTER = ')' class Node attr_reader :type, :nodes, :token, :start, :end def initialize(type, nodes, token, start, ending) raise 'Either nodes or token must be defined' if nodes.nil? && token.nil? @type = type @nodes = nodes @token = token @start = start @end = ending end def text return @nodes.map { |value| value.text }.join('') if @token.nil? @token end def to_hash hash = Hash.new hash['type'] = @type hash['nodes'] = @nodes.map { |node| node.to_hash } unless @nodes.nil? hash['token'] = @token unless @token.nil? hash['start'] = @start hash['end'] = @end hash end end module NodeType TEXT = 'TEXT_NODE' OPTIONAL = 'OPTIONAL_NODE' ALTERNATION = 'ALTERNATION_NODE' ALTERNATIVE = 'ALTERNATIVE_NODE' PARAMETER = 'PARAMETER_NODE' EXPRESSION = 'EXPRESSION_NODE' end class Token attr_reader :type, :text, :start, :end def initialize(type, text, start, ending) @type, @text, @start, @end = type, text, start, ending end def self.is_escape_character(codepoint) codepoint.chr(Encoding::UTF_8) == ESCAPE_CHARACTER end def self.can_escape(codepoint) c = codepoint.chr(Encoding::UTF_8) if c == ' ' # TODO: Unicode whitespace? return true end case c when ESCAPE_CHARACTER true when ALTERNATION_CHARACTER true when BEGIN_PARAMETER_CHARACTER true when END_PARAMETER_CHARACTER true when BEGIN_OPTIONAL_CHARACTER true when END_OPTIONAL_CHARACTER true else false end end def self.type_of(codepoint) c = codepoint.chr(Encoding::UTF_8) if c == ' ' # TODO: Unicode whitespace? return TokenType::WHITE_SPACE end case c when ALTERNATION_CHARACTER TokenType::ALTERNATION when BEGIN_PARAMETER_CHARACTER TokenType::BEGIN_PARAMETER when END_PARAMETER_CHARACTER TokenType::END_PARAMETER when BEGIN_OPTIONAL_CHARACTER TokenType::BEGIN_OPTIONAL when END_OPTIONAL_CHARACTER TokenType::END_OPTIONAL else TokenType::TEXT end end def self.symbol_of(token) case token when TokenType::BEGIN_OPTIONAL return BEGIN_OPTIONAL_CHARACTER when TokenType::END_OPTIONAL return END_OPTIONAL_CHARACTER when TokenType::BEGIN_PARAMETER return BEGIN_PARAMETER_CHARACTER when TokenType::END_PARAMETER return END_PARAMETER_CHARACTER when TokenType::ALTERNATION return ALTERNATION_CHARACTER else return '' end end def self.purpose_of(token) case token when TokenType::BEGIN_OPTIONAL return 'optional text' when TokenType::END_OPTIONAL return 'optional text' when TokenType::BEGIN_PARAMETER return 'a parameter' when TokenType::END_PARAMETER return 'a parameter' when TokenType::ALTERNATION return 'alternation' else return '' end end def to_hash { 'type' => @type, 'text' => @text, 'start' => @start, 'end' => @end } end end module TokenType START_OF_LINE = 'START_OF_LINE' END_OF_LINE = 'END_OF_LINE' WHITE_SPACE = 'WHITE_SPACE' BEGIN_OPTIONAL = 'BEGIN_OPTIONAL' END_OPTIONAL = 'END_OPTIONAL' BEGIN_PARAMETER = 'BEGIN_PARAMETER' END_PARAMETER = 'END_PARAMETER' ALTERNATION = 'ALTERNATION' TEXT = 'TEXT' end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/generated_expression.rb0000644000004100000410000000152215152064305033206 0ustar www-datawww-data# frozen_string_literal: true module Cucumber module CucumberExpressions class GeneratedExpression attr_reader :parameter_types def initialize(expression_template, parameters_types) @expression_template, @parameter_types = expression_template, parameters_types end def source sprintf(@expression_template, *@parameter_types.map(&:name)) end def parameter_names usage_by_type_name = Hash.new(0) @parameter_types.map do |t| get_parameter_name(t.name, usage_by_type_name) end end private def get_parameter_name(type_name, usage_by_type_name) count = usage_by_type_name[type_name] count += 1 usage_by_type_name[type_name] = count count == 1 ? type_name : "#{type_name}#{count}" end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb0000644000004100000410000001753415152064305034443 0ustar www-datawww-data# 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 cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/tree_regexp.rb0000644000004100000410000000453615152064305031312 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/group_builder' require 'cucumber/cucumber_expressions/errors' module Cucumber module CucumberExpressions class TreeRegexp attr_reader :regexp, :group_builder def initialize(regexp) @regexp = regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp) @group_builder = create_group_builder(@regexp) end def match(s) match = @regexp.match(s) return nil if match.nil? group_indices = (0..match.length).to_a.to_enum @group_builder.build(match, group_indices) end private def is_non_capturing(source, i) # Regex is valid. Bounds check not required. if source[i + 1] != '?' # (X) return false end if source[i + 2] != '<' # (?:X) # (?idmsuxU-idmsuxU) # (?idmsux-idmsux:X) # (?=X) # (?!X) # (?>X) return true end if source[i + 3] == '=' || source[i + 3] == '!' # (?<=X) # (?X) raise CucumberExpressionError.new('Named capture groups are not supported. See https://github.com/cucumber/cucumber/issues/329') end private def create_group_builder(regexp) source = regexp.source stack = [GroupBuilder.new] group_start_stack = [] escaping = false char_class = false source.each_char.with_index do |c, i| if c == '[' && !escaping char_class = true elsif c == ']' && !escaping char_class = false elsif c == '(' && !escaping && !char_class group_start_stack.push(i) group_builder = GroupBuilder.new non_capturing = is_non_capturing(source, i) group_builder.set_non_capturing! if non_capturing stack.push(group_builder) elsif c == ')' && !escaping && !char_class gb = stack.pop group_start = group_start_stack.pop if gb.capturing? gb.source = source[group_start + 1...i] stack.last.add(gb) else gb.move_children_to(stack.last) end end escaping = c == '\\' && !escaping end stack.pop end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/group_builder.rb0000644000004100000410000000201515152064305031631 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/group' module Cucumber module CucumberExpressions class GroupBuilder attr_accessor :source def initialize @group_builders = [] @capturing = true end def add(group_builder) @group_builders.push(group_builder) end def build(match, group_indices) group_index = group_indices.next children = @group_builders.map { |gb| gb.build(match, group_indices) } Group.new( match[group_index], match.offset(group_index)[0], match.offset(group_index)[1], children.empty? ? nil : children ) end def set_non_capturing! @capturing = false end def capturing? @capturing end def move_children_to(group_builder) @group_builders.each do |child| group_builder.add(child) end end def children @group_builders end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/parameter_type_matcher.rb0000644000004100000410000000314215152064305033515 0ustar www-datawww-data# frozen_string_literal: true module Cucumber module CucumberExpressions class ParameterTypeMatcher attr_reader :parameter_type def initialize(parameter_type, regexp, text, match_position = 0) @parameter_type, @regexp, @text = parameter_type, regexp, text @match = @regexp.match(@text, match_position) end def advance_to(new_match_position) (new_match_position...@text.length).each do |advanced_position| matcher = self.class.new(parameter_type, @regexp, @text, advanced_position) return matcher if matcher.find && matcher.full_word? end self.class.new(parameter_type, @regexp, @text, @text.length) end def find !@match.nil? && !group.empty? end def full_word? space_before_match_or_sentence_start? && space_after_match_or_sentence_end? end def start @match.begin(0) end def group @match.captures[0] end def <=>(other) pos_comparison = start <=> other.start return pos_comparison if pos_comparison != 0 length_comparison = other.group.length <=> group.length return length_comparison if length_comparison != 0 0 end private def space_before_match_or_sentence_start? match_begin = @match.begin(0) match_begin == 0 || @text[match_begin - 1].match(/\p{Z}|\p{P}|\p{S}/) end def space_after_match_or_sentence_end? match_end = @match.end(0) match_end == @text.length || @text[match_end].match(/\p{Z}|\p{P}|\p{S}/) end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/argument.rb0000644000004100000410000000235715152064305030622 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/group' require 'cucumber/cucumber_expressions/errors' module Cucumber module CucumberExpressions class Argument attr_reader :group, :parameter_type def self.build(tree_regexp, text, parameter_types) group = tree_regexp.match(text) return nil if group.nil? arg_groups = group.children.nil? ? [] : group.children if arg_groups.length != parameter_types.length raise CucumberExpressionError.new( "Expression #{tree_regexp.regexp.inspect} has #{arg_groups.length} capture groups (#{arg_groups.map(&:value)}), " \ "but there were #{parameter_types.length} parameter types (#{parameter_types.map(&:name)})" ) end parameter_types.zip(arg_groups).map do |parameter_type, arg_group| Argument.new(arg_group, parameter_type) end end def initialize(group, parameter_type) @group, @parameter_type = group, parameter_type end def value(self_obj = :nil) raise 'No self_obj' if self_obj == :nil group_values = @group ? @group.values : nil @parameter_type.transform(self_obj, group_values) end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/expression_factory.rb0000644000004100000410000000144615152064305032724 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/errors' require 'cucumber/cucumber_expressions/cucumber_expression' require 'cucumber/cucumber_expressions/regular_expression' module Cucumber module CucumberExpressions class ExpressionFactory def initialize(parameter_type_registry) @parameter_type_registry = parameter_type_registry end def create_expression(string_or_regexp) case string_or_regexp when String then CucumberExpression.new(string_or_regexp, @parameter_type_registry) when Regexp then RegularExpression.new(string_or_regexp, @parameter_type_registry) else raise CucumberExpressionError.new("Can't create an expression from #{string_or_regexp.inspect}") end end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/cucumber_expression.rb0000644000004100000410000000766415152064305033072 0ustar www-datawww-data# 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 cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/parameter_type.rb0000644000004100000410000000563115152064305032017 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/errors' module Cucumber module CucumberExpressions class ParameterType ILLEGAL_PARAMETER_NAME_PATTERN = /([\[\]()$.|?*+])/.freeze UNESCAPE_PATTERN = /(\\([\[$.|?*+\]]))/.freeze attr_reader :name, :type, :transformer, :use_for_snippets, :prefer_for_regexp_match, :regexps def self.check_parameter_type_name(type_name) raise CucumberExpressionError.new("Illegal character in parameter name {#{type_name}}. Parameter names may not contain '[]()$.|?*+'") unless is_valid_parameter_type_name(type_name) end def self.is_valid_parameter_type_name(type_name) unescaped_type_name = type_name.gsub(UNESCAPE_PATTERN) { Regexp.last_match(2) } !(ILLEGAL_PARAMETER_NAME_PATTERN =~ unescaped_type_name) end # Create a new Parameter # # @param name the name of the parameter type # @param regexp [Array] list of regexps for capture groups. A single regexp can also be used # @param type the return type of the transformed # @param transformer lambda that transforms a String to (possibly) another type # @param use_for_snippets true if this should be used for snippet generation # @param prefer_for_regexp_match true if this should be preferred over similar types # def initialize(name, regexp, type, transformer, use_for_snippets, prefer_for_regexp_match) raise "regexp can't be nil" if regexp.nil? raise "type can't be nil" if type.nil? raise "transformer can't be nil" if transformer.nil? raise "use_for_snippets can't be nil" if use_for_snippets.nil? raise "prefer_for_regexp_match can't be nil" if prefer_for_regexp_match.nil? self.class.check_parameter_type_name(name) unless name.nil? @name, @type, @transformer, @use_for_snippets, @prefer_for_regexp_match = name, type, transformer, use_for_snippets, prefer_for_regexp_match @regexps = string_array(regexp) end def transform(self_obj, group_values) self_obj.instance_exec(*group_values, &@transformer) end def <=>(other) return -1 if prefer_for_regexp_match && !other.prefer_for_regexp_match return 1 if other.prefer_for_regexp_match && !prefer_for_regexp_match return name <=> other.name end private def string_array(regexps) array = regexps.is_a?(Array) ? regexps : [regexps] array.map { |regexp| regexp.is_a?(String) ? regexp : regexp_source(regexp) } end def regexp_source(regexp) [ 'EXTENDED', 'IGNORECASE', 'MULTILINE' ].each do |option_name| option = Regexp.const_get(option_name) raise CucumberExpressionError.new("ParameterType Regexps can't use option Regexp::#{option_name}") if regexp.options & option != 0 end regexp.source end end end end ././@LongLink0000644000000000000000000000015000000000000011577 Lustar rootrootcucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rbcucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/cucumber_expression_generator0000644000004100000410000000673215152064305034531 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/parameter_type_matcher' require 'cucumber/cucumber_expressions/generated_expression' require 'cucumber/cucumber_expressions/combinatorial_generated_expression_factory' module Cucumber module CucumberExpressions class CucumberExpressionGenerator def initialize(parameter_type_registry) @parameter_type_registry = parameter_type_registry end def generate_expressions(text) parameter_type_combinations = [] parameter_type_matchers = create_parameter_type_matchers(text) expression_template = +'' pos = 0 loop do matching_parameter_type_matchers = [] parameter_type_matchers.each do |parameter_type_matcher| advanced_parameter_type_matcher = parameter_type_matcher.advance_to(pos) matching_parameter_type_matchers.push(advanced_parameter_type_matcher) if advanced_parameter_type_matcher.find end if matching_parameter_type_matchers.any? matching_parameter_type_matchers = matching_parameter_type_matchers.sort best_parameter_type_matcher = matching_parameter_type_matchers[0] best_parameter_type_matchers = matching_parameter_type_matchers.select do |m| (m <=> best_parameter_type_matcher).zero? end # Build a list of parameter types without duplicates. The reason there # might be duplicates is that some parameter types have more than one regexp, # which means multiple ParameterTypeMatcher objects will have a reference to the # same ParameterType. # We're sorting the list so prefer_for_regexp_match parameter types are listed first. # Users are most likely to want these, so they should be listed at the top. parameter_types = [] best_parameter_type_matchers.each do |parameter_type_matcher| parameter_types.push(parameter_type_matcher.parameter_type) unless parameter_types.include?(parameter_type_matcher.parameter_type) end parameter_types.sort! parameter_type_combinations.push(parameter_types) expression_template += escape(text.slice(pos...best_parameter_type_matcher.start)) expression_template += '{%s}' pos = best_parameter_type_matcher.start + best_parameter_type_matcher.group.length else break end break if pos >= text.length end expression_template += escape(text.slice(pos..-1)) CombinatorialGeneratedExpressionFactory.new( expression_template, parameter_type_combinations ).generate_expressions end private def create_parameter_type_matchers(text) parameter_matchers = [] @parameter_type_registry.parameter_types.each do |parameter_type| parameter_matchers += create_parameter_type_matchers2(parameter_type, text) if parameter_type.use_for_snippets end parameter_matchers end def create_parameter_type_matchers2(parameter_type, text) regexps = parameter_type.regexps regexps.map do |regexp| regexp = Regexp.new("(#{regexp})") ParameterTypeMatcher.new(parameter_type, regexp, text, 0) end end def escape(s) s.gsub(/%/, '%%') .gsub(/\(/, '\\(') .gsub(/{/, '\\{') .gsub(/\//, '\\/') end end end end ././@LongLink0000644000000000000000000000016500000000000011605 Lustar rootrootcucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/combinatorial_generated_expression_factory.rbcucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/combinatorial_generated_expre0000644000004100000410000000300515152064305034431 0ustar www-datawww-data# frozen_string_literal: true require('cucumber/cucumber_expressions/generated_expression') module Cucumber module CucumberExpressions class CombinatorialGeneratedExpressionFactory def initialize(expression_template, parameter_type_combinations) @expression_template = expression_template @parameter_type_combinations = parameter_type_combinations end def generate_expressions generated_expressions = [] generate_permutations(generated_expressions, 0, []) generated_expressions end # 256 generated expressions ought to be enough for anybody MAX_EXPRESSIONS = 256 def generate_permutations(generated_expressions, depth, current_parameter_types) return if generated_expressions.length >= MAX_EXPRESSIONS if depth == @parameter_type_combinations.length generated_expression = GeneratedExpression.new(@expression_template, current_parameter_types) generated_expressions.push(generated_expression) return end (0...@parameter_type_combinations[depth].length).each do |i| # Avoid recursion if no elements can be added. break if generated_expressions.length >= MAX_EXPRESSIONS new_current_parameter_types = current_parameter_types.dup # clone new_current_parameter_types.push(@parameter_type_combinations[depth][i]) generate_permutations(generated_expressions, depth + 1, new_current_parameter_types) end end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/regular_expression.rb0000644000004100000410000000234315152064305032713 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/argument' require 'cucumber/cucumber_expressions/parameter_type' require 'cucumber/cucumber_expressions/tree_regexp' module Cucumber module CucumberExpressions class RegularExpression def initialize(expression_regexp, parameter_type_registry) @expression_regexp = expression_regexp @parameter_type_registry = parameter_type_registry @tree_regexp = TreeRegexp.new(@expression_regexp) end def match(text) parameter_types = @tree_regexp.group_builder.children.map do |group_builder| parameter_type_regexp = group_builder.source @parameter_type_registry.lookup_by_regexp( parameter_type_regexp, @expression_regexp, text ) || ParameterType.new( nil, parameter_type_regexp, String, ->(*s) { s[0] }, false, false ) end Argument.build(@tree_regexp, text, parameter_types) end def regexp @expression_regexp end def source @expression_regexp.source end def to_s regexp.inspect end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/errors.rb0000644000004100000410000001604115152064305030307 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/ast' module Cucumber module CucumberExpressions class CucumberExpressionError < StandardError def build_message(index, expression, pointer, problem, solution) m = <<~ERROR This Cucumber Expression has a problem at column #{index + 1}: #{expression} #{pointer} #{problem}. #{solution} ERROR m.strip end def point_at(index) ' ' * index + '^' end def point_at_located(node) pointer = [point_at(node.start)] if node.start + 1 < node.end for _ in node.start + 1...node.end - 1 pointer.push('-') end pointer.push('^') end pointer.join('') end end class AlternativeMayNotExclusivelyContainOptionals < CucumberExpressionError def initialize(node, expression) super( build_message( node.start, expression, point_at_located(node), 'An alternative may not exclusively contain optionals', "If you did not mean to use an optional you can use '\\(' to escape the '('" ) ) end end class AlternativeMayNotBeEmpty < CucumberExpressionError def initialize(node, expression) super( build_message( node.start, expression, point_at_located(node), 'Alternative may not be empty', "If you did not mean to use an alternative you can use '\\/' to escape the '/'" ) ) end end class CantEscape < CucumberExpressionError def initialize(expression, index) super( build_message( index, expression, point_at(index), "Only the characters '{', '}', '(', ')', '\\', '/' and whitespace can be escaped", "If you did mean to use an '\\' you can use '\\\\' to escape it" ) ) end end class OptionalMayNotBeEmpty < CucumberExpressionError def initialize(node, expression) super( build_message( node.start, expression, point_at_located(node), 'An optional must contain some text', "If you did not mean to use an optional you can use '\\(' to escape the '('" ) ) end end class ParameterIsNotAllowedInOptional < CucumberExpressionError def initialize(node, expression) super( build_message( node.start, expression, point_at_located(node), 'An optional may not contain a parameter type', "If you did not mean to use an parameter type you can use '\\{' to escape the '{'" ) ) end end class OptionalIsNotAllowedInOptional < CucumberExpressionError def initialize(node, expression) super( build_message( node.start, expression, point_at_located(node), 'An optional may not contain an other optional', "If you did not mean to use an optional type you can use '\\(' to escape the '('. For more complicated expressions consider using a regular expression instead." ) ) end end class TheEndOfLineCannotBeEscaped < CucumberExpressionError def initialize(expression) index = expression.codepoints.length - 1 super( build_message( index, expression, point_at(index), 'The end of line can not be escaped', "You can use '\\\\' to escape the '\\'" ) ) end end class MissingEndToken < CucumberExpressionError def initialize(expression, begin_token, end_token, current) begin_symbol = Token::symbol_of(begin_token) end_symbol = Token::symbol_of(end_token) purpose = Token::purpose_of(begin_token) super( build_message( current.start, expression, point_at_located(current), "The '#{begin_symbol}' does not have a matching '#{end_symbol}'", "If you did not intend to use #{purpose} you can use '\\#{begin_symbol}' to escape the #{purpose}" ) ) end end class AlternationNotAllowedInOptional < CucumberExpressionError def initialize(expression, current) super( build_message( current.start, expression, point_at_located(current), 'An alternation can not be used inside an optional', "If you did not mean to use an alternation you can use '\\/' to escape the '/'. Otherwise rephrase your expression or consider using a regular expression instead." ) ) end end class InvalidParameterTypeName < CucumberExpressionError def initialize(type_name) super("Illegal character in parameter name {#{type_name}}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'") end end class InvalidParameterTypeNameInNode < CucumberExpressionError def initialize(expression, token) super( build_message( token.start, expression, point_at_located(token), "Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", 'Did you mean to use a regular expression?' ) ) end end class UndefinedParameterTypeError < CucumberExpressionError attr_reader :undefined_parameter_type_name def initialize(node, expression, undefined_parameter_type_name) super( build_message( node.start, expression, point_at_located(node), "Undefined parameter type '#{undefined_parameter_type_name}'", "Please register a ParameterType for '#{undefined_parameter_type_name}'" ) ) @undefined_parameter_type_name = undefined_parameter_type_name end end class AmbiguousParameterTypeError < CucumberExpressionError def initialize(parameter_type_regexp, expression_regexp, parameter_types, generated_expressions) super(<<~ERROR) Your Regular Expression /#{expression_regexp.source}/ matches multiple parameter types with regexp /#{parameter_type_regexp}/: #{parameter_type_names(parameter_types)} I couldn't decide which one to use. You have two options: 1) Use a Cucumber Expression instead of a Regular Expression. Try one of these: #{expressions(generated_expressions)} 2) Make one of the parameter types preferential and continue to use a Regular Expression. ERROR end private def parameter_type_names(parameter_types) parameter_types.map { |p| "{#{p.name}}" }.join("\n ") end def expressions(generated_expressions) generated_expressions.map { |ge| ge.source }.join("\n ") end end end end cucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/parameter_type_registry.rb0000644000004100000410000001026015152064305033741 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/parameter_type' require 'cucumber/cucumber_expressions/errors' require 'cucumber/cucumber_expressions/cucumber_expression_generator' require 'bigdecimal' module Cucumber module CucumberExpressions class ParameterTypeRegistry INTEGER_REGEXPS = [/-?\d+/, /\d+/].freeze FLOAT_REGEXP = /(?=.*\d.*)[-+]?\d*(?:\.(?=\d.*))?\d*(?:\d+[E][-+]?\d+)?/.freeze WORD_REGEXP = /[^\s]+/.freeze STRING_REGEXP = /"([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)'/.freeze ANONYMOUS_REGEXP = /.*/.freeze def initialize @parameter_type_by_name = {} @parameter_types_by_regexp = Hash.new { |hash, regexp| hash[regexp] = [] } define_parameter_type(ParameterType.new('int', INTEGER_REGEXPS, Integer, ->(s = nil) { s && s.to_i }, true, true)) define_parameter_type(ParameterType.new('float', FLOAT_REGEXP, Float, ->(s = nil) { s && s.to_f }, true, false)) define_parameter_type(ParameterType.new('word', WORD_REGEXP, String, ->(s = nil) { s }, false, false)) define_parameter_type(ParameterType.new('string', STRING_REGEXP, String, ->(s1, s2) { arg = s1 != nil ? s1 : s2; arg.gsub('\\"', '"').gsub("\\'", "'") }, true, false)) define_parameter_type(ParameterType.new('', ANONYMOUS_REGEXP, String, ->(s = nil) { s }, false, true)) define_parameter_type(ParameterType.new('bigdecimal', FLOAT_REGEXP, BigDecimal, ->(s = nil) { BigDecimal(s) }, false, false)) define_parameter_type(ParameterType.new('biginteger', INTEGER_REGEXPS, Integer, ->(s = nil) { s && s.to_i }, false, false)) define_parameter_type(ParameterType.new('byte', INTEGER_REGEXPS, Integer, ->(s = nil) { s && s.to_i }, false, false)) define_parameter_type(ParameterType.new('short', INTEGER_REGEXPS, Integer, ->(s = nil) { s && s.to_i }, false, false)) define_parameter_type(ParameterType.new('long', INTEGER_REGEXPS, Integer, ->(s = nil) { s && s.to_i }, false, false)) define_parameter_type(ParameterType.new('double', FLOAT_REGEXP, Float, ->(s = nil) { s && s.to_f }, false, false)) end def lookup_by_type_name(name) @parameter_type_by_name[name] end def lookup_by_regexp(parameter_type_regexp, expression_regexp, text) parameter_types = @parameter_types_by_regexp[parameter_type_regexp] return nil if parameter_types.nil? if parameter_types.length > 1 && !parameter_types[0].prefer_for_regexp_match # We don't do this check on insertion because we only want to restrict # ambiguity when we look up by Regexp. Users of CucumberExpression should # not be restricted. generated_expressions = CucumberExpressionGenerator.new(self).generate_expressions(text) raise AmbiguousParameterTypeError.new(parameter_type_regexp, expression_regexp, parameter_types, generated_expressions) end parameter_types.first end def parameter_types @parameter_type_by_name.values end def define_parameter_type(parameter_type) if parameter_type.name != nil if @parameter_type_by_name.has_key?(parameter_type.name) if parameter_type.name.length == 0 raise CucumberExpressionError.new('The anonymous parameter type has already been defined') else raise CucumberExpressionError.new("There is already a parameter with name #{parameter_type.name}") end end @parameter_type_by_name[parameter_type.name] = parameter_type end parameter_type.regexps.each do |parameter_type_regexp| parameter_types = @parameter_types_by_regexp[parameter_type_regexp] if parameter_types.any? && parameter_types[0].prefer_for_regexp_match && parameter_type.prefer_for_regexp_match raise CucumberExpressionError.new( "There can only be one preferential parameter type per regexp. The regexp /#{parameter_type_regexp}/ is used for two: {#{parameter_types[0].name}} and {#{parameter_type.name}}" ) end parameter_types.push(parameter_type) parameter_types.sort! end end end end end ././@LongLink0000644000000000000000000000015000000000000011577 Lustar rootrootcucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rbcucumber-cucumber-expressions-19.0.0/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer0000644000004100000410000000525515152064305034554 0ustar www-datawww-data# frozen_string_literal: true require 'cucumber/cucumber_expressions/ast' require 'cucumber/cucumber_expressions/errors' module Cucumber module CucumberExpressions class CucumberExpressionTokenizer def tokenize(expression) @expression = expression tokens = [] @buffer = [] previous_token_type = TokenType::START_OF_LINE treat_as_text = false @escaped = 0 @buffer_start_index = 0 codepoints = expression.codepoints tokens.push(Token.new(TokenType::START_OF_LINE, '', 0, 0)) if codepoints.empty? codepoints.each do |codepoint| if !treat_as_text && Token.is_escape_character(codepoint) @escaped += 1 treat_as_text = true next end current_token_type = token_type_of(codepoint, treat_as_text) treat_as_text = false if should_create_new_token?(previous_token_type, current_token_type) token = convert_buffer_to_token(previous_token_type) previous_token_type = current_token_type @buffer.push(codepoint) tokens.push(token) else previous_token_type = current_token_type @buffer.push(codepoint) end end if @buffer.length > 0 token = convert_buffer_to_token(previous_token_type) tokens.push(token) end raise TheEndOfLineCannotBeEscaped.new(expression) if treat_as_text tokens.push(Token.new(TokenType::END_OF_LINE, '', codepoints.length, codepoints.length)) tokens end private # TODO: Make these lambdas def convert_buffer_to_token(token_type) escape_tokens = 0 if token_type == TokenType::TEXT escape_tokens = @escaped @escaped = 0 end consumed_index = @buffer_start_index + @buffer.length + escape_tokens t = Token.new( token_type, @buffer.map { |codepoint| codepoint.chr(Encoding::UTF_8) }.join(''), @buffer_start_index, consumed_index ) @buffer = [] @buffer_start_index = consumed_index t end def token_type_of(codepoint, treat_as_text) return Token.type_of(codepoint) unless treat_as_text return TokenType::TEXT if Token.can_escape(codepoint) raise CantEscape.new(@expression, @buffer_start_index + @buffer.length + @escaped) end def should_create_new_token?(previous_token_type, current_token_type) current_token_type != previous_token_type || (current_token_type != TokenType::WHITE_SPACE && current_token_type != TokenType::TEXT) end end end end cucumber-cucumber-expressions-19.0.0/LICENSE0000644000004100000410000000207615152064305020634 0ustar www-datawww-dataMIT License Copyright (c) 2016 Cucumber Ltd and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.