Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add #start_with? and #end_with? mutations #1201

Merged
merged 2 commits into from
Jan 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# 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')`
* Add `/static\z/` -> `#end_with?` mutations:
* `a.match(/text\z/)` -> `b.end_with?('text')`
* `a.match?(/text\z/)` -> `b.end_with?('text')`
* `a =~ /text\z/` -> `b.end_with?('text')`

* [#1200](https://github.com/mbj/mutant/pull/1200)
* Add unused group name mutation: `/(?<foo>bar)/` -> `/(?<_foo>bar)/`.

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that destructuring instead of using #slice avoids the mutation we chose to ignore before.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a followup we could also promote interpolations.

a.match?(/\Afoo#{dynamic}/)

to:

a.end_with?(dynamic)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, something like that might be interesting but I am not sure I will go that deep right away. I have a couple of other small improvements for similar cases I am thinking of as well that are pending.


# 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? { |child| child.type.equal?(:str) }

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

to_ast(body_expression)
end
end # Regexp
end # AST
end # Mutant
20 changes: 3 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,17 @@ 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.
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
36 changes: 35 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,10 @@ class Send < self
}
)

REGEXP_MATCH_METHODS = %i[=~ match match?].freeze
REGEXP_START_WITH_NODES = %i[regexp_bos_anchor regexp_literal_literal].freeze
REGEXP_END_WITH_NODES = %i[regexp_literal_literal regexp_eos_anchor].freeze

private

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

def emit_selector_specific_mutations
emit_start_end_with_mutations
emit_predicate_mutations
emit_array_mutation
emit_static_send
Expand All @@ -94,6 +99,35 @@ def emit_selector_specific_mutations
emit_lambda_mutation
end

def emit_start_end_with_mutations # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
return unless REGEXP_MATCH_METHODS.include?(selector) && arguments.one?

argument = Mutant::Util.one(arguments)

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

regexp_children = regexp_ast.children

case regexp_children.map(&:type)
when REGEXP_START_WITH_NODES
emit_start_with(regexp_children)
when REGEXP_END_WITH_NODES
emit_end_with(regexp_children)
end
end

def emit_start_with(regexp_nodes)
literal = Mutant::Util.one(regexp_nodes.last.children)
emit_type(receiver, :start_with?, s(:str, literal))
end

def emit_end_with(regexp_nodes)
literal = Mutant::Util.one(regexp_nodes.first.children)
emit_type(receiver, :end_with?, s(:str, literal))
end

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

Expand Down
88 changes: 88 additions & 0 deletions meta/send.rb
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,91 @@
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

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

singleton_mutations

mutation 'a.match?(/foo\z/)'
mutation 'a.match'
mutation 'a'
mutation '/foo\z/'
mutation 'a.match(//)'
mutation 'a.match(/nomatch\A/)'
mutation 'self.match(/foo\z/)'
mutation "a.end_with?('foo')"
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
18 changes: 18 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,18 @@
# frozen_string_literal: true

RSpec.describe Mutant::AST::Regexp, '.expand_regexp_ast' do
it 'returns the expanded AST' do
parser_ast = Unparser.parse('/foo/')

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
parser_ast = Unparser.parse('/foo#{bar}/')

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