diff --git a/Changelog.md b/Changelog.md index 9e58c2b5a..0b208ae51 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,7 @@ +# Unreleased + +* Reintroduce regexp mutation support [#1166](https://github.com/mbj/mutant/pull/1166) + # v0.10.23 2020-12-30 * [#1179](https://github.com/mbj/mutant/pull/1181) diff --git a/Gemfile.lock b/Gemfile.lock index 69b0c9d75..4345f0177 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,7 @@ PATH mprelude (~> 0.1.0) parser (~> 3.0.0) procto (~> 0.0.2) + regexp_parser (~> 2.0, >= 2.0.3) unparser (~> 0.5.6) variable (~> 0.0.1) @@ -51,24 +52,24 @@ GEM ast (~> 2.4.1) procto (0.0.3) rainbow (3.0.0) - regexp_parser (2.0.2) + regexp_parser (2.0.3) rexml (3.2.4) rspec (3.10.0) rspec-core (~> 3.10.0) rspec-expectations (~> 3.10.0) rspec-mocks (~> 3.10.0) - rspec-core (3.10.0) + rspec-core (3.10.1) rspec-support (~> 3.10.0) - rspec-expectations (3.10.0) + rspec-expectations (3.10.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.10.0) + rspec-mocks (3.10.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) - rspec-support (3.10.0) + rspec-support (3.10.1) rubocop (1.7.0) parallel (~> 1.10) parser (>= 2.7.1.5) diff --git a/lib/mutant.rb b/lib/mutant.rb index c70ea9c1f..0b9a1ed7a 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -17,6 +17,7 @@ require 'parser' require 'parser/current' require 'pathname' +require 'regexp_parser' require 'set' require 'singleton' require 'stringio' @@ -53,6 +54,15 @@ module Mutant require 'mutant/ast/named_children' require 'mutant/ast/node_predicates' require 'mutant/ast/find_metaclass_containing' +require 'mutant/ast/regexp' +require 'mutant/ast/regexp/transformer' +require 'mutant/ast/regexp/transformer/direct' +require 'mutant/ast/regexp/transformer/named_group' +require 'mutant/ast/regexp/transformer/options_group' +require 'mutant/ast/regexp/transformer/quantifier' +require 'mutant/ast/regexp/transformer/recursive' +require 'mutant/ast/regexp/transformer/root' +require 'mutant/ast/regexp/transformer/text' require 'mutant/ast/meta' require 'mutant/ast/meta/send' require 'mutant/ast/meta/const' @@ -75,6 +85,13 @@ module Mutant require 'mutant/mutator/util/symbol' require 'mutant/mutator/node' require 'mutant/mutator/node/generic' +require 'mutant/mutator/node/regexp' +require 'mutant/mutator/node/regexp/alternation_meta' +require 'mutant/mutator/node/regexp/capture_group' +require 'mutant/mutator/node/regexp/character_type' +require 'mutant/mutator/node/regexp/end_of_line_anchor' +require 'mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor' +require 'mutant/mutator/node/regexp/greedy_zero_or_more' 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 000000000..c2180ea28 --- /dev/null +++ b/lib/mutant/ast/regexp.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Mutant + module AST + # Regexp source mapper + module Regexp + # Parse regex string into expression + # + # @param regexp [String] + # + # @return [Regexp::Expression, nil] + def self.parse(regexp) + ::Regexp::Parser.parse(regexp) + 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 000000000..f88347eda --- /dev/null +++ b/lib/mutant/ast/regexp/transformer.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +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 + + 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 + + def ast(*children) + s(type, *children) + end + + def quantify(node) + return node unless expression.quantified? + + Quantifier.to_ast(expression.quantifier).append(node) + end + + def children + expression.map(&Regexp.public_method(:to_ast)) + end + + 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 + + abstract_method :transform + + 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 + + def expression_token + self.class::TABLE.lookup(node.type).token + end + + 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/direct.rb b/lib/mutant/ast/regexp/transformer/direct.rb new file mode 100644 index 000000000..4c956d015 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/direct.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +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 Layout/LineLength + TABLE = Table.create( + [:regexp_alnum_posixclass, [:posixclass, :alnum, '[:alnum:]'], ::Regexp::Expression::PosixClass], + [:regexp_alpha_posixclass, [:posixclass, :alpha, '[:alpha:]'], ::Regexp::Expression::PosixClass], + [:regexp_alpha_property, [:property, :alpha, '\p{Alpha}'], ::Regexp::Expression::UnicodeProperty::Alpha], + [:regexp_alternation_escape, [:escape, :alternation, '\|'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_arabic_property, [:property, :arabic, '\p{Arabic}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_ascii_posixclass, [:posixclass, :ascii, '[:ascii:]'], ::Regexp::Expression::PosixClass], + [:regexp_backspace_escape, [:escape, :backspace, '\b'], ::Regexp::Expression::EscapeSequence::Backspace], + [:regexp_bell_escape, [:escape, :bell, '\a'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_blank_posixclass, [:posixclass, :blank, '[:blank:]'], ::Regexp::Expression::PosixClass], + [:regexp_bol_anchor, [:anchor, :bol, '^'], ::Regexp::Expression::Anchor::BeginningOfLine], + [:regexp_bol_escape, [:escape, :bol, '\^'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_bos_anchor, [:anchor, :bos, '\\A'], ::Regexp::Expression::Anchor::BeginningOfString], + [:regexp_carriage_escape, [:escape, :carriage, '\r'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_cntrl_posixclass, [:posixclass, :cntrl, '[:cntrl:]'], ::Regexp::Expression::PosixClass], + [:regexp_digit_posixclass, [:posixclass, :digit, '[:digit:]'], ::Regexp::Expression::PosixClass], + [:regexp_digit_type, [:type, :digit, '\d'], ::Regexp::Expression::CharacterType::Digit], + [:regexp_dot_escape, [:escape, :dot, '\.'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_dot_meta, [:meta, :dot, '.'], ::Regexp::Expression::CharacterType::Any], + [:regexp_eol_anchor, [:anchor, :eol, '$'], ::Regexp::Expression::Anchor::EndOfLine], + [:regexp_eol_escape, [:escape, :eol, '\$'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_eos_anchor, [:anchor, :eos, '\\z'], ::Regexp::Expression::Anchor::EndOfString], + [:regexp_eos_ob_eol_anchor, [:anchor, :eos_ob_eol, '\\Z'], ::Regexp::Expression::Anchor::EndOfStringOrBeforeEndOfLine], + [:regexp_escape_escape, [:escape, :escape, '\e'], ::Regexp::Expression::EscapeSequence::AsciiEscape], + [:regexp_form_feed_escape, [:escape, :form_feed, '\f'], ::Regexp::Expression::EscapeSequence::FormFeed], + [:regexp_graph_posixclass, [:posixclass, :graph, '[:graph:]'], ::Regexp::Expression::PosixClass], + [:regexp_group_close_escape, [:escape, :group_close, '\)'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_group_open_escape, [:escape, :group_open, '\('], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_han_property, [:property, :han, '\p{Han}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_hangul_property, [:property, :hangul, '\p{Hangul}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_hex_type, [:type, :hex, '\h'], ::Regexp::Expression::CharacterType::Hex], + [:regexp_hiragana_property, [:property, :hiragana, '\p{Hiragana}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_interval_close_escape, [:escape, :interval_close, '\}'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_interval_open_escape, [:escape, :interval_open, '\{'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_katakana_property, [:property, :katakana, '\p{Katakana}'], ::Regexp::Expression::UnicodeProperty::Script], + [:regexp_letter_property, [:property, :letter, '\p{L}'], ::Regexp::Expression::UnicodeProperty::Letter::Any], + [:regexp_linebreak_type, [:type, :linebreak, '\R'], ::Regexp::Expression::CharacterType::Linebreak], + [:regexp_lower_posixclass, [:posixclass, :lower, '[:lower:]'], ::Regexp::Expression::PosixClass], + [:regexp_mark_keep, [:keep, :mark, '\K'], ::Regexp::Expression::Keep::Mark], + [:regexp_match_start_anchor, [:anchor, :match_start, '\\G'], ::Regexp::Expression::Anchor::MatchStart], + [:regexp_newline_escape, [:escape, :newline, '\n'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_nondigit_type, [:type, :nondigit, '\D'], ::Regexp::Expression::CharacterType::NonDigit], + [:regexp_nonhex_type, [:type, :nonhex, '\H'], ::Regexp::Expression::CharacterType::NonHex], + [:regexp_nonspace_type, [:type, :nonspace, '\S'], ::Regexp::Expression::CharacterType::NonSpace], + [:regexp_nonword_boundary_anchor, [:anchor, :nonword_boundary, '\\B'], ::Regexp::Expression::Anchor::NonWordBoundary], + [:regexp_nonword_type, [:type, :nonword, '\W'], ::Regexp::Expression::CharacterType::NonWord], + [:regexp_one_or_more_escape, [:escape, :one_or_more, '\+'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_print_nonposixclass, [:nonposixclass, :print, '[:^print:]'], ::Regexp::Expression::PosixClass], + [:regexp_print_nonproperty, [:nonproperty, :print, '\P{Print}'], ::Regexp::Expression::UnicodeProperty::Print], + [:regexp_print_posixclass, [:posixclass, :print, '[:print:]'], ::Regexp::Expression::PosixClass], + [:regexp_print_posixclass, [:posixclass, :print, '[:print:]'], ::Regexp::Expression::PosixClass], + [:regexp_print_property, [:property, :print, '\p{Print}'], ::Regexp::Expression::UnicodeProperty::Print], + [:regexp_punct_posixclass, [:posixclass, :punct, '[:punct:]'], ::Regexp::Expression::PosixClass], + [:regexp_set_close_escape, [:escape, :set_close, '\]'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_set_open_escape, [:escape, :set_open, '\['], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_space_posixclass, [:posixclass, :space, '[:space:]'], ::Regexp::Expression::PosixClass], + [:regexp_space_type, [:type, :space, '\s'], ::Regexp::Expression::CharacterType::Space], + [:regexp_upper_posixclass, [:posixclass, :upper, '[:upper:]'], ::Regexp::Expression::PosixClass], + [:regexp_vertical_tab_escape, [:escape, :vertical_tab, '\v'], ::Regexp::Expression::EscapeSequence::VerticalTab], + [:regexp_word_boundary_anchor, [:anchor, :word_boundary, '\b'], ::Regexp::Expression::Anchor::WordBoundary], + [:regexp_word_posixclass, [:posixclass, :word, '[:word:]'], ::Regexp::Expression::PosixClass], + [:regexp_word_type, [:type, :word, '\w'], ::Regexp::Expression::CharacterType::Word], + [:regexp_xdigit_posixclass, [:posixclass, :xdigit, '[:xdigit:]'], ::Regexp::Expression::PosixClass], + [:regexp_xgrapheme_type, [:type, :xgrapheme, '\X'], ::Regexp::Expression::CharacterType::ExtendedGrapheme], + [:regexp_zero_or_more_escape, [:escape, :zero_or_more, '\*'], ::Regexp::Expression::EscapeSequence::Literal], + [:regexp_zero_or_one_escape, [:escape, :zero_or_one, '\?'], ::Regexp::Expression::EscapeSequence::Literal] + ) + # rubocop:enable Layout/LineLength + + private + + 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/named_group.rb b/lib/mutant/ast/regexp/transformer/named_group.rb new file mode 100644 index 000000000..40b23b281 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/named_group.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutant + module AST + module Regexp + class Transformer + # Transformer for named groups + class NamedGroup < self + register :regexp_named_group + + # Mapper from `Regexp::Expression` to `Parser::AST::Node` + class ExpressionToAST < Transformer::ExpressionToAST + + # Transform named group into node + # + # @return [Parser::AST::Node] + def call + quantify(ast(expression.name, *children)) + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + include NamedChildren + + children :name + + private + + def transform + named_group.tap do |expression| + expression.expressions = subexpressions + end + end + + def subexpressions + remaining_children.map(&Regexp.public_method(:to_expression)) + end + + def named_group + ::Regexp::Expression::Group::Named.new( + ::Regexp::Token.new(:group, :named, "(?<#{name}>") + ) + end + end # ASTToExpression + end # NamedGroup + 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 000000000..b011650df --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/options_group.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Mutant + module AST + module Regexp + class Transformer + # Transformer for option groups + class OptionsGroup < self + register :regexp_options_group + register :regexp_options_switch_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.option_changes, *children)) + end + end # ExpressionToAST + + # Mapper from `Parser::AST::Node` to `Regexp::Expression` + class ASTToExpression < Transformer::ASTToExpression + include NamedChildren + + children :option_changes + + private + + def transform + options_group.tap do |expression| + expression.expressions = subexpressions + end + end + + def subexpressions + remaining_children.map(&Regexp.public_method(:to_expression)) + end + + def options_group + ::Regexp::Expression::Group::Options.new( + ::Regexp::Token.new(:group, type, text) + ) + end + + def type + { + regexp_options_group: :options, + regexp_options_switch_group: :options_switch + }.fetch(node.type) + end + + def text + pos, neg = option_changes.partition { |_opt, val| val }.map do |arr| + arr.map(&:first).join + end + neg_opt_sep = '-' unless neg.empty? + content_sep = ':' unless type.equal?(:options_switch) + + "(?#{pos}#{neg_opt_sep}#{neg}#{content_sep}" + 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 000000000..787269ccf --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/quantifier.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +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 + + 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] + }.transform_values { |arguments| Quantifier.new(*arguments) }.to_h) + + private + + def transform + Regexp.to_expression(subject).dup.tap do |expression| + expression.quantify(type, text, min, max, mode) + end + end + + def text + if type.equal?(:interval) + interval_text + suffix + else + suffix + end + end + + def type + quantifier.type + end + + def suffix + quantifier.suffix + end + + def mode + quantifier.mode + end + + def quantifier + QUANTIFIER_MAP.fetch(node.type) + end + + def interval_text + interval = [min, max].map { |num| num if num.positive? }.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 000000000..dfbd285e3 --- /dev/null +++ b/lib/mutant/ast/regexp/transformer/recursive.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +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 + + # Expression::Sequence represents conditional branches, alternation branches, and intersection branches + # rubocop:disable Layout/LineLength + TABLE = Table.create( + [:regexp_alternation_meta, [:meta, :alternation, '|'], ::Regexp::Expression::Alternation], + [:regexp_atomic_group, [:group, :atomic, '(?>'], ::Regexp::Expression::Group::Atomic], + [:regexp_capture_group, [:group, :capture, '('], ::Regexp::Expression::Group::Capture], + [:regexp_character_set, [:set, :character, '['], ::Regexp::Expression::CharacterSet], + [:regexp_intersection_set, [:set, :intersection, '&&'], ::Regexp::Expression::CharacterSet::Intersection], + [:regexp_lookahead_assertion, [:assertion, :lookahead, '(?='], ::Regexp::Expression::Assertion::Lookahead], + [:regexp_lookbehind_assertion, [:assertion, :lookbehind, '(?<='], ::Regexp::Expression::Assertion::Lookbehind], + [:regexp_nlookahead_assertion, [:assertion, :nlookahead, '(?!'], ::Regexp::Expression::Assertion::NegativeLookahead], + [:regexp_nlookbehind_assertion, [:assertion, :nlookbehind, '(?.)/' singleton_mutations mutation '//' mutation '/nomatch\A/' end + +Pathname + .glob(Pathname.new(__dir__).join('regexp', '*.rb')) + .sort + .each(&Kernel.public_method(:require)) diff --git a/meta/regexp/character_types.rb b/meta/regexp/character_types.rb new file mode 100644 index 000000000..240e9d313 --- /dev/null +++ b/meta/regexp/character_types.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +mutations = { + [:regexp_digit_type, '/\d/'] => [:regexp_nondigit_type, '/\D/'], + [:regexp_hex_type, '/\h/'] => [:regexp_nonhex_type, '/\H/'], + [:regexp_space_type, '/\s/'] => [:regexp_nonspace_type, '/\S/'], + [:regexp_word_boundary_anchor, '/\b/'] => [:regexp_nonword_boundary_anchor, '/\B/'], + [:regexp_word_type, '/\w/'] => [:regexp_nonword_type, '/\W/'], + [:regexp_xgrapheme_type, '/\X/'] => [:regexp_linebreak_type, '/\R/'] +} + +mutations = mutations.merge(mutations.invert) + +mutations.each do |(source_type, source_mutation), (_, regexp_mutation)| + Mutant::Meta::Example.add :regexp, source_type do + source(source_mutation) + + singleton_mutations + regexp_mutations + + mutation(regexp_mutation) + end +end diff --git a/meta/regexp/regexp_alternation_meta.rb b/meta/regexp/regexp_alternation_meta.rb new file mode 100644 index 000000000..d2623e754 --- /dev/null +++ b/meta/regexp/regexp_alternation_meta.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_alternation_meta do + source '/\A(foo|bar|baz)\z/' + + singleton_mutations + regexp_mutations + + mutation '/\A(foo|bar)\z/' + mutation '/\A(foo|baz)\z/' + mutation '/\A(bar|baz)\z/' + mutation '/\A(?:foo|bar|baz)\z/' +end diff --git a/meta/regexp/regexp_bol_anchor.rb b/meta/regexp/regexp_bol_anchor.rb new file mode 100644 index 000000000..45082a18b --- /dev/null +++ b/meta/regexp/regexp_bol_anchor.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_bol_anchor do + source '/^/' + + singleton_mutations + regexp_mutations + + mutation '/\\A/' +end diff --git a/meta/regexp/regexp_bos_anchor.rb b/meta/regexp/regexp_bos_anchor.rb new file mode 100644 index 000000000..7f46aed01 --- /dev/null +++ b/meta/regexp/regexp_bos_anchor.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_bos_anchor do + source '/\A/' + + singleton_mutations + regexp_mutations +end + +Mutant::Meta::Example.add :regexp_bos_anchor do + source '/^#{a}/' + + singleton_mutations + regexp_mutations + + mutation '/^#{nil}/' + mutation '/^#{self}/' +end diff --git a/meta/regexp/regexp_capture_group.rb b/meta/regexp/regexp_capture_group.rb new file mode 100644 index 000000000..fe986dbba --- /dev/null +++ b/meta/regexp/regexp_capture_group.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_capture_group do + source '/()/' + + singleton_mutations + regexp_mutations +end + +Mutant::Meta::Example.add :regexp_capture_group do + source '/(foo|bar)/' + + singleton_mutations + regexp_mutations + + mutation '/(?:foo|bar)/' + mutation '/(foo)/' + mutation '/(bar)/' +end diff --git a/meta/regexp/regexp_eol_anchor.rb b/meta/regexp/regexp_eol_anchor.rb new file mode 100644 index 000000000..dcff6485f --- /dev/null +++ b/meta/regexp/regexp_eol_anchor.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_eol_anchor do + source '/$/' + + singleton_mutations + regexp_mutations + + mutation '/\z/' +end diff --git a/meta/regexp/regexp_eos_anchor.rb b/meta/regexp/regexp_eos_anchor.rb new file mode 100644 index 000000000..ab9362108 --- /dev/null +++ b/meta/regexp/regexp_eos_anchor.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_eos_anchor do + source '/\z/' + + singleton_mutations + regexp_mutations +end diff --git a/meta/regexp/regexp_eos_ob_eol_anchor.rb b/meta/regexp/regexp_eos_ob_eol_anchor.rb new file mode 100644 index 000000000..08e62df68 --- /dev/null +++ b/meta/regexp/regexp_eos_ob_eol_anchor.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_eos_ob_eol_anchor do + source '/\Z/' + + singleton_mutations + regexp_mutations + + mutation '/\z/' +end diff --git a/meta/regexp/regexp_greedy_zero_or_more.rb b/meta/regexp/regexp_greedy_zero_or_more.rb new file mode 100644 index 000000000..f248c17dd --- /dev/null +++ b/meta/regexp/regexp_greedy_zero_or_more.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_greedy_zero_or_more do + source '/\d*/' + + singleton_mutations + regexp_mutations + + mutation '/\d/' + mutation '/\d+/' + mutation '/\D*/' +end diff --git a/meta/regexp/regexp_root_expression.rb b/meta/regexp/regexp_root_expression.rb new file mode 100644 index 000000000..95e8bfcf2 --- /dev/null +++ b/meta/regexp/regexp_root_expression.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Mutant::Meta::Example.add :regexp_root_expression do + source '/^/' + + singleton_mutations + regexp_mutations + + mutation '/\\A/' +end diff --git a/mutant.gemspec b/mutant.gemspec index 4e459b20b..0b6be6817 100644 --- a/mutant.gemspec +++ b/mutant.gemspec @@ -34,6 +34,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency('mprelude', '~> 0.1.0') gem.add_runtime_dependency('parser', '~> 3.0.0') gem.add_runtime_dependency('procto', '~> 0.0.2') + gem.add_runtime_dependency('regexp_parser', '~> 2.0', '>= 2.0.3') gem.add_runtime_dependency('unparser', '~> 0.5.6') gem.add_runtime_dependency('variable', '~> 0.0.1') diff --git a/mutant.sh b/mutant.sh index 29bb9a275..5dd0b8f1e 100755 --- a/mutant.sh +++ b/mutant.sh @@ -2,6 +2,7 @@ bundle exec mutant run \ --zombie \ + --ignore-subject Mutant::Mutator::Node::Literal::Regex#body \ --ignore-subject Mutant::CLI#add_debug_options \ --ignore-subject Mutant::Expression::Namespace::Recursive#initialize \ --ignore-subject Mutant::Isolation::Fork::Parent#call \ 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 000000000..60c4e0190 --- /dev/null +++ b/spec/unit/mutant/ast/regexp/parse_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Mutant::AST::Regexp, '.parse' do + def apply(input) + described_class.parse(input) + end + + context 'on regexp regexp_parser does accept' do + it 'parses using minor ruby version' do + expect(apply(/foo/).to_re).to eql(/foo/) + end + 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 000000000..7a4981c94 --- /dev/null +++ b/spec/unit/mutant/ast/regexp/transformer/lookup_table/table_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +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 000000000..848e323f5 --- /dev/null +++ b/spec/unit/mutant/ast/regexp/transformer/lookup_table_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +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 000000000..aab88131e --- /dev/null +++ b/spec/unit/mutant/ast/regexp/transformer_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +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 000000000..4a2a0155b --- /dev/null +++ b/spec/unit/mutant/ast/regexp_spec.rb @@ -0,0 +1,712 @@ +# frozen_string_literal: true + +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' + + unless regexp.encoding.name.eql?('ASCII-8BIT') + include_context 'regexp round trip' + end + 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(/\^/, :regexp_bol_escape) do + s(:regexp_root_expression, + s(:regexp_bol_escape)) +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, + s(:regexp_literal_literal, 'a'), + s(:regexp_literal_literal, 'b')))) +end + +RegexpSpec.expect_mapping(/[ab]/, :regexp_character_set) do + s(:regexp_root_expression, + s(:regexp_character_set, + s(:regexp_literal_literal, 'a'), + s(:regexp_literal_literal, 'b'))) +end + +RegexpSpec.expect_mapping(/[a-j]/, :regexp_character_set) do + s(:regexp_root_expression, + s(:regexp_character_set, + s(:regexp_range_set, + s(:regexp_literal_literal, 'a'), + s(:regexp_literal_literal, 'j')))) +end + +RegexpSpec.expect_mapping(/[\b]/, :regexp_backspace_escape) do + s(:regexp_root_expression, + s(:regexp_character_set, + s(:regexp_backspace_escape))) +end + +RegexpSpec.expect_mapping(/()(?(1)a|b)/, :regexp_open_conditional) do + s(:regexp_root_expression, + s(:regexp_capture_group), + s(:regexp_open_conditional, + s(:regexp_condition_conditional, '(1)'), + s(:regexp_sequence_expression, + s(:regexp_literal_literal, 'a')), + s(:regexp_sequence_expression, + s(:regexp_literal_literal, 'b')))) +end + +RegexpSpec.expect_mapping(/[ab&&bc]/, :regexp_intersection_set) do + s(:regexp_root_expression, + s(:regexp_character_set, + s(:regexp_intersection_set, + s(:regexp_sequence_expression, + s(:regexp_literal_literal, 'a'), + s(:regexp_literal_literal, 'b')), + s(:regexp_sequence_expression, + s(:regexp_literal_literal, 'b'), + s(:regexp_literal_literal, 'c'))))) +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, { + x: true + }, + 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, + s(:regexp_literal_literal, 'a'), + s(:regexp_literal_literal, '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(/\101/, :regexp_octal_escape) do + s(:regexp_root_expression, + s(:regexp_octal_escape, '\\101')) +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_nonhex_type) do + s(:regexp_root_expression, + s(:regexp_nonhex_type)) +end + +RegexpSpec.expect_mapping(/\R/, :regexp_linebreak_type) do + s(:regexp_root_expression, + s(:regexp_linebreak_type)) +end + +RegexpSpec.expect_mapping(/\X/, :regexp_xgrapheme_type) do + s(:regexp_root_expression, + s(:regexp_xgrapheme_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_property) do + s(:regexp_root_expression, + s(:regexp_letter_property)) +end + +# We deliberately want to assert regexp_literal_escape nodes so this cop does +# not make sense to have enabled here. +# +# rubocop:disable Style/RedundantRegexpEscape +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(/\a)+/, :regexp_named_group) do + s(:regexp_root_expression, + s(:regexp_greedy_one_or_more, 1, -1, + s(:regexp_named_group, 'foo', + s(:regexp_literal_literal, 'a')))) +end + +RegexpSpec.expect_mapping(/(?)\g/, :regexp_name_call_backref) do + s(:regexp_root_expression, + s(:regexp_named_group, 'a'), + 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(/(? 0.1.0) parser (~> 3.0.0) procto (~> 0.0.2) + regexp_parser (~> 2.0, >= 2.0.3) unparser (~> 0.5.6) variable (~> 0.0.1) mutant-minitest (0.10.23) @@ -53,6 +54,7 @@ GEM parser (3.0.0.0) ast (~> 2.4.1) procto (0.0.3) + regexp_parser (2.0.3) thread_safe (0.3.6) unparser (0.5.6) abstract_type (~> 0.0.7) diff --git a/test_app/Gemfile.rspec3.8.lock b/test_app/Gemfile.rspec3.8.lock index ae923cd98..25487c776 100644 --- a/test_app/Gemfile.rspec3.8.lock +++ b/test_app/Gemfile.rspec3.8.lock @@ -14,6 +14,7 @@ PATH mprelude (~> 0.1.0) parser (~> 3.0.0) procto (~> 0.0.2) + regexp_parser (~> 2.0, >= 2.0.3) unparser (~> 0.5.6) variable (~> 0.0.1) mutant-rspec (0.10.23) @@ -52,6 +53,7 @@ GEM parser (3.0.0.0) ast (~> 2.4.1) procto (0.0.3) + regexp_parser (2.0.3) rspec (3.8.0) rspec-core (~> 3.8.0) rspec-expectations (~> 3.8.0)