Skip to content

Commit

Permalink
Add /\Astatic/ -> #start_with? mutations
Browse files Browse the repository at this point in the history
- Adds the following mutations:
  * `a.match(/\Atext/)` -> `b.start_with?('text')`
  * `a.match?(/\Atext/)` -> `b.start_with?('text')`
  * `a =~ /\Atext/` -> `b.start_with?('text')`
- NOTE: Adds a `Mutant::AST::Regexp.expand_regexp_ast` to avoid repeating AST expansion logic in the `send` mutator. At that level, the node appears as a simple `parser` `s(:regexp ...)` node so I chose to fully parse the regexp rather than try to do a string pattern.
- Closes #169
  • Loading branch information
dgollahon committed Jan 3, 2021
1 parent 8e9d51c commit 0a5ade5
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 19 deletions.
9 changes: 9 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Unreleased

* [#1201](https://github.com/mbj/mutant/pull/1201)

* Add `/\Astatic/` -> `#start_with?` mutations:
* `a.match(/\Atext/)` -> `b.start_with?('text')`
* `a.match?(/\Atext/)` -> `b.start_with?('text')`
* `a =~ /\Atext/` -> `b.start_with?('text')`

# v0.10.25 2021-01-02-03

* [#1194](https://github.com/mbj/mutant/pull/1194)
Expand Down
17 changes: 17 additions & 0 deletions lib/mutant/ast/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ def self.to_ast(expression)
def self.to_expression(node)
Transformer.lookup(node.type).to_expression(node)
end

# Convert's a `parser` `regexp` node into more fine-grained AST nodes.
#
# @param node [Parser::AST::Node]
#
# @return [Parser::AST::Node]
def self.expand_regexp_ast(node)
*body, _opts = node.children

# NOTE: We only mutate parts of regexp body if the body is composed of
# only strings. Regular expressions with interpolation are skipped
return unless body.all? { |node| node.type.equal?(:str) }

body_expression = parse(body.map(&:children).join)

to_ast(body_expression)
end
end # Regexp
end # AST
end # Mutant
21 changes: 4 additions & 17 deletions lib/mutant/mutator/node/literal/regex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,18 @@ def dispatch
emit_type(s(:str, NULL_REGEXP_SOURCE), options)
end

# NOTE: will only mutate parts of regexp body if the
# body is composed of only strings. Regular expressions
# with interpolation are skipped
def mutate_body
return unless body.all?(&method(:n_str?))
# NOTE: will only mutate parts of regexp body if the body is composed of only strings.
# Regular expressions with interpolation are skipped.
# require 'pry'; binding.pry
return unless (body_ast = AST::Regexp.expand_regexp_ast(input))

Mutator.mutate(body_ast).each do |mutation|
source = AST::Regexp.to_expression(mutation).to_s
emit_type(s(:str, source), options)
end
end

def body_ast
AST::Regexp.to_ast(body_expression)
end

def body_expression
AST::Regexp.parse(body.map(&:children).join)
end
memoize :body_expression

def body
children.slice(0...-1)
end

end # Regex
end # Literal
end # Node
Expand Down
22 changes: 21 additions & 1 deletion lib/mutant/mutator/node/send.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class Send < self
is_a?: %i[instance_of?],
kind_of?: %i[instance_of?],
map: %i[each],
method: %i[public_method],
match: %i[match?],
method: %i[public_method],
reverse_each: %i[each],
reverse_map: %i[map each],
reverse_merge: %i[merge],
Expand All @@ -51,6 +51,8 @@ class Send < self
}
)

REGEXP_MATCH_METHODS = %i[=~ match match?]

private

def dispatch
Expand Down Expand Up @@ -84,6 +86,7 @@ def normal_dispatch
end

def emit_selector_specific_mutations
emit_start_with_mutation
emit_predicate_mutations
emit_array_mutation
emit_static_send
Expand All @@ -94,6 +97,23 @@ def emit_selector_specific_mutations
emit_lambda_mutation
end

