From 89b92f3e94de21bd2debbf73324fbfa2afe78156 Mon Sep 17 00:00:00 2001 From: Daniel Gollahon Date: Tue, 15 Dec 2020 00:54:08 -0800 Subject: [PATCH] Reintroduce Regexp mutations This reverts commit 21d3fefdc9ec3ce5c979e3a4fb6932365b98cd87. This was not a clean revert. Note that: - The version of `regexp_parser` was 1.3.0, now it is 1.8.2 to accomodate our current `rubocop` version and because there were some relevant bugfixes implemented between 1.3.x and 1.8.x. We should eventually move to 2.0 but it is currently incompatible with this integration. There are some issues with the frozen Regexp classes getting mutated so we may have to open an issue. - Since "expected exception" support was removed from the specs, I have had to exclude two files entirely. This seems unfortunate as it reduces our overall coverage. - Since unsupported nodes are no longer explicitly tracked, I removed the code that used to handle that for regular expressions. See: https://github.com/mbj/mutant/pull/1021 - I had to change the example case for where we are more permissive than `regexp_parser` because `regexp_parser` has decided to become more permissive and try to match Ruby's semantics. It was actually very hard to find a case that failed--I brute-forced 50 million regexp strings that had perfect parity of being accepted and then stumbled onto the single hex escape case by accident. See: https://github.com/ammar/regexp_parser/issues/75 - Changed an access pattern for regexp mutations which became equivalent based on this: https://github.com/ammar/regexp_parser/blame/4ca7cec03b210e3e00473b7b1a7308f963190c1e/lib/regexp_parser/expression/subexpression.rb#L30-L33 - I have marked several dispatch methods as `private`. - I have also removed the old YARD doc comments on private methods at @mbj's request. - Some other minor conflicts and small spec assertion changes were resolved as well. --- Changelog.md | 4 + Gemfile.lock | 3 +- lib/mutant.rb | 17 + lib/mutant/ast/regexp.rb | 43 ++ lib/mutant/ast/regexp/transformer.rb | 150 ++++ lib/mutant/ast/regexp/transformer/direct.rb | 121 +++ .../ast/regexp/transformer/named_group.rb | 50 ++ .../ast/regexp/transformer/options_group.rb | 68 ++ .../ast/regexp/transformer/quantifier.rb | 90 +++ .../ast/regexp/transformer/recursive.rb | 56 ++ lib/mutant/ast/regexp/transformer/root.rb | 28 + lib/mutant/ast/regexp/transformer/text.rb | 58 ++ lib/mutant/ast/types.rb | 117 ++- lib/mutant/meta/example/dsl.rb | 10 +- lib/mutant/mutator/node/literal/regex.rb | 26 + lib/mutant/mutator/node/regexp.rb | 31 + .../mutator/node/regexp/alternation_meta.rb | 20 + .../mutator/node/regexp/capture_group.rb | 25 + .../mutator/node/regexp/character_type.rb | 31 + .../mutator/node/regexp/end_of_line_anchor.rb | 20 + ..._of_string_or_before_end_of_line_anchor.rb | 20 + .../node/regexp/greedy_zero_or_more.rb | 24 + meta/regexp.rb | 32 +- meta/regexp/character_types.rb | 23 + meta/regexp/regexp_alternation_meta.rb | 13 + meta/regexp/regexp_bol_anchor.rb | 10 + meta/regexp/regexp_bos_anchor.rb | 18 + meta/regexp/regexp_capture_group.rb | 19 + meta/regexp/regexp_eol_anchor.rb | 10 + meta/regexp/regexp_eos_anchor.rb | 8 + meta/regexp/regexp_eos_ob_eol_anchor.rb | 10 + meta/regexp/regexp_greedy_zero_or_more.rb | 12 + meta/regexp/regexp_root_expression.rb | 10 + mutant.gemspec | 1 + spec/integrations.yml | 4 + spec/unit/mutant/ast/regexp/parse_spec.rb | 19 + .../transformer/lookup_table/table_spec.rb | 21 + .../regexp/transformer/lookup_table_spec.rb | 35 + .../mutant/ast/regexp/transformer_spec.rb | 21 + spec/unit/mutant/ast/regexp_spec.rb | 712 ++++++++++++++++++ spec/unit/mutant/meta/example/dsl_spec.rb | 27 + test_app/Gemfile.minitest.lock | 2 + test_app/Gemfile.rspec3.8.lock | 2 + 43 files changed, 2003 insertions(+), 18 deletions(-) create mode 100644 lib/mutant/ast/regexp.rb create mode 100644 lib/mutant/ast/regexp/transformer.rb create mode 100644 lib/mutant/ast/regexp/transformer/direct.rb create mode 100644 lib/mutant/ast/regexp/transformer/named_group.rb create mode 100644 lib/mutant/ast/regexp/transformer/options_group.rb create mode 100644 lib/mutant/ast/regexp/transformer/quantifier.rb create mode 100644 lib/mutant/ast/regexp/transformer/recursive.rb create mode 100644 lib/mutant/ast/regexp/transformer/root.rb create mode 100644 lib/mutant/ast/regexp/transformer/text.rb create mode 100644 lib/mutant/mutator/node/regexp.rb create mode 100644 lib/mutant/mutator/node/regexp/alternation_meta.rb create mode 100644 lib/mutant/mutator/node/regexp/capture_group.rb create mode 100644 lib/mutant/mutator/node/regexp/character_type.rb create mode 100644 lib/mutant/mutator/node/regexp/end_of_line_anchor.rb create mode 100644 lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb create mode 100644 lib/mutant/mutator/node/regexp/greedy_zero_or_more.rb create mode 100644 meta/regexp/character_types.rb create mode 100644 meta/regexp/regexp_alternation_meta.rb create mode 100644 meta/regexp/regexp_bol_anchor.rb create mode 100644 meta/regexp/regexp_bos_anchor.rb create mode 100644 meta/regexp/regexp_capture_group.rb create mode 100644 meta/regexp/regexp_eol_anchor.rb create mode 100644 meta/regexp/regexp_eos_anchor.rb create mode 100644 meta/regexp/regexp_eos_ob_eol_anchor.rb create mode 100644 meta/regexp/regexp_greedy_zero_or_more.rb create mode 100644 meta/regexp/regexp_root_expression.rb create mode 100644 spec/unit/mutant/ast/regexp/parse_spec.rb create mode 100644 spec/unit/mutant/ast/regexp/transformer/lookup_table/table_spec.rb create mode 100644 spec/unit/mutant/ast/regexp/transformer/lookup_table_spec.rb create mode 100644 spec/unit/mutant/ast/regexp/transformer_spec.rb create mode 100644 spec/unit/mutant/ast/regexp_spec.rb diff --git a/Changelog.md b/Changelog.md index 6860bfb9a..4c7bf30a4 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.20 2020-12-16 [#1159](https://github.com/mbj/mutant/pull/1159) diff --git a/Gemfile.lock b/Gemfile.lock index d4a204c84..b0a63671d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,7 @@ PATH mprelude (~> 0.1.0) parser (~> 2.7.1) procto (~> 0.0.2) + regexp_parser (~> 1.8) unparser (~> 0.5.4) variable (~> 0.0.1) @@ -51,7 +52,7 @@ GEM ast (~> 2.4.1) procto (0.0.3) rainbow (3.0.0) - regexp_parser (2.0.0) + regexp_parser (1.8.2) rexml (3.2.4) rspec (3.10.0) rspec-core (~> 3.10.0) diff --git a/lib/mutant.rb b/lib/mutant.rb index 6f03924e8..0e854df34 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' @@ -52,6 +53,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' @@ -74,6 +84,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..ad535afc2 --- /dev/null +++ b/lib/mutant/ast/regexp.rb @@ -0,0 +1,43 @@ +# 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] + # + # rubocop:disable Lint/SuppressedException + def self.parse(regexp) + ::Regexp::Parser.parse(regexp) + # `regexp_parser` is more strict than MRI + # See: https://github.com/ammar/regexp_parser/issues/75 + rescue ::Regexp::Scanner::PrematureEndError + end + # rubocop:enable Lint/SuppressedException + + # 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, '(? [: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 0955a8f45..090ea02cd 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', '~> 2.7.1') gem.add_runtime_dependency('procto', '~> 0.0.2') + gem.add_runtime_dependency('regexp_parser', '~> 1.8') gem.add_runtime_dependency('unparser', '~> 0.5.4') gem.add_runtime_dependency('variable', '~> 0.0.1') diff --git a/spec/integrations.yml b/spec/integrations.yml index f59fbdb88..f83a4d6b1 100644 --- a/spec/integrations.yml +++ b/spec/integrations.yml @@ -7,6 +7,10 @@ mutation_coverage: false mutation_generation: true exclude: + # TODO: Figure out if there is a better way to handle these + - core/matchdata/begin_spec.rb + - core/matchdata/end_spec.rb + # END TODO - command_line/fixtures/bad_syntax.rb - command_line/fixtures/freeze_flag_required_diff_enc.rb - core/file/stat_spec.rb 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..e79d8bc0b --- /dev/null +++ b/spec/unit/mutant/ast/regexp/parse_spec.rb @@ -0,0 +1,19 @@ +# 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 + + context 'on regexp regexp_parser does not accept' do + it 'returns nil' do + expect(apply(/\xA/)).to be(nil) + 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 (~> 2.7.1) procto (~> 0.0.2) + regexp_parser (~> 1.8) unparser (~> 0.5.4) variable (~> 0.0.1) mutant-minitest (0.10.20) @@ -53,6 +54,7 @@ GEM parser (2.7.2.0) ast (~> 2.4.1) procto (0.0.3) + regexp_parser (1.8.2) thread_safe (0.3.6) unparser (0.5.4) abstract_type (~> 0.0.7) diff --git a/test_app/Gemfile.rspec3.8.lock b/test_app/Gemfile.rspec3.8.lock index 1351a9d2a..6484d0eae 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 (~> 2.7.1) procto (~> 0.0.2) + regexp_parser (~> 1.8) unparser (~> 0.5.4) variable (~> 0.0.1) mutant-rspec (0.10.20) @@ -52,6 +53,7 @@ GEM parser (2.7.2.0) ast (~> 2.4.1) procto (0.0.3) + regexp_parser (1.8.2) rspec (3.8.0) rspec-core (~> 3.8.0) rspec-expectations (~> 3.8.0)