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

Fix const scope #1426

Merged
merged 3 commits into from
Mar 9, 2024
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
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
24 changes: 12 additions & 12 deletions lib/mutant/bootstrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module Bootstrap
'%<scope_class>s#name from: %<scope>s raised an error: %<exception>s'

CLASS_NAME_TYPE_MISMATCH_FORMAT =
'%<scope_class>s#name from: %<scope>s returned %<name>s'
'%<scope_class>s#name from: %<raw_scope>s returned %<name>s'

private_constant(*constants(false))

Expand Down Expand Up @@ -139,41 +139,41 @@ def self.matchable_scopes(env)
env.record(__method__) do
config = env.config

scopes = env.world.object_space.each_object(Module).with_object([]) do |scope, aggregate|
expression = expression(config.reporter, config.expression_parser, scope) || next
aggregate << Scope.new(raw: scope, expression: expression)
scopes = env.world.object_space.each_object(Module).with_object([]) do |raw_scope, aggregate|
expression = expression(config.reporter, config.expression_parser, raw_scope) || next
aggregate << Scope.new(raw: raw_scope, expression: expression)
end

scopes.sort_by { |scope| scope.expression.syntax }
end
end
private_class_method :matchable_scopes

def self.scope_name(reporter, scope)
scope.name
def self.scope_name(reporter, raw_scope)
raw_scope.name
rescue => exception
semantics_warning(
reporter,
CLASS_NAME_RAISED_EXCEPTION,
exception: exception.inspect,
scope: scope,
scope_class: scope.class
scope: raw_scope,
scope_class: raw_scope.class
)
nil
end
private_class_method :scope_name

# rubocop:disable Metrics/MethodLength
def self.expression(reporter, expression_parser, scope)
name = scope_name(reporter, scope) or return
def self.expression(reporter, expression_parser, raw_scope)
name = scope_name(reporter, raw_scope) or return

unless name.instance_of?(String)
semantics_warning(
reporter,
CLASS_NAME_TYPE_MISMATCH_FORMAT,
name: name,
scope_class: scope.class,
scope: scope
scope_class: raw_scope.class,
raw_scope: raw_scope
)
return
end
Expand Down
92 changes: 31 additions & 61 deletions lib/mutant/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +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)

NAMESPACE_DELIMITER = '::'
class ConstantScope
include AST::Sexp

# Return root node for mutation
#
# @return [Parser::AST::Node]
def root(node)
nesting.reverse.reduce(node) do |current, scope|
self.class.wrap(scope, current)
class Class < self
include Anima.new(:const, :descendant)

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

# Identification string
#
# @return [String]
def identification
scope.name
end
class Module < self
include Anima.new(:const, :descendant)

# Wrap node into ast node
#
# @param [Class, Module] scope
# @param [Parser::AST::Node] node
#
# @return [Parser::AST::Class]
# if scope is of kind Class
#
# @return [Parser::AST::Module]
# if scope is of kind module
def self.wrap(scope, node)
name = s(:const, nil, scope.name.split(NAMESPACE_DELIMITER).last.to_sym)
case scope
when Class
s(:class, name, nil, node)
when Module
s(:module, name, node)
def call(node)
s(:module, const, descendant.call(node))
end
end
end

# Nesting of scope
#
# @return [Enumerable<Class,Module>]
def nesting
const = Object
name_nesting.map do |name|
const = const.const_get(name)
class None < self
include Equalizer.new

def call(node)
node
end
end
end
memoize :nesting

# Unqualified name of scope
#
# @return [String]
def unqualified_name
name_nesting.last
def match_expressions
scope.match_expressions
end

# Match expressions for scope
# Return root node for mutation
#
# @return [Enumerable<Expression>]
def match_expressions
name_nesting.each_index.reverse_each.map do |index|
Expression::Namespace::Recursive.new(
scope_name: name_nesting.take(index.succ).join(NAMESPACE_DELIMITER)
)
end
# @return [Parser::AST::Node]
def root(node)
constant_scope.call(node)
end
memoize :match_expressions

private

def name_nesting
scope.name.split(NAMESPACE_DELIMITER)
# Identification string
#
# @return [String]
def identification
scope.raw.name
end
memoize :name_nesting

end # Context
end # Mutant
5 changes: 4 additions & 1 deletion lib/mutant/expression/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ def self.valid_method_name?(name)
private

def scope
Object.const_get(scope_name)
Scope.new(
raw: Object.const_get(scope_name),
expression: Namespace::Exact.new(scope_name: scope_name)
)
end

end # Method
Expand Down
5 changes: 4 additions & 1 deletion lib/mutant/expression/methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ def match_length(expression)
private

def scope
Object.const_get(scope_name)
Scope.new(
expression: Namespace::Exact.new(scope_name: scope_name),
raw: Object.const_get(scope_name)
)
end

end # Methods
Expand Down
8 changes: 4 additions & 4 deletions lib/mutant/expression/namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ class Exact < self
#
# @return [Matcher]
def matcher
scope = find_scope
raw_scope = find_raw_scope

if scope
Matcher::Scope.new(scope: scope)
if raw_scope
Matcher::Scope.new(scope: Scope.new(expression: self, raw: raw_scope))
else
Matcher::Null.new
end
Expand All @@ -83,7 +83,7 @@ def matcher

private

def find_scope
def find_raw_scope
Object.const_get(scope_name)
rescue NameError # rubocop:disable Lint/SuppressedException
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mutant/matcher/descendants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def call(env)
const = env.world.try_const_get(const_name) or return EMPTY_ARRAY

Chain.new(
matchers: matched_scopes(env, const).map { |scope| Scope.new(scope: scope.raw) }
matchers: matched_scopes(env, const).map { |scope| Scope.new(scope: scope) }
).call(env)
end

Expand Down
34 changes: 30 additions & 4 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 Expand Up @@ -151,9 +177,9 @@ def visibility
# end
#
# Change to this once 3.0 is EOL.
if scope.private_methods.include?(method_name)
if scope.raw.private_methods.include?(method_name)
:private
elsif scope.protected_methods.include?(method_name)
elsif scope.raw.protected_methods.include?(method_name)
:protected
else
:public
Expand Down
9 changes: 5 additions & 4 deletions lib/mutant/matcher/method/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Instance < self

# Dispatching builder, detects memoizable case
#
# @param [Class, Module] scope
# @param [Scope] scope
# @param [UnboundMethod] method
#
# @return [Matcher::Method::Instance]
Expand All @@ -31,7 +31,7 @@ def self.new(scope:, target_method:)
# rubocop:enable Metrics/MethodLength

def self.memoized_method?(scope, method_name)
scope < Adamantium && scope.memoized?(method_name)
scope.raw < Adamantium && scope.raw.memoized?(method_name)
end
private_class_method :memoized_method?

Expand All @@ -48,9 +48,9 @@ def match?(node)
end

def visibility
if scope.private_instance_methods.include?(method_name)
if scope.raw.private_instance_methods.include?(method_name)
:private
elsif scope.protected_instance_methods.include?(method_name)
elsif scope.raw.protected_instance_methods.include?(method_name)
:protected
else
:public
Expand All @@ -65,6 +65,7 @@ class Memoized < self

def source_location
scope
.raw
.unmemoized_instance_method(method_name)
.source_location
end
Expand Down
Loading