def emit_start_with_mutation
return unless REGEXP_MATCH_METHODS.include?(selector)
return unless arguments.one?

argument = Mutant::Util.one(arguments)

return unless argument.type.equal?(:regexp) && (
regexp_ast = AST::Regexp.expand_regexp_ast(argument)
)

return unless regexp_ast.children.map(&:type).eql?(%i[regexp_bos_anchor regexp_literal_literal])

literal = Mutant::Util.one(regexp_ast.children.last.children)

emit(s(:send, receiver, :start_with?, s(:str, literal)))
end

def emit_predicate_mutations
return unless selector.match?(/\?\z/) && !selector.equal?(:defined?)

Expand Down
73 changes: 73 additions & 0 deletions meta/send.rb
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,76 @@
mutation 'a === self'
mutation 'a.is_a?(b)'
end

Mutant::Meta::Example.add :send do
source 'a.match?(/\Afoo/)'

singleton_mutations

mutation 'a'
mutation 'a.match?'
mutation '/\Afoo/'
mutation 'self.match?(/\Afoo/)'
mutation 'a.match?(//)'
mutation 'a.match?(/nomatch\A/)'
mutation "a.start_with?('foo')"
mutation 'false'
mutation 'true'
end

Mutant::Meta::Example.add :send do
source 'match(/\A\d/)'

singleton_mutations

mutation 'match'
mutation '/\A\d/'
mutation 'match?(/\A\d/)'
mutation 'match(/\A\D/)'
mutation 'match(//)'
mutation 'match(/nomatch\A/)'
end

Mutant::Meta::Example.add :send do
source 'a =~ /\Afoo/'

singleton_mutations

mutation 'a'
mutation 'nil =~ /\Afoo/'
mutation 'self =~ /\Afoo/'
mutation '/\Afoo/'
mutation 'a =~ //'
mutation 'a =~ /nomatch\A/'
mutation 'a.match?(/\Afoo/)'
end

Mutant::Meta::Example.add :send do
source 'match?(/\Afoo/, 1)'

singleton_mutations

mutation 'match?(/\Afoo/)'
mutation 'match?(1)'
mutation 'match?(/\Afoo/, nil)'
mutation 'match?(/\Afoo/, self)'
mutation 'match?(/\Afoo/, -1)'
mutation 'match?(/\Afoo/, 0)'
mutation 'match?(/\Afoo/, 2)'
mutation 'match?'
mutation 'match?(//, 1)'
mutation 'match?(/nomatch\A/, 1)'
mutation 'false'
mutation 'true'
end

Mutant::Meta::Example.add :send do
source 'foo(/\Abar/)'

singleton_mutations

mutation 'foo'
mutation '/\Abar/'
mutation 'foo(//)'
mutation 'foo(/nomatch\A/)'
end
1 change: 0 additions & 1 deletion mutant.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ matcher:
ignore:
- Mutant::Isolation::Fork::Parent#call
- Mutant::Mutator::Node::Argument#skip?
- Mutant::Mutator::Node::Literal::Regex#body
- Mutant::Mutator::Node::ProcargZero#dispatch
- Mutant::Mutator::Node::When#mutate_conditions
- Mutant::Zombifier#call
25 changes: 25 additions & 0 deletions spec/unit/mutant/ast/regexp/expand_regexp_ast_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

RSpec.describe Mutant::AST::Regexp, '.expand_regexp_ast' do
it 'returns the expanded AST' do
# /foo/
parser_ast = s(:regexp, s(:str, 'foo'), s(:regopt))

expect(described_class.expand_regexp_ast(parser_ast)).to eql(
s(:regexp_root_expression,
s(:regexp_literal_literal, "foo"))
)
end

it 'returns `nil` for complex regexps' do
# /foo#{bar}/
parser_ast =
s(:regexp,
s(:str, 'foo'),
s(:begin,
s(:send, nil, :bar)),
s(:regopt))

expect(described_class.expand_regexp_ast(parser_ast)).to be(nil)
end
end

0 comments on commit 0a5ade5

Please sign in to comment.