Skip to content

Commit

Permalink
Fix constant scope of inserted mutations
Browse files Browse the repository at this point in the history
[Fix #1422]
  • Loading branch information
mbj committed Mar 9, 2024
1 parent 70cd945 commit 4250ea8
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 97 deletions.
6 changes: 5 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# v0.11.29 [unreleased]
# v0.11.29 2024-03-09

* [#1426](https://github.com/mbj/mutant/pull/1426)
Fix mutations to unintentionally change constant scope.
This fixes: https://github.com/mbj/mutant/issues/1422.

* [#1421](https://github.com/mbj/mutant/pull/1421)
Change to optional warning display via --print-warnings environment option.
Expand Down
17 changes: 8 additions & 9 deletions lib/mutant/ast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ class AST
)

class View
include Adamantium, Anima.new(:node, :path)
include Adamantium, Anima.new(:node, :stack)
end

def on_line(line)
line_map.fetch(line, EMPTY_HASH).map do |node, path|
View.new(node: node, path: path)
line_map.fetch(line, EMPTY_HASH).map do |node, stack|
View.new(node: node, stack: stack)
end
end

Expand All @@ -22,21 +22,20 @@ def on_line(line)
def line_map
line_map = {}

walk_path(node) do |node, path|
walk_path(node, []) do |node, stack|
expression = node.location.expression || next
(line_map[expression.line] ||= []) << [node, path]
(line_map[expression.line] ||= []) << [node, stack]
end

line_map
end
memoize :line_map

def walk_path(node, stack = [node.type], &block)
block.call(node, stack.dup)
def walk_path(node, stack, &block)
block.call(node, stack)
stack = [*stack, node]
node.children.grep(::Parser::AST::Node) do |child|
stack.push(child.type)
walk_path(child, stack, &block)
stack.pop
end
end
end # AST
Expand Down
57 changes: 36 additions & 21 deletions lib/mutant/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,53 @@
module Mutant
# An abstract context where mutations can be applied to.
class Context
include Adamantium, Anima.new(:scope, :source_path)
extend AST::Sexp
include Adamantium, Anima.new(:constant_scope, :scope, :source_path)

def match_expressions
scope.match_expressions
class ConstantScope
include AST::Sexp

class Class < self
include Anima.new(:const, :descendant)

def call(node)
s(:class, const, nil, descendant.call(node))
end
end

class Module < self
include Anima.new(:const, :descendant)

def call(node)
s(:module, const, descendant.call(node))
end
end

class None < self
include Equalizer.new

def call(node)
node
end
end
end

# Identification string
#
# @return [String]
def identification
scope.raw.name
def match_expressions
scope.match_expressions
end

# Return root node for mutation
#
# @return [Parser::AST::Node]
def root(node)
scope.nesting.reverse.reduce(node) do |current, raw_scope|
self.class.wrap(raw_scope, current)
end
constant_scope.call(node)
end

# Wrap node into ast node
def self.wrap(raw_scope, node)
name = s(:const, nil, raw_scope.name.split(Scope::NAMESPACE_DELIMITER).last.to_sym)
case raw_scope
when Class
s(:class, name, nil, node)
when Module
s(:module, name, node)
end
# Identification string
#
# @return [String]
def identification
scope.raw.name
end

end # Context
end # Mutant
30 changes: 28 additions & 2 deletions lib/mutant/matcher/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class Method < self
CLOSURE_WARNING_FORMAT =
'%s is dynamically defined in a closure, unable to emit subject'

CONSTANT_SCOPES = {
class: Context::ConstantScope::Class,
module: Context::ConstantScope::Module
}.freeze

# Matched subjects
#
# @param [Env] env
Expand All @@ -28,6 +33,8 @@ def call(env)
# Present to avoid passing the env argument around in case the
# logic would be implemented directly on the Matcher::Method
# instance
#
# rubocop:disable Metrics/ClassLength
class Evaluator
include(
AbstractType,
Expand Down Expand Up @@ -57,7 +64,7 @@ def call
def match_view
return EMPTY_ARRAY if matched_view.nil?

if matched_view.path.any?(&:block.public_method(:equal?))
if matched_view.stack.any? { |node| node.type.equal?(:block) }
env.warn(CLOSURE_WARNING_FORMAT % target_method)

return EMPTY_ARRAY
Expand All @@ -80,7 +87,26 @@ def method_name
end

def context
Context.new(scope: scope, source_path: source_path)
Context.new(constant_scope: constant_scope, scope: scope, source_path: source_path)
end

# rubocop:disable Metrics/MethodLength
def constant_scope
matched_view
.stack
.reverse
.reduce(Context::ConstantScope::None.new) do |descendant, node|
klass = CONSTANT_SCOPES[node.type]

if klass
klass.new(
const: node.children.fetch(0),
descendant: descendant
)
else
descendant
end
end
end

def ast
Expand Down
5 changes: 3 additions & 2 deletions lib/mutant/meta/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ def identification
# @return [Context]
def context
Context.new(
scope: scope,
source_path: location.path
constant_scope: Context::ConstantScope::None.new,
scope: scope,
source_path: location.path
)
end

Expand Down
5 changes: 3 additions & 2 deletions spec/support/shared_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ def setup_shared_context

let(:subject_a_context) do
Mutant::Context.new(
scope: scope,
source_path: 'suvject-a.rb'
constant_scope: Mutant::Context::ConstantScope::None.new,
scope: scope,
source_path: 'subject-a.rb'
)
end

Expand Down
14 changes: 8 additions & 6 deletions spec/unit/mutant/ast_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ def apply(line)
let(:node) do
Unparser.parse(<<~RUBY)
begin
1; 2
def foo
begin
1; 2
end
end
RUBY
end
Expand All @@ -27,18 +29,18 @@ def apply(line)
it 'returns expected view' do
expect(apply(2)).to eql(
[
described_class::View.new(node: node, path: %i[kwbegin])
described_class::View.new(node: node, stack: [])
]
)
end
end

context 'line populated with more than one' do
it 'returns expected view' do
expect(apply(3)).to eql(
expect(apply(4)).to eql(
[
described_class::View.new(node: s(:int, 1), path: %i[kwbegin int]),
described_class::View.new(node: s(:int, 2), path: %i[kwbegin int])
described_class::View.new(node: s(:int, 1), stack: [node, node.children.fetch(2)]),
described_class::View.new(node: s(:int, 2), stack: [node, node.children.fetch(2)])
]
)
end
Expand Down
6 changes: 5 additions & 1 deletion spec/unit/mutant/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -746,10 +746,14 @@ def self.main_body
)
end

let(:constant_scope) do
Mutant::Context::ConstantScope::None.new
end

let(:subject_a) do
Mutant::Subject::Method::Instance.new(
config: Mutant::Subject::Config::DEFAULT,
context: Mutant::Context.new(scope: scope, source_path: 'subject.rb'),
context: Mutant::Context.new(constant_scope: constant_scope, scope: scope, source_path: 'subject.rb'),
node: s(:def, :send, s(:args), nil),
visibility: :public
)
Expand Down
Loading

0 comments on commit 4250ea8

Please sign in to comment.