diff --git a/config/flay.yml b/config/flay.yml index 3e35074a..092e69b3 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 16 -total_score: 1181 +total_score: 1290 diff --git a/lib/mutant.rb b/lib/mutant.rb index 6c69ee38..f755a2dc 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -14,6 +14,7 @@ require 'parallel' require 'parser' require 'parser/current' require 'pathname' +require 'regexp_parser' require 'set' require 'stringio' require 'unparser' @@ -43,12 +44,23 @@ require 'mutant/version' require 'mutant/env' require 'mutant/env/bootstrap' require 'mutant/util' +require 'mutant/registry' require 'mutant/ast' require 'mutant/ast/sexp' require 'mutant/ast/types' require 'mutant/ast/nodes' require 'mutant/ast/named_children' require 'mutant/ast/node_predicates' +require 'mutant/ast/regexp' +require 'mutant/ast/regexp/transformer' +require 'mutant/ast/regexp/transformer/direct' +require 'mutant/ast/regexp/transformer/text' +require 'mutant/ast/regexp/transformer/recursive' +require 'mutant/ast/regexp/transformer/quantifier' +require 'mutant/ast/regexp/transformer/options_group' +require 'mutant/ast/regexp/transformer/character_set' +require 'mutant/ast/regexp/transformer/root' +require 'mutant/ast/regexp/transformer/alternative' require 'mutant/ast/meta' require 'mutant/ast/meta/send' require 'mutant/ast/meta/const' @@ -72,13 +84,13 @@ require 'mutant/parallel/source' require 'mutant/warning_filter' require 'mutant/require_highjack' require 'mutant/mutation' -require 'mutant/registry' require 'mutant/mutator' require 'mutant/mutator/util' require 'mutant/mutator/util/array' require 'mutant/mutator/util/symbol' require 'mutant/mutator/node' require 'mutant/mutator/node/generic' +require 'mutant/mutator/node/regexp' require 'mutant/mutator/node/literal' require 'mutant/mutator/node/literal/boolean' require 'mutant/mutator/node/literal/range' diff --git a/lib/mutant/ast/regexp.rb b/lib/mutant/ast/regexp.rb new file mode 100644 index 00000000..0d44bca4 --- /dev/null +++ b/lib/mutant/ast/regexp.rb @@ -0,0 +1,53 @@ +module Mutant + module AST + # Regexp source mapper + module Regexp + UNSUPPORTED_EXPRESSION_TYPE = :conditional + + private_constant(*constants(false)) + + # Parse regex string into expression + # + # @param regexp [String] + # + # @return [Regexp::Expression] + def self.parse(regexp) + ::Regexp::Parser.parse( + regexp, + "ruby/#{RUBY_VERSION.split('.').first(2).join('.')}" + ) + end + + # Check if expression is supported by mapper + # + # @param expression [Regexp::Expression] + # + # @return [Boolean] + def self.supported?(expression) + expression.terminal? || expression.all? do |subexp| + !subexp.type.equal?(UNSUPPORTED_EXPRESSION_TYPE) && supported?(subexp) + end + end + + # Convert expression into ast node + # + # @param expression [Regexp::Expression] + # + # @return [Parser::AST::Node] + def self.to_ast(expression) + ast_type = :"regexp_#{expression.token}_#{expression.type}" + + Transformer.lookup(ast_type).to_ast(expression) + end + + # Convert node into expression + # + # @param node [Parser::AST::Node] + # + # @return [Regexp::Expression] + def self.to_expression(node) + Transformer.lookup(node.type).to_expression(node) + end + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer.rb b/lib/mutant/ast/regexp/transformer.rb new file mode 100644 index 00000000..482431c2 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer.rb @@ -0,0 +1,185 @@ +module Mutant + module AST + module Regexp + # Regexp bijective mapper + # + # Transforms parsed regular expression representation from + # `Regexp::Expression` instances (provided by `regexp_parser`) into + # equivalent representations using `Parser::AST::Node` + class Transformer + include AbstractType + + REGISTRY = Registry.new + + # Lookup transformer class for regular expression node type + # + # @param type [Symbol] + # + # @return [Class] + def self.lookup(type) + REGISTRY.lookup(type) + end + + # Register transformer class as responsible for handling node type + # + # @param type [Symbol] + # + # @return [undefined] + def self.register(type) + REGISTRY.register(type, self) + end + private_class_method :register + + # Transform expression + # + # @param expression [Regexp::Expression] + # + # @return [Parser::AST::Node] + def self.to_ast(expression) + self::ExpressionToAST.call(expression) + end + + # Transform node + # + # @param node [Parser::AST::Node] + # + # @return [Regexp::Expression] + def self.to_expression(node) + self::ASTToExpression.call(node) + end + + # Abstract expression transformer + class ExpressionToAST + PREFIX = :regexp + + include Concord.new(:expression), Procto.call, AST::Sexp, AbstractType, Adamantium + + private + + # Node with provided children using node type constructed in `type` + # + # @param [Object,Parser::AST::Node] child of node + # + # @return [Parser::AST::Node] + def ast(*children) + s(type, *children) + end + + # Wrap provided node in a quantifier + # + # @param node [Parser::AST::Node] + # + # @return [Parser::AST::Node] + # quantifier node wrapping provided node if expression is quantified + # + # @return [Parser::AST::Node] + # original node otherwise + def quantify(node) + return node unless expression.quantified? + + Quantifier.to_ast(expression.quantifier).append(node) + end + + # Transformed children of expression + # + # @return [Array] + def children + expression.expressions.map(&Regexp.method(:to_ast)) + end + + # Node type constructed from token and type of `Regexp::Expression` + # + # @return [Symbol] + def type + :"#{PREFIX}_#{expression.token}_#{expression.type}" + end + end # ExpressionToAST + + # Abstract node transformer + class ASTToExpression + include Concord.new(:node), Procto.call, AbstractType, Adamantium + + # Call generic transform method and freeze result + # + # @return [Regexp::Expression] + def call + transform.freeze + end + + private + + # Transformation of ast into expression + # + # @return [Regexp::Expression] + abstract_method :transform + + # Transformed children of node + # + # @return [Array] + def subexpressions + node.children.map(&Regexp.public_method(:to_expression)) + end + end # ASTToExpression + + # Mixin for node transformers + # + # Helps construct a mapping from Parser::AST::Node domain to + # Regexp::Expression domain + module LookupTable + Mapping = Class.new.include(Concord::Public.new(:token, :regexp_class)) + + # Table mapping ast types to object information for regexp domain + class Table + + # Coerce array of mapping information into structured table + # + # @param [Array(Symbol, Array, Class)] + # + # @return [Table] + def self.create(*rows) + table = rows.map do |ast_type, token, klass| + [ast_type, Mapping.new(::Regexp::Token.new(*token), klass)] + end.to_h + + new(table) + end + + include Concord.new(:table), Adamantium + + # Types defined by the table + # + # @return [Array] + def types + table.keys + end + + # Lookup mapping information given an ast node type + # + # @param type [Symbol] + # + # @return [Mapping] + def lookup(type) + table.fetch(type) + end + end # Table + + private + + # Lookup expression token given node type + # + # @return [Regexp::Token] + def expression_token + self.class::TABLE.lookup(node.type).token + end + + # Lookup regexp class given node type + # + # @return [Class] + def expression_class + self.class::TABLE.lookup(node.type).regexp_class + end + end # LookupTable + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/alternative.rb b/lib/mutant/ast/regexp/transformer/alternative.rb new file mode 100644 index 00000000..5e4db606 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/alternative.rb @@ -0,0 +1,39 @@ +module Mutant + module AST + module Regexp + class Transformer + # Transformer for Regexp `alternative` nodes + # + # This transformer is very similar to the generic recursive mapper + # except for the fact that the `Regexp::Expression` class for + # `alternative` nodes has a unique constructor + class Alternative < self + register :regexp_sequence_expression + + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + ExpressionToAST = Class.new(Recursive::ExpressionToAST) + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + # Alternative instance with dummy values for `level`, `set_level`, + # and `conditional_level`. These values do not affect unparsing + ALTERNATIVE = IceNine.deep_freeze( + ::Regexp::Expression::Alternative.new(0, 0, 0) + ) + + private + + # Transform ast into expression + # + # @return [Regexp::Expression::Alternative] + def transform + ALTERNATIVE.dup.tap do |alt| + alt.expressions = subexpressions + end + end + end # ASTToExpression + end # Alternative + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/character_set.rb b/lib/mutant/ast/regexp/transformer/character_set.rb new file mode 100644 index 00000000..4030aa03 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/character_set.rb @@ -0,0 +1,46 @@ +module Mutant + module AST + module Regexp + class Transformer + # Transformer for character sets + # + # The `Regexp::Expression` representation of a character set + # is unique due to its usage of the `#members` attribute which + # is why it gets its own transformer + class CharacterSet < self + register :regexp_character_set + + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + class ExpressionToAST < Transformer::ExpressionToAST + # Transform character set expression into node + # + # @return [Parser::AST::Node] + def call + quantify(ast(*expression.members)) + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + CHARACTER_SET = IceNine.deep_freeze( + ::Regexp::Expression::CharacterSet.new( + ::Regexp::Token.new(:set, :character, '[') + ) + ) + + private + + # Transform node into expression + # + # @return [Regexp::Expression] + def transform + CHARACTER_SET.dup.tap do |expression| + expression.members = node.children + end + end + end # ASTToExpression + end # CharacterSet + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/direct.rb b/lib/mutant/ast/regexp/transformer/direct.rb new file mode 100644 index 00000000..750542c9 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/direct.rb @@ -0,0 +1,99 @@ +module Mutant + module AST + module Regexp + class Transformer + # Transformer for nodes which map directly to other domain + # + # A node maps "directly" to another domain if the node never + # has children or text which needs to be preserved for a mapping + # + # @example direct mapping + # + # input = /\d/ + # expression = Regexp::Parser.parse(input).first + # node = Transformer::Direct.to_ast(expression) + # + # # the digit type always has the same text and no children + # expression.text # => "\\d" + # expression.terminal? # => true + # + # # therefore the `Parser::AST::Node` is always the same + # node # => s(:regexp_digit_type) + class Direct < self + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + class ExpressionToAST < Transformer::ExpressionToAST + # Transform expression into node + # + # @return [Parser::AST::Node] + def call + quantify(ast) + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + include LookupTable + + # rubocop:disable LineLength + TABLE = Table.create( + [:regexp_one_or_more_escape, [:escape, :one_or_more, '\+'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_zero_or_one_escape, [:escape, :zero_or_one, '\?'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_alternation_escape, [:escape, :alternation, '\|'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_group_open_escape, [:escape, :group_open, '\('], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_group_close_escape, [:escape, :group_close, '\)'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_interval_open_escape, [:escape, :interval_open, '\{'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_interval_close_escape, [:escape, :interval_close, '\}'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_newline_escape, [:escape, :newline, '\n'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_zero_or_more_escape, [:escape, :zero_or_more, '\*'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_carriage_escape, [:escape, :carriage, '\r'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_dot_escape, [:escape, :dot, '\.'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_set_open_escape, [:escape, :set_open, '\['], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_set_close_escape, [:escape, :set_close, '\]'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_eol_escape, [:escape, :eol, '\$'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_bell_escape, [:escape, :bell, '\a'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_escape_escape, [:escape, :escape, '\e'], ::Regexp::Expression::EscapeSequence::AsciiEscape], + [:regexp_form_feed_escape, [:escape, :form_feed, '\f'], ::Regexp::Expression::EscapeSequence::FormFeed], + [:regexp_vertical_tab_escape, [:escape, :vertical_tab, '\v'], ::Regexp::Expression::EscapeSequence::VerticalTab], + [:regexp_mark_keep, [:keep, :mark, '\K'], ::Regexp::Expression::Keep::Mark], + [:regexp_bos_anchor, [:anchor, :bos, '\\A'], ::Regexp::Expression::Anchor::BeginningOfString], + [:regexp_match_start_anchor, [:anchor, :match_start, '\\G'], ::Regexp::Expression::Anchor::MatchStart], + [:regexp_word_boundary_anchor, [:anchor, :word_boundary, '\b'], ::Regexp::Expression::Anchor::WordBoundary], + [:regexp_eos_ob_eol_anchor, [:anchor, :eos_ob_eol, '\\Z'], ::Regexp::Expression::Anchor::EndOfStringOrBeforeEndOfLine], + [:regexp_eos_anchor, [:anchor, :eos, '\\z'], ::Regexp::Expression::Anchor::EndOfString], + [:regexp_bol_anchor, [:anchor, :bol, '^'], ::Regexp::Expression::Anchor::BeginningOfLine], + [:regexp_eol_anchor, [:anchor, :eol, '$'], ::Regexp::Expression::Anchor::EndOfLine], + [:regexp_nonword_boundary_anchor, [:anchor, :nonword_boundary, '\\B'], ::Regexp::Expression::Anchor::NonWordBoundary], + [:regexp_alpha_property, [:property, :alpha, '\p{Alpha}'], ::Regexp::Expression::UnicodeProperty::Alpha], + [:regexp_script_arabic_property, [:property, :script_arabic, '\p{Arabic}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_script_hangul_property, [:property, :script_hangul, '\p{Hangul}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_script_han_property, [:property, :script_han, '\p{Han}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_script_hiragana_property, [:property, :script_hiragana, '\p{Hiragana}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_script_katakana_property, [:property, :script_katakana, '\p{Katakana}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_letter_any_property, [:property, :letter_any, '\p{L}'], ::Regexp::Expression::UnicodeProperty::Letter::Any], + [:regexp_digit_type, [:type, :digit, '\d'], ::Regexp::Expression::CharacterType::Digit], + [:regexp_space_type, [:type, :space, '\s'], ::Regexp::Expression::CharacterType::Space], + [:regexp_word_type, [:type, :word, '\w'], ::Regexp::Expression::CharacterType::Word], + [:regexp_hex_type, [:type, :hex, '\h'], ::Regexp::Expression::CharacterType::Hex], + [:regexp_nonhex_type, [:type, :nonhex, '\H'], ::Regexp::Expression::CharacterType::NonHex], + [:regexp_nondigit_type, [:type, :nondigit, '\D'], ::Regexp::Expression::CharacterType::NonDigit], + [:regexp_nonspace_type, [:type, :nonspace, '\S'], ::Regexp::Expression::CharacterType::NonSpace], + [:regexp_nonword_type, [:type, :nonword, '\W'], ::Regexp::Expression::CharacterType::NonWord], + [:regexp_dot_meta, [:meta, :dot, '.'], ::Regexp::Expression::CharacterType::Any] + ) + + private + + # Transform ast into expression + # + # @return [Regexp::Expression] + def transform + expression_class.new(expression_token) + end + end # ASTToExpression + + ASTToExpression::TABLE.types.each(&method(:register)) + end # Direct + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/options_group.rb b/lib/mutant/ast/regexp/transformer/options_group.rb new file mode 100644 index 00000000..5918b910 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/options_group.rb @@ -0,0 +1,66 @@ +module Mutant + module AST + module Regexp + class Transformer + # Transformer for option groups + class OptionsGroup < self + register :regexp_options_group + + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + class ExpressionToAST < Transformer::ExpressionToAST + + # Transform options group into node + # + # @return [Parser::AST::Node] + def call + quantify(ast(expression.options, *children)) + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + include NamedChildren + + children :options + + private + + # Covnert node into expression + # + # @return [Regexp::Expression::Group::Options] + def transform + options_group.tap do |expression| + expression.expressions = subexpressions + end + end + + # Recursive mapping of children + # + # @return [Array] + def subexpressions + remaining_children.map(&Regexp.public_method(:to_expression)) + end + + # Options group instance constructed from options text + # + # @return [Regexp::Expression::Group::Options] + def options_group + ::Regexp::Expression::Group::Options.new( + ::Regexp::Token.new(:group, :options, text) + ) + end + + # Flag text constructed from enabled options + # + # @return [String] + def text + flags = options.map { |key, value| key if value }.join + + "(?#{flags}-:" + end + end # ASTToExpression + end # OptionsGroup + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/quantifier.rb b/lib/mutant/ast/regexp/transformer/quantifier.rb new file mode 100644 index 00000000..90d3b2e6 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/quantifier.rb @@ -0,0 +1,112 @@ +module Mutant + module AST + module Regexp + class Transformer + # Transformer for regexp quantifiers + class Quantifier < self + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + class ExpressionToAST < Transformer::ExpressionToAST + # Transform quantifier into node + # + # @return [Parser::AST::Node] + def call + ast(expression.min, expression.max) + end + + private + + # Custom `type` for quantifiers which use `mode` instead of `type` + # + # @return [Symbol] + def type + :"regexp_#{expression.mode}_#{expression.token}" + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + include NamedChildren + + children :min, :max, :subject + + Quantifier = Class.new.include(Concord::Public.new(:type, :suffix, :mode)) + + QUANTIFIER_MAP = IceNine.deep_freeze({ + regexp_greedy_zero_or_more: [:zero_or_more, '*', :greedy], + regexp_greedy_one_or_more: [:one_or_more, '+', :greedy], + regexp_greedy_zero_or_one: [:zero_or_one, '?', :greedy], + regexp_possessive_zero_or_one: [:zero_or_one, '?+', :possessive], + regexp_reluctant_zero_or_more: [:zero_or_more, '*?', :reluctant], + regexp_reluctant_one_or_more: [:one_or_more, '+?', :reluctant], + regexp_possessive_zero_or_more: [:zero_or_more, '*+', :possessive], + regexp_possessive_one_or_more: [:one_or_more, '++', :possessive], + regexp_greedy_interval: [:interval, '', :greedy], + regexp_reluctant_interval: [:interval, '?', :reluctant], + regexp_possessive_interval: [:interval, '+', :possessive] + }.map { |ast_type, arguments| [ast_type, Quantifier.new(*arguments)] }.to_h) + + private + + # Transform ast into quantifier attached to expression + # + # @return [Regexp::Expression] + def transform + Regexp.to_expression(subject).dup.tap do |expression| + expression.quantify(type, text, min, max, mode) + end + end + + # Quantifier text + # + # @return [String] + def text + if type.equal?(:interval) + interval_text + suffix + else + suffix + end + end + + # Type of quantifier + # + # @return [:zero_or_more,:one_or_more,:interval] + def type + quantifier.type + end + + # Regexp symbols used to specify quantifier + # + # @return [String] + def suffix + quantifier.suffix + end + + # The quantifier "mode" + # + # @return [:greedy,:possessive,:reluctant] + def mode + quantifier.mode + end + + # Quantifier mapping information for current node + # + # @return [Quantifier] + def quantifier + QUANTIFIER_MAP.fetch(node.type) + end + + # Interval text constructed from min and max + # + # @return [String] + def interval_text + interval = [min, max].map { |num| num if num > 0 }.uniq + "{#{interval.join(',')}}" + end + end # ASTToExpression + + ASTToExpression::QUANTIFIER_MAP.keys.each(&method(:register)) + end # Quantifier + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/recursive.rb b/lib/mutant/ast/regexp/transformer/recursive.rb new file mode 100644 index 00000000..914750ad --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/recursive.rb @@ -0,0 +1,50 @@ +module Mutant + module AST + module Regexp + class Transformer + # Transformer for nodes with children + class Recursive < self + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + class ExpressionToAST < Transformer::ExpressionToAST + # Transform expression and children into nodes + # + # @return [Parser::AST::Node] + def call + quantify(ast(*children)) + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + include LookupTable + + # rubocop:disable LineLength + TABLE = Table.create( + [:regexp_alternation_meta, [:meta, :alternation, '|'], ::Regexp::Expression::Alternation], + [:regexp_nlookahead_assertion, [:assertion, :nlookahead, '(?!'], ::Regexp::Expression::Assertion::NegativeLookahead], + [:regexp_passive_group, [:group, :passive, '(?:'], ::Regexp::Expression::Group::Passive], + [:regexp_nlookbehind_assertion, [:assertion, :nlookbehind, '(?'], ::Regexp::Expression::Group::Atomic], + [:regexp_capture_group, [:group, :capture, '('], ::Regexp::Expression::Group::Capture] + ) + + private + + # Transform nodes and their children into expressions + # + # @return [Regexp::Expression] + def transform + expression_class.new(expression_token).tap do |expression| + expression.expressions = subexpressions + end + end + end # ASTToExpression + + ASTToExpression::TABLE.types.each(&method(:register)) + end # Recursive + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/root.rb b/lib/mutant/ast/regexp/transformer/root.rb new file mode 100644 index 00000000..b7c196b2 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/root.rb @@ -0,0 +1,29 @@ +module Mutant + module AST + module Regexp + class Transformer + # Transformer for root nodes + class Root < self + register :regexp_root_expression + + ExpressionToAST = Class.new(Recursive::ExpressionToAST) + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + + private + + # Transform node into root expression + # + # @return [Regexp::Expression::Root] + def transform + ::Regexp::Expression::Root.new.tap do |root| + root.expressions = subexpressions + end + end + end # ASTToExpression + end # Root + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/regexp/transformer/text.rb b/lib/mutant/ast/regexp/transformer/text.rb new file mode 100644 index 00000000..23fc4767 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/text.rb @@ -0,0 +1,55 @@ +module Mutant + module AST + module Regexp + class Transformer + # Regexp AST transformer for nodes that encode a text value + class Text < self + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + class ExpressionToAST < Transformer::ExpressionToAST + # Transform expression into node preserving text value + # + # @return [Parser::AST::Node] + def call + quantify(ast(expression.text)) + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + include LookupTable + + TABLE = Table.create( + [:regexp_literal_literal, %i[literal literal], ::Regexp::Expression::Literal], + [:regexp_comment_group, %i[group comment], ::Regexp::Expression::Group::Comment], + [:regexp_named_group, %i[group named], ::Regexp::Expression::Group::Named], + [:regexp_number_backref, %i[backref number], ::Regexp::Expression::Backreference::Number], + [:regexp_name_call_backref, %i[backref name_call], ::Regexp::Expression::Backreference::NameCall], + [:regexp_whitespace_free_space, %i[free_space whitespace], ::Regexp::Expression::WhiteSpace], + [:regexp_comment_free_space, %i[free_space comment], ::Regexp::Expression::WhiteSpace], + [:regexp_hex_escape, %i[escape hex], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_literal_escape, %i[escape literal], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_backslash_escape, %i[escape backslash], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_tab_escape, %i[escape tab], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_codepoint_list_escape, %i[escape codepoint_list], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_control_escape, %i[escape control], ::Regexp::Expression::EscapeSequence::Control], + [:regexp_meta_sequence_escape, %i[escape meta_sequence], ::Regexp::Expression::EscapeSequence::Control] + ) + + private + + # Transform node to expression with text value + # + # @return [Regexp::Expression] + def transform + token = expression_token.dup + token.text = Util.one(node.children) + expression_class.new(token) + end + end # ASTToExpression + + ASTToExpression::TABLE.types.each(&method(:register)) + end # Text + end # Transformer + end # Regexp + end # AST +end # Mutant diff --git a/lib/mutant/ast/types.rb b/lib/mutant/ast/types.rb index dd3f9682..ebc149cf 100644 --- a/lib/mutant/ast/types.rb +++ b/lib/mutant/ast/types.rb @@ -3,6 +3,7 @@ module Mutant # Groups of node types # # :reek:TooManyConstants + # rubocop:disable Metrics/ModuleLength module Types symbolset = ->(strings) { strings.map(&:to_sym).to_set.freeze } @@ -40,8 +41,96 @@ module Mutant # BLACKLIST = symbolset.(%w[not]) + # Nodes generated by regular expression body parsing + REGEXP = symbolset.(%w[ + regexp_alpha_property + regexp_alternation_escape + regexp_alternation_meta + regexp_atomic_group + regexp_backslash_escape + regexp_bell_escape + regexp_bol_anchor + regexp_bos_anchor + regexp_capture_group + regexp_carriage_escape + regexp_character_set + regexp_character_set + regexp_codepoint_list_escape + regexp_comment_free_space + regexp_comment_group + regexp_control_escape + regexp_digit_type + regexp_dot_escape + regexp_dot_meta + regexp_eol_anchor + regexp_eol_escape + regexp_eos_anchor + regexp_eos_ob_eol_anchor + regexp_escape_escape + regexp_form_feed_escape + regexp_greedy_interval + regexp_greedy_one_or_more + regexp_greedy_zero_or_more + regexp_greedy_zero_or_one + regexp_group_close_escape + regexp_group_open_escape + regexp_hex_escape + regexp_hex_type + regexp_interval_close_escape + regexp_interval_open_escape + regexp_letter_any_property + regexp_literal_escape + regexp_literal_literal + regexp_lookahead_assertion + regexp_lookbehind_assertion + regexp_mark_keep + regexp_match_start_anchor + regexp_meta_sequence_escape + regexp_name_call_backref + regexp_named_group + regexp_newline_escape + regexp_nlookahead_assertion + regexp_nlookbehind_assertion + regexp_nondigit_type + regexp_nonspace_type + regexp_nonword_boundary_anchor + regexp_nonword_type + regexp_nonhex_type + regexp_number_backref + regexp_one_or_more_escape + regexp_open_conditional + regexp_options_group + regexp_passive_group + regexp_possessive_interval + regexp_possessive_one_or_more + regexp_possessive_zero_or_more + regexp_possessive_zero_or_one + regexp_reluctant_interval + regexp_reluctant_one_or_more + regexp_reluctant_zero_or_more + regexp_root_expression + regexp_script_arabic_property + regexp_script_han_property + regexp_script_hangul_property + regexp_script_hiragana_property + regexp_script_katakana_property + regexp_sequence_expression + regexp_set_close_escape + regexp_set_open_escape + regexp_space_type + regexp_tab_escape + regexp_vertical_tab_escape + regexp_whitespace_free_space + regexp_word_boundary_anchor + regexp_word_type + regexp_zero_or_more_escape + regexp_zero_or_one_escape + ]) + # Nodes that are NOT generated by parser but used by mutant / unparser. - EXTRA = symbolset.(%w[empty]) + GENERATED = symbolset.(%w[empty]) + + EXTRA = symbolset.(GENERATED + REGEXP) # All node types mutant handles ALL = symbolset.((Parser::Meta::NODE_TYPES + EXTRA) - BLACKLIST) diff --git a/lib/mutant/mutator/node/literal/regex.rb b/lib/mutant/mutator/node/literal/regex.rb index bd0e51ae..8085f7ac 100644 --- a/lib/mutant/mutator/node/literal/regex.rb +++ b/lib/mutant/mutator/node/literal/regex.rb @@ -23,6 +23,7 @@ module Mutant # # @return [undefined] def dispatch + mutate_body emit_singletons unless parent_node children.each_with_index do |child, index| mutate_child(index) unless n_str?(child) @@ -31,6 +32,45 @@ module Mutant emit_type(s(:str, NULL_REGEXP_SOURCE), options) end + # Mutate regexp body + # + # @note will only mutate parts of regexp body if the + # body is composed of only strings. Regular expressions + # with interpolation are skipped + # + # @return [undefined] + def mutate_body + return unless body.all?(&method(:n_str?)) + return unless AST::Regexp.supported?(body_expression) + + Mutator.mutate(body_ast).each do |mutation| + source = AST::Regexp.to_expression(mutation).to_s + emit_type(s(:str, source), options) + end + end + + # AST representation of regexp body + # + # @return [Parser::AST::Node] + def body_ast + AST::Regexp.to_ast(body_expression) + end + + # Expression representation of regexp body + # + # @return [Regexp::Expression] + def body_expression + AST::Regexp.parse(body.map(&:children).join) + end + memoize :body_expression + + # Children of regexp node which compose regular expression source + # + # @return [Array] + def body + children.slice(0...-1) + end + end # Regex end # Literal end # Node diff --git a/lib/mutant/mutator/node/named_value/variable_assignment.rb b/lib/mutant/mutator/node/named_value/variable_assignment.rb index 8d6086ea..493cf68f 100644 --- a/lib/mutant/mutator/node/named_value/variable_assignment.rb +++ b/lib/mutant/mutator/node/named_value/variable_assignment.rb @@ -16,7 +16,7 @@ module Mutant } MAP = IceNine.deep_freeze( - Hash[map.map { |type, prefix| [type, [prefix, /^#{Regexp.escape(prefix)}/]] }] + Hash[map.map { |type, prefix| [type, [prefix, /^#{::Regexp.escape(prefix)}/]] }] ) handle(*MAP.keys) diff --git a/lib/mutant/mutator/node/regexp.rb b/lib/mutant/mutator/node/regexp.rb new file mode 100644 index 00000000..f2a9e324 --- /dev/null +++ b/lib/mutant/mutator/node/regexp.rb @@ -0,0 +1,44 @@ +module Mutant + class Mutator + class Node + module Regexp + # Generic regexp mutator + class Generic < Node + handle(*(AST::Types::REGEXP - %i[regexp_root_expression regexp_bol_anchor])) + + # Noop dispatch + # + # @return [undefined] + def dispatch + end + end # Generic + + # Mutator for root expression regexp wrapper + class RootExpression < Node + handle(:regexp_root_expression) + + # Emit mutations for children of root node + # + # @return [undefined] + def dispatch + children.each_index(&method(:mutate_child)) + end + end # RootExpression + + # Mutator for beginning of line anchor `^` + class BeginningOfLineAnchor < Node + handle(:regexp_bol_anchor) + + # Emit mutations + # + # Replace `^` with `\A` + # + # @return [undefined] + def dispatch + emit(s(:regexp_bos_anchor)) + end + end # BeginningOfLineAnchor + end # Regexp + end # Node + end # Mutator +end # Mutant diff --git a/lib/mutant/mutator/node/regopt.rb b/lib/mutant/mutator/node/regopt.rb index db5ce8e2..5b344d80 100644 --- a/lib/mutant/mutator/node/regopt.rb +++ b/lib/mutant/mutator/node/regopt.rb @@ -25,7 +25,7 @@ module Mutant (children - MUTATED_FLAGS) end - end # Generic + end # Regopt end # Node end # Mutator end # Mutant diff --git a/meta/regex.rb b/meta/regex.rb deleted file mode 100644 index e62bcf28..00000000 --- a/meta/regex.rb +++ /dev/null @@ -1,45 +0,0 @@ -Mutant::Meta::Example.add :regexp do - source '/foo/' - - singleton_mutations - - # match all inputs - mutation '//' - - # match no input - mutation '/nomatch\A/' -end - -Mutant::Meta::Example.add :regexp do - source '/#{foo.bar}n/' - - singleton_mutations - mutation '/#{foo}n/' - mutation '/#{self.bar}n/' - mutation '/#{nil}n/' - mutation '/#{self}n/' - - # match all inputs - mutation '//' - - # match no input - mutation '/nomatch\A/' -end - -Mutant::Meta::Example.add :regexp do - source 'true if /foo/' - - singleton_mutations - mutation 'false if /foo/' - mutation 'nil if /foo/' - mutation 'true if true' - mutation 'true if false' - mutation 'true if nil' - mutation 'true' - - # match all inputs - mutation 'true if //' - - # match no input - mutation 'true if /nomatch\A/' -end diff --git a/meta/regexp.rb b/meta/regexp.rb new file mode 100644 index 00000000..1954f0e7 --- /dev/null +++ b/meta/regexp.rb @@ -0,0 +1,106 @@ +Mutant::Meta::Example.add :regexp do + source '/foo/' + + singleton_mutations + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' +end + +Mutant::Meta::Example.add :regexp do + source '/#{foo.bar}n/' + + singleton_mutations + mutation '/#{foo}n/' + mutation '/#{self.bar}n/' + mutation '/#{nil}n/' + mutation '/#{self}n/' + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' +end + +Mutant::Meta::Example.add :regexp do + source '/#{foo}/' + + singleton_mutations + mutation '/#{self}/' + mutation '/#{nil}/' + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' +end + +Mutant::Meta::Example.add :regexp do + source '/#{foo}#{nil}/' + + singleton_mutations + mutation '/#{nil}#{nil}/' + mutation '/#{self}#{nil}/' + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' +end + +Mutant::Meta::Example.add :regexp do + source '//' + + singleton_mutations + + # match no input + mutation '/nomatch\A/' +end + +Mutant::Meta::Example.add :regexp do + source 'true if /foo/' + + singleton_mutations + mutation 'false if /foo/' + mutation 'nil if /foo/' + mutation 'true if true' + mutation 'true if false' + mutation 'true if nil' + mutation 'true' + + # match all inputs + mutation 'true if //' + + # match no input + mutation 'true if /nomatch\A/' +end + +Mutant::Meta::Example.add :regexp do + source '/(?(1)(foo)(bar))/' + + singleton_mutations + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' +end + +Pathname + .glob(Pathname.new(__dir__).join('regexp', '*.rb')) + .sort + .each(&Kernel.public_method(:require)) + +# Re-register examples for all regular expression nodes for node_type `:regexp` +Mutant::Meta::Example::ALL.each do |example| + next unless example.node_type.to_s.start_with?('regexp_') + + Mutant::Meta::Example::ALL << example.with(node_type: :regexp) +end diff --git a/meta/regexp/regexp_bol_anchor.rb b/meta/regexp/regexp_bol_anchor.rb new file mode 100644 index 00000000..c1a8fe2b --- /dev/null +++ b/meta/regexp/regexp_bol_anchor.rb @@ -0,0 +1,13 @@ +Mutant::Meta::Example.add :regexp_bol_anchor do + source '/^/' + + singleton_mutations + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' + + mutation '/\\A/' +end diff --git a/meta/regexp/regexp_bos_anchor.rb b/meta/regexp/regexp_bos_anchor.rb new file mode 100644 index 00000000..ce8d7424 --- /dev/null +++ b/meta/regexp/regexp_bos_anchor.rb @@ -0,0 +1,26 @@ +Mutant::Meta::Example.add :regexp_bos_anchor do + source '/\A/' + + singleton_mutations + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' +end + +Mutant::Meta::Example.add :regexp_bos_anchor do + source '/^#{a}/' + + singleton_mutations + + mutation '/^#{nil}/' + mutation '/^#{self}/' + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' +end diff --git a/meta/regexp/regexp_root_expression.rb b/meta/regexp/regexp_root_expression.rb new file mode 100644 index 00000000..c32ec02c --- /dev/null +++ b/meta/regexp/regexp_root_expression.rb @@ -0,0 +1,13 @@ +Mutant::Meta::Example.add :regexp_root_expression do + source '/^/' + + singleton_mutations + + # match all inputs + mutation '//' + + # match no input + mutation '/nomatch\A/' + + mutation '/\\A/' +end diff --git a/mutant.gemspec b/mutant.gemspec index 418421f8..7e312f9c 100644 --- a/mutant.gemspec +++ b/mutant.gemspec @@ -35,6 +35,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency('equalizer', '~> 0.0.9') gem.add_runtime_dependency('anima', '~> 0.3.0') gem.add_runtime_dependency('concord', '~> 0.1.5') + gem.add_runtime_dependency('regexp_parser', '~> 0.3.6') gem.add_development_dependency('devtools', '~> 0.1.4') gem.add_development_dependency('bundler', '~> 1.10') diff --git a/spec/integrations.yml b/spec/integrations.yml index 77ec7925..f671465a 100644 --- a/spec/integrations.yml +++ b/spec/integrations.yml @@ -88,6 +88,15 @@ - optional/capi/string_spec.rb '#': - language/regexp/escapes_spec.rb + "#": + - language/regexp/interpolation_spec.rb +- name: regexp_parser + namespace: Regexp + repo_uri: 'https://github.com/ammar/regexp_parser.git' + mutation_coverage: false + mutation_generation: true + expect_coverage: 0 # not run + expected_errors: {} - name: auom namespace: AUOM repo_uri: 'https://github.com/mbj/auom.git' diff --git a/spec/support/corpus.rb b/spec/support/corpus.rb index fa91c3a2..9a819956 100644 --- a/spec/support/corpus.rb +++ b/spec/support/corpus.rb @@ -250,7 +250,7 @@ module MutantSpec def initialize(*error_info) super(MESSAGE % error_info) end - end + end # UnnecessaryExpectation include Concord.new(:map), Adamantium diff --git a/spec/support/warnings.yml b/spec/support/warnings.yml index bbd90a90..3bd4ceb5 100644 --- a/spec/support/warnings.yml +++ b/spec/support/warnings.yml @@ -1,3 +1,4 @@ --- - 'lib/parser/lexer.rb:10791: warning: assigned but unused variable - testEof' - 'lib/parser/source/rewriter.rb:392: warning: assigned but unused variable - begin_pos' +- 'lib/regexp_parser/scanner.rb:1646: warning: assigned but unused variable - testEof' diff --git a/spec/unit/mutant/ast/regexp/parse_spec.rb b/spec/unit/mutant/ast/regexp/parse_spec.rb new file mode 100644 index 00000000..624c6e3e --- /dev/null +++ b/spec/unit/mutant/ast/regexp/parse_spec.rb @@ -0,0 +1,7 @@ +RSpec.describe Mutant::AST::Regexp, '.parse' do + before { stub_const('RUBY_VERSION', '2.3.9') } + + it 'parses using minor ruby version' do + expect(described_class.parse(/foo/).to_re).to eql(/foo/) + end +end diff --git a/spec/unit/mutant/ast/regexp/supported_predicate_spec.rb b/spec/unit/mutant/ast/regexp/supported_predicate_spec.rb new file mode 100644 index 00000000..78596d59 --- /dev/null +++ b/spec/unit/mutant/ast/regexp/supported_predicate_spec.rb @@ -0,0 +1,14 @@ +RSpec.describe Mutant::AST::Regexp, '.supported?' do + subject { described_class.supported?(expression) } + + let(:expression) { described_class.parse(regexp) } + let(:regexp) { /foo/ } + + it { should be(true) } + + context 'conditional regular expressions' do + let(:regexp) { /((?(1)(foo)(bar)))/ } + + it { should be(false) } + end +end diff --git a/spec/unit/mutant/ast/regexp/transformer/lookup_table/table_spec.rb b/spec/unit/mutant/ast/regexp/transformer/lookup_table/table_spec.rb new file mode 100644 index 00000000..8a5c8257 --- /dev/null +++ b/spec/unit/mutant/ast/regexp/transformer/lookup_table/table_spec.rb @@ -0,0 +1,19 @@ +RSpec.describe Mutant::AST::Regexp::Transformer::LookupTable::Table do + subject { table.lookup(:regexp_fake_thing) } + + let(:expression_class) { class_double(Regexp::Expression) } + + let(:table) do + described_class.create( + [:regexp_fake_thing, %i[thing fake], expression_class] + ) + end + + its(:token) { should eql(Regexp::Token.new(:thing, :fake)) } + + its(:regexp_class) { should be(expression_class) } + + it 'exposes list of types' do + expect(table.types).to eql([:regexp_fake_thing]) + end +end diff --git a/spec/unit/mutant/ast/regexp/transformer/lookup_table_spec.rb b/spec/unit/mutant/ast/regexp/transformer/lookup_table_spec.rb new file mode 100644 index 00000000..dcfad4bb --- /dev/null +++ b/spec/unit/mutant/ast/regexp/transformer/lookup_table_spec.rb @@ -0,0 +1,33 @@ +RSpec.describe Mutant::AST::Regexp::Transformer::LookupTable do + subject(:pair) { mapper.new(s(:regexp_fake)).pair } + + let(:table) { instance_double(described_class::Table) } + let(:token) { ::Regexp::Token.new } + let(:klass) { ::Regexp::Expression } + + let(:mapping) do + described_class::Mapping.new(token, klass) + end + + let(:mapper) do + fake_table = table + + Class.new do + include Concord.new(:node), Mutant::AST::Regexp::Transformer::LookupTable + + const_set(:TABLE, fake_table) + + def pair + [expression_token, expression_class] + end + end + end + + before do + allow(table).to receive(:lookup).with(:regexp_fake).and_return(mapping) + end + + it 'constructs regexp lookup table' do + expect(pair).to eql([token, klass]) + end +end diff --git a/spec/unit/mutant/ast/regexp/transformer_spec.rb b/spec/unit/mutant/ast/regexp/transformer_spec.rb new file mode 100644 index 00000000..e969c9cf --- /dev/null +++ b/spec/unit/mutant/ast/regexp/transformer_spec.rb @@ -0,0 +1,19 @@ +RSpec.describe Mutant::AST::Regexp::Transformer do + before do + stub_const("#{described_class}::REGISTRY", Mutant::Registry.new) + end + + it 'registers types to a given class' do + klass = Class.new(described_class) { register(:regexp_bos_anchor) } + + expect(described_class.lookup(:regexp_bos_anchor)).to be(klass) + end + + it 'rejects duplicate registrations' do + Class.new(described_class) { register(:regexp_bos_anchor) } + + expect { Class.new(described_class) { register(:regexp_bos_anchor) } } + .to raise_error(Mutant::Registry::RegistryError) + .with_message('Duplicate type registration: :regexp_bos_anchor') + end +end diff --git a/spec/unit/mutant/ast/regexp_spec.rb b/spec/unit/mutant/ast/regexp_spec.rb new file mode 100644 index 00000000..dbfa13bd --- /dev/null +++ b/spec/unit/mutant/ast/regexp_spec.rb @@ -0,0 +1,617 @@ +module RegexpSpec + class Expression < SimpleDelegator + NO_EXPRESSIONS = Object.new.freeze + + include Equalizer.new(:type, :token, :text, :quantifier, :expressions) + + def quantifier + return Quantifier::NONE unless quantified? + + Quantifier.new(super()) + end + + def expressions + return NO_EXPRESSIONS if terminal? + + super().map(&self.class.public_method(:new)) + end + + class Quantifier < SimpleDelegator + NONE = Object.new.freeze + + include Equalizer.new(:token, :text, :mode, :min, :max) + end # Quantifier + end # Expression + + RSpec.shared_context 'regexp transformation' do + let(:parsed) { Mutant::AST::Regexp.parse(regexp) } + let(:ast) { Mutant::AST::Regexp.to_ast(parsed) } + let(:expression) { Mutant::AST::Regexp.to_expression(ast) } + + def expect_frozen_expression(expression, root = expression) + expect(expression.frozen?).to( + be(true), + "Expected #{root} to be deep frozen" + ) + + return if expression.terminal? + + expression.expressions.each do |subexpression| + expect_frozen_expression(subexpression, root) + end + end + + it 'transforms into ast' do + expect(ast).to eql(expected) + end + + it 'deep freezes expression mapping' do + expect_frozen_expression(expression) + end + + it 'transforms ast back to expression' do + expect(Expression.new(expression)).to eql(Expression.new(parsed)) + end + end + + RSpec.shared_context 'regexp round trip' do + let(:round_trip) { expression.to_re } + + it 'round trips Regexp' do + expect(round_trip).to eql(regexp) + end + end + + def self.expect_mapping(regexp, type, &block) + RSpec.describe Mutant::AST::Regexp::Transformer.lookup(type) do + context "when mapping #{regexp.inspect}" do + let(:regexp) { regexp } + let(:expected, &block) + + include_context 'regexp transformation' + + return if regexp.encoding.name.eql?('ASCII-8BIT') + + include_context 'regexp round trip' + end + end + end +end # RegexpSpec + +RegexpSpec.expect_mapping(/A/, :regexp_root_expression) do + s(:regexp_root_expression, + s(:regexp_literal_literal, 'A')) +end + +RegexpSpec.expect_mapping(/\p{Alpha}/, :regexp_alpha_property) do + s(:regexp_root_expression, + s(:regexp_alpha_property)) +end + +RegexpSpec.expect_mapping(/foo|bar/, :regexp_alternation_meta) do + s(:regexp_root_expression, + s(:regexp_alternation_meta, + s(:regexp_sequence_expression, + s(:regexp_literal_literal, 'foo')), + s(:regexp_sequence_expression, + s(:regexp_literal_literal, 'bar')))) +end + +RegexpSpec.expect_mapping(/(?>a)/, :regexp_atomic_group) do + s(:regexp_root_expression, + s(:regexp_atomic_group, + s(:regexp_literal_literal, 'a'))) +end + +RegexpSpec.expect_mapping(/\\/, :regexp_backslash_escape) do + s(:regexp_root_expression, + s(:regexp_backslash_escape, '\\\\')) +end + +RegexpSpec.expect_mapping(/^/, :regexp_bol_anchor) do + s(:regexp_root_expression, + s(:regexp_bol_anchor)) +end + +RegexpSpec.expect_mapping(/\A/, :regexp_bos_anchor) do + s(:regexp_root_expression, + s(:regexp_bos_anchor)) +end + +RegexpSpec.expect_mapping(/(foo)/, :regexp_capture_group) do + s(:regexp_root_expression, + s(:regexp_capture_group, + s(:regexp_literal_literal, 'foo'))) +end + +RegexpSpec.expect_mapping(/()\1/, :regexp_number_backref) do + s(:regexp_root_expression, + s(:regexp_capture_group), + s(:regexp_number_backref, '\\1')) +end + +RegexpSpec.expect_mapping(/(a)*/, :regexp_capture_group) do + s(:regexp_root_expression, + s(:regexp_greedy_zero_or_more, 0, -1, + s(:regexp_capture_group, + s(:regexp_literal_literal, 'a')))) +end + +RegexpSpec.expect_mapping(/\r/, :regexp_carriage_escape) do + s(:regexp_root_expression, + s(:regexp_carriage_escape)) +end + +RegexpSpec.expect_mapping(/\a/, :regexp_bell_escape) do + s(:regexp_root_expression, + s(:regexp_bell_escape)) +end + +RegexpSpec.expect_mapping(/\?/, :regexp_zero_or_one_escape) do + s(:regexp_root_expression, + s(:regexp_zero_or_one_escape)) +end + +RegexpSpec.expect_mapping(/\|/, :regexp_alternation_escape) do + s(:regexp_root_expression, + s(:regexp_alternation_escape)) +end + +RegexpSpec.expect_mapping(/\c2/, :regexp_control_escape) do + s(:regexp_root_expression, + s(:regexp_control_escape, '\\c2')) +end + +RegexpSpec.expect_mapping(/\M-B/n, :regexp_meta_sequence_escape) do + s(:regexp_root_expression, + s(:regexp_meta_sequence_escape, '\M-B')) +end + +RegexpSpec.expect_mapping(/\K/, :regexp_mark_keep) do + s(:regexp_root_expression, + s(:regexp_mark_keep)) +end + +RegexpSpec.expect_mapping(/\e/, :regexp_escape_escape) do + s(:regexp_root_expression, + s(:regexp_escape_escape)) +end + +RegexpSpec.expect_mapping(/\f/, :regexp_form_feed_escape) do + s(:regexp_root_expression, + s(:regexp_form_feed_escape)) +end + +RegexpSpec.expect_mapping(/\v/, :regexp_vertical_tab_escape) do + s(:regexp_root_expression, + s(:regexp_vertical_tab_escape)) +end + +RegexpSpec.expect_mapping(/\e/, :regexp_escape_escape) do + s(:regexp_root_expression, + s(:regexp_escape_escape)) +end + +RegexpSpec.expect_mapping(/[ab]+/, :regexp_character_set) do + s(:regexp_root_expression, + s(:regexp_greedy_one_or_more, 1, -1, + s(:regexp_character_set, 'a', 'b'))) +end + +RegexpSpec.expect_mapping(/[ab]/, :regexp_character_set) do + s(:regexp_root_expression, + s(:regexp_character_set, 'a', 'b')) +end + +RegexpSpec.expect_mapping(/[a-j]/, :regexp_character_set) do + s(:regexp_root_expression, + s(:regexp_character_set, 'a-j')) +end + +RegexpSpec.expect_mapping(/\u{9879}/, :regexp_codepoint_list_escape) do + s(:regexp_root_expression, + s(:regexp_codepoint_list_escape, '\\u{9879}')) +end + +RegexpSpec.expect_mapping(/(?#foo)/, :regexp_comment_group) do + s(:regexp_root_expression, + s(:regexp_comment_group, '(?#foo)')) +end + +RegexpSpec.expect_mapping(/(?x-: # comment +)/, :regexp_comment_free_space) do + s(:regexp_root_expression, + s(:regexp_options_group, { + m: false, + i: false, + x: true, + d: false, + a: false, + u: false + }, + s(:regexp_whitespace_free_space, ' '), + s(:regexp_comment_free_space, "# comment\n"))) +end + +RegexpSpec.expect_mapping(/\d/, :regexp_digit_type) do + s(:regexp_root_expression, + s(:regexp_digit_type)) +end + +RegexpSpec.expect_mapping(/\./, :regexp_dot_escape) do + s(:regexp_root_expression, + s(:regexp_dot_escape)) +end + +RegexpSpec.expect_mapping(/.+/, :regexp_dot_meta) do + s(:regexp_root_expression, + s(:regexp_greedy_one_or_more, 1, -1, + s(:regexp_dot_meta))) +end + +RegexpSpec.expect_mapping(/$/, :regexp_eol_anchor) do + s(:regexp_root_expression, + s(:regexp_eol_anchor)) +end + +RegexpSpec.expect_mapping(/\$/, :regexp_eol_escape) do + s(:regexp_root_expression, + s(:regexp_eol_escape)) +end + +RegexpSpec.expect_mapping(/\z/, :regexp_eos_anchor) do + s(:regexp_root_expression, + s(:regexp_eos_anchor)) +end + +RegexpSpec.expect_mapping(/\Z/, :regexp_eos_ob_eol_anchor) do + s(:regexp_root_expression, + s(:regexp_eos_ob_eol_anchor)) +end + +RegexpSpec.expect_mapping(/a{1,}/, :regexp_greedy_interval) do + s(:regexp_root_expression, + s(:regexp_greedy_interval, 1, -1, + s(:regexp_literal_literal, 'a'))) +end + +RegexpSpec.expect_mapping(/.{2}/, :regexp_greedy_interval) do + s(:regexp_root_expression, + s(:regexp_greedy_interval, 2, 2, + s(:regexp_dot_meta))) +end + +RegexpSpec.expect_mapping(/.{3,5}/, :regexp_greedy_interval) do + s(:regexp_root_expression, + s(:regexp_greedy_interval, 3, 5, + s(:regexp_dot_meta))) +end + +RegexpSpec.expect_mapping(/.{,3}/, :regexp_greedy_interval) do + s(:regexp_root_expression, + s(:regexp_greedy_interval, 0, 3, + s(:regexp_dot_meta))) +end + +RegexpSpec.expect_mapping(/.+/, :regexp_greedy_one_or_more) do + s(:regexp_root_expression, + s(:regexp_greedy_one_or_more, 1, -1, + s(:regexp_dot_meta))) +end + +RegexpSpec.expect_mapping(/[ab]+/, :regexp_greedy_one_or_more) do + s(:regexp_root_expression, + s(:regexp_greedy_one_or_more, 1, -1, + s(:regexp_character_set, 'a', 'b'))) +end + +RegexpSpec.expect_mapping(/(a)*/, :regexp_greedy_zero_or_more) do + s(:regexp_root_expression, + s(:regexp_greedy_zero_or_more, 0, -1, + s(:regexp_capture_group, + s(:regexp_literal_literal, 'a')))) +end + +RegexpSpec.expect_mapping(/.*/, :regexp_greedy_zero_or_more) do + s(:regexp_root_expression, + s(:regexp_greedy_zero_or_more, 0, -1, + s(:regexp_dot_meta))) +end + +RegexpSpec.expect_mapping(/.?/, :regexp_greedy_zero_or_one) do + s(:regexp_root_expression, + s(:regexp_greedy_zero_or_one, 0, 1, + s(:regexp_dot_meta))) +end + +RegexpSpec.expect_mapping(/\)/, :regexp_group_close_escape) do + s(:regexp_root_expression, + s(:regexp_group_close_escape)) +end + +RegexpSpec.expect_mapping(/\(/, :regexp_group_open_escape) do + s(:regexp_root_expression, + s(:regexp_group_open_escape)) +end + +RegexpSpec.expect_mapping(/\xFF/n, :regexp_hex_escape) do + s(:regexp_root_expression, + s(:regexp_hex_escape, '\\xFF')) +end + +RegexpSpec.expect_mapping(/\h/, :regexp_hex_type) do + s(:regexp_root_expression, + s(:regexp_hex_type)) +end + +RegexpSpec.expect_mapping(/\H/, :regexp_hex_type) do + s(:regexp_root_expression, + s(:regexp_nonhex_type)) +end + +RegexpSpec.expect_mapping(/\}/, :regexp_interval_close_escape) do + s(:regexp_root_expression, + s(:regexp_interval_close_escape)) +end + +RegexpSpec.expect_mapping(/\{/, :regexp_interval_open_escape) do + s(:regexp_root_expression, + s(:regexp_interval_open_escape)) +end + +RegexpSpec.expect_mapping(/\p{L}/, :regexp_letter_any_property) do + s(:regexp_root_expression, + s(:regexp_letter_any_property)) +end + +RegexpSpec.expect_mapping(/\-/, :regexp_literal_escape) do + s(:regexp_root_expression, + s(:regexp_literal_escape, '\\-')) +end + +RegexpSpec.expect_mapping(/\ /, :regexp_literal_escape) do + s(:regexp_root_expression, + s(:regexp_literal_escape, '\\ ')) +end + +RegexpSpec.expect_mapping(/\#/, :regexp_literal_escape) do + s(:regexp_root_expression, + s(:regexp_literal_escape, '\\#')) +end + +RegexpSpec.expect_mapping(/\:/, :regexp_literal_escape) do + s(:regexp_root_expression, + s(:regexp_literal_escape, '\\:')) +end + +RegexpSpec.expect_mapping(/\)/, :regexp_named_group) do + s(:regexp_root_expression, + s(:regexp_named_group, '(?')) +end + +RegexpSpec.expect_mapping(/(?)\g/, :regexp_name_call_backref) do + s(:regexp_root_expression, + s(:regexp_named_group, '(?'), + s(:regexp_name_call_backref, '\\g')) +end + +RegexpSpec.expect_mapping(/\n/, :regexp_newline_escape) do + s(:regexp_root_expression, + s(:regexp_newline_escape)) +end + +RegexpSpec.expect_mapping(/(?!a)/, :regexp_nlookahead_assertion) do + s(:regexp_root_expression, + s(:regexp_nlookahead_assertion, + s(:regexp_literal_literal, 'a'))) +end + +RegexpSpec.expect_mapping(/(?