From 1d6287f9d1d9f87391bcffd673c6a2b44bed7f49 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Fri, 8 Mar 2024 04:52:43 +0000 Subject: [PATCH 1/3] Change to consistently use scope object --- lib/mutant/bootstrap.rb | 24 ++-- lib/mutant/context.rb | 23 +--- lib/mutant/expression/method.rb | 5 +- lib/mutant/expression/methods.rb | 5 +- lib/mutant/expression/namespace.rb | 8 +- lib/mutant/matcher/descendants.rb | 2 +- lib/mutant/matcher/method.rb | 4 +- lib/mutant/matcher/method/instance.rb | 9 +- lib/mutant/matcher/methods.rb | 15 ++- lib/mutant/matcher/namespace.rb | 2 +- lib/mutant/meta/example.rb | 11 +- lib/mutant/subject/method.rb | 2 +- lib/mutant/subject/method/instance.rb | 5 +- lib/mutant/subject/method/metaclass.rb | 2 +- lib/mutant/subject/method/singleton.rb | 4 +- spec/support/shared_context.rb | 9 +- spec/unit/mutant/bootstrap_spec.rb | 11 +- spec/unit/mutant/cli_spec.rb | 9 +- spec/unit/mutant/context_spec.rb | 30 ++++- spec/unit/mutant/expression/method_spec.rb | 11 ++ spec/unit/mutant/expression/methods_spec.rb | 13 +- .../mutant/expression/namespace/exact_spec.rb | 9 +- spec/unit/mutant/matcher/descendants_spec.rb | 5 +- .../mutant/matcher/method/instance_spec.rb | 127 +++++++++++++++--- .../mutant/matcher/method/metaclass_spec.rb | 80 ++++++++--- .../mutant/matcher/method/singleton_spec.rb | 88 +++++++++--- .../mutant/matcher/methods/instance_spec.rb | 30 +++-- .../mutant/matcher/methods/metaclass_spec.rb | 13 +- .../mutant/matcher/methods/singleton_spec.rb | 13 +- spec/unit/mutant/matcher/namespace_spec.rb | 2 +- spec/unit/mutant/matcher/scope_spec.rb | 8 +- spec/unit/mutant/meta/example_spec.rb | 9 +- .../mutant/subject/method/instance_spec.rb | 48 ++++--- .../mutant/subject/method/metaclass_spec.rb | 23 ++-- .../mutant/subject/method/singleton_spec.rb | 19 +-- 35 files changed, 491 insertions(+), 187 deletions(-) diff --git a/lib/mutant/bootstrap.rb b/lib/mutant/bootstrap.rb index 555173e59..54e2a8a70 100644 --- a/lib/mutant/bootstrap.rb +++ b/lib/mutant/bootstrap.rb @@ -20,7 +20,7 @@ module Bootstrap '%s#name from: %s raised an error: %s' CLASS_NAME_TYPE_MISMATCH_FORMAT = - '%s#name from: %s returned %s' + '%s#name from: %s returned %s' private_constant(*constants(false)) @@ -139,9 +139,9 @@ 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 } @@ -149,31 +149,31 @@ def self.matchable_scopes(env) 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 diff --git a/lib/mutant/context.rb b/lib/mutant/context.rb index 0b7fc27da..1828d1f3c 100644 --- a/lib/mutant/context.rb +++ b/lib/mutant/context.rb @@ -12,8 +12,8 @@ class Context # # @return [Parser::AST::Node] def root(node) - nesting.reverse.reduce(node) do |current, scope| - self.class.wrap(scope, current) + nesting.reverse.reduce(node) do |current, raw_scope| + self.class.wrap(raw_scope, current) end end @@ -21,22 +21,13 @@ def root(node) # # @return [String] def identification - scope.name + scope.raw.name end # 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 + def self.wrap(raw_scope, node) + name = s(:const, nil, raw_scope.name.split(NAMESPACE_DELIMITER).last.to_sym) + case raw_scope when Class s(:class, name, nil, node) when Module @@ -77,7 +68,7 @@ def match_expressions private def name_nesting - scope.name.split(NAMESPACE_DELIMITER) + scope.raw.name.split(NAMESPACE_DELIMITER) end memoize :name_nesting diff --git a/lib/mutant/expression/method.rb b/lib/mutant/expression/method.rb index b1fd67bf9..ecfcb6171 100644 --- a/lib/mutant/expression/method.rb +++ b/lib/mutant/expression/method.rb @@ -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 diff --git a/lib/mutant/expression/methods.rb b/lib/mutant/expression/methods.rb index 6a5df5647..89bd81432 100644 --- a/lib/mutant/expression/methods.rb +++ b/lib/mutant/expression/methods.rb @@ -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 diff --git a/lib/mutant/expression/namespace.rb b/lib/mutant/expression/namespace.rb index e44af252c..1319f2053 100644 --- a/lib/mutant/expression/namespace.rb +++ b/lib/mutant/expression/namespace.rb @@ -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 @@ -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 diff --git a/lib/mutant/matcher/descendants.rb b/lib/mutant/matcher/descendants.rb index 5f73dc635..574d95a43 100644 --- a/lib/mutant/matcher/descendants.rb +++ b/lib/mutant/matcher/descendants.rb @@ -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 diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb index bbc54bf3b..4b9f07dfe 100644 --- a/lib/mutant/matcher/method.rb +++ b/lib/mutant/matcher/method.rb @@ -151,9 +151,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 diff --git a/lib/mutant/matcher/method/instance.rb b/lib/mutant/matcher/method/instance.rb index 19556a8d3..4e2015051 100644 --- a/lib/mutant/matcher/method/instance.rb +++ b/lib/mutant/matcher/method/instance.rb @@ -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] @@ -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? @@ -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 @@ -65,6 +65,7 @@ class Memoized < self def source_location scope + .raw .unmemoized_instance_method(method_name) .source_location end diff --git a/lib/mutant/matcher/methods.rb b/lib/mutant/matcher/methods.rb index c6aa50f8a..150bb725b 100644 --- a/lib/mutant/matcher/methods.rb +++ b/lib/mutant/matcher/methods.rb @@ -57,11 +57,11 @@ class Singleton < self private def access(_env, method_name) - scope.method(method_name) + scope.raw.method(method_name) end def candidate_scope - scope.singleton_class + scope.raw.singleton_class end end # Singleton @@ -73,11 +73,11 @@ class Metaclass < self private def access(_env, method_name) - scope.method(method_name) + scope.raw.method(method_name) end def candidate_scope - scope.singleton_class + scope.raw.singleton_class end end # Metaclass @@ -105,18 +105,19 @@ class Instance < self private # rubocop:disable Lint/RescueException + # mutant:disable - unstable source locations under < ruby-3.2 def access(env, method_name) - scope.instance_method(method_name) + candidate_scope.instance_method(method_name) rescue Exception => exception env.warn( - MESSAGE % { scope: scope, method_name: method_name, exception: exception } + MESSAGE % { scope: scope, method_name: method_name, exception: exception.inspect } ) nil end # rubocop:enable Lint/RescueException def candidate_scope - scope + scope.raw end end # Instance diff --git a/lib/mutant/matcher/namespace.rb b/lib/mutant/matcher/namespace.rb index 9b056bd6f..bfcb6a86f 100644 --- a/lib/mutant/matcher/namespace.rb +++ b/lib/mutant/matcher/namespace.rb @@ -13,7 +13,7 @@ class Namespace < self # @return [Enumerable] def call(env) Chain.new( - matchers: matched_scopes(env).map { |scope| Scope.new(scope: scope.raw) } + matchers: matched_scopes(env).map { |scope| Scope.new(scope: scope) } ).call(env) end diff --git a/lib/mutant/meta/example.rb b/lib/mutant/meta/example.rb index 06531c665..eaa547b36 100644 --- a/lib/mutant/meta/example.rb +++ b/lib/mutant/meta/example.rb @@ -39,7 +39,7 @@ def identification # @return [Context] def context Context.new( - scope: Object, + scope: scope, source_path: location.path ) end @@ -65,6 +65,15 @@ def generated end memoize :generated + private + + def scope + Scope.new( + expression: Expression::Namespace::Exact.new(scope_name: 'Object'), + raw: Object + ) + end + end # Example end # Meta end # Mutant diff --git a/lib/mutant/subject/method.rb b/lib/mutant/subject/method.rb index 3d150d29b..7659d9454 100644 --- a/lib/mutant/subject/method.rb +++ b/lib/mutant/subject/method.rb @@ -20,7 +20,7 @@ def expression Expression::Method.new( method_name: name.to_s, scope_symbol: self.class::SYMBOL, - scope_name: scope.name + scope_name: scope.raw.name ) end memoize :expression diff --git a/lib/mutant/subject/method/instance.rb b/lib/mutant/subject/method/instance.rb index 528287519..2d61d7e11 100644 --- a/lib/mutant/subject/method/instance.rb +++ b/lib/mutant/subject/method/instance.rb @@ -13,12 +13,12 @@ class Instance < self # # @return [self] def prepare - scope.undef_method(name) + scope.raw.undef_method(name) self end def post_insert - scope.__send__(visibility, name) + scope.raw.__send__(visibility, name) self end @@ -31,6 +31,7 @@ class Memoized < self # @return [self] def prepare scope + .raw .instance_variable_get(:@memoized_methods) .delete(name) diff --git a/lib/mutant/subject/method/metaclass.rb b/lib/mutant/subject/method/metaclass.rb index 867b29faa..075ace507 100644 --- a/lib/mutant/subject/method/metaclass.rb +++ b/lib/mutant/subject/method/metaclass.rb @@ -15,7 +15,7 @@ class Metaclass < self # # @return [self] def prepare - scope.singleton_class.public_send(:undef_method, name) + scope.raw.singleton_class.undef_method(name) self end diff --git a/lib/mutant/subject/method/singleton.rb b/lib/mutant/subject/method/singleton.rb index 353180ba5..de89fe61d 100644 --- a/lib/mutant/subject/method/singleton.rb +++ b/lib/mutant/subject/method/singleton.rb @@ -13,12 +13,12 @@ class Singleton < self # # @return [self] def prepare - scope.singleton_class.__send__(:undef_method, name) + scope.raw.singleton_class.undef_method(name) self end def post_insert - scope.singleton_class.__send__(visibility, name) + scope.raw.singleton_class.__send__(visibility, name) self end diff --git a/spec/support/shared_context.rb b/spec/support/shared_context.rb index ce0803c3b..324ddb22d 100644 --- a/spec/support/shared_context.rb +++ b/spec/support/shared_context.rb @@ -119,9 +119,16 @@ def setup_shared_context ) end + let(:scope) do + Mutant::Scope.new( + expression: Mutant::Expression::Namespace::Exact.new(scope_name: 'Object'), + raw: Object + ) + end + let(:subject_a_context) do Mutant::Context.new( - scope: Object, + scope: scope, source_path: 'suvject-a.rb' ) end diff --git a/spec/unit/mutant/bootstrap_spec.rb b/spec/unit/mutant/bootstrap_spec.rb index 9a3435891..14d202457 100644 --- a/spec/unit/mutant/bootstrap_spec.rb +++ b/spec/unit/mutant/bootstrap_spec.rb @@ -298,8 +298,15 @@ def object.name ) end + let(:scope) do + Mutant::Scope.new( + expression: parse_expression('TestApp::Literal'), + raw: TestApp::Literal + ) + end + let(:expected_subjects) do - Mutant::Matcher::Scope.new(scope: TestApp::Literal).call(env_initial) + Mutant::Matcher::Scope.new(scope: scope).call(env_initial) end let(:expected_env) do @@ -330,7 +337,7 @@ def object.name ) Mutant::Matcher::Scope - .new(scope: TestApp::Literal) + .new(scope: scope) .call(Mutant::Env.empty(world, config)).last end diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index 4c00ec97b..2f2a1845f 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -739,10 +739,17 @@ def self.main_body ) end + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: Object + ) + end + let(:subject_a) do Mutant::Subject::Method::Instance.new( config: Mutant::Subject::Config::DEFAULT, - context: Mutant::Context.new(scope: Object, source_path: 'subject.rb'), + context: Mutant::Context.new(scope: scope, source_path: 'subject.rb'), node: s(:def, :send, s(:args), nil), visibility: :public ) diff --git a/spec/unit/mutant/context_spec.rb b/spec/unit/mutant/context_spec.rb index fd5e14580..d9a05552f 100644 --- a/spec/unit/mutant/context_spec.rb +++ b/spec/unit/mutant/context_spec.rb @@ -2,12 +2,12 @@ RSpec.describe Mutant::Context do describe '.wrap' do - subject { described_class.wrap(scope, node) } + subject { described_class.wrap(raw_scope, node) } let(:node) { s(:str, 'test') } context 'with Module as scope' do - let(:scope) { Mutant } + let(:raw_scope) { Mutant } let(:expected) do s(:module, @@ -19,7 +19,7 @@ end context 'with Class as scope' do - let(:scope) { Mutant::Context } + let(:raw_scope) { Mutant::Context } let(:expected) do s(:class, @@ -34,12 +34,18 @@ let(:object) { described_class.new(scope: scope, source_path: source_path) } let(:source_path) { instance_double(Pathname) } - let(:scope) { TestApp::Literal } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: TestApp::Literal + ) + end describe '#identification' do subject { object.identification } - it { should eql(scope.name) } + it { should eql(scope.raw.name) } end describe '#root' do @@ -70,7 +76,12 @@ class Literal subject { object.unqualified_name } context 'with top level constant name' do - let(:scope) { TestApp } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: TestApp + ) + end it 'should return the unqualified name' do should eql('TestApp') @@ -92,7 +103,12 @@ class Literal subject { object.match_expressions } context 'on toplevel scope' do - let(:scope) { TestApp } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: TestApp + ) + end it { should eql([parse_expression('TestApp*')]) } end diff --git a/spec/unit/mutant/expression/method_spec.rb b/spec/unit/mutant/expression/method_spec.rb index f65e50d8c..a35feb742 100644 --- a/spec/unit/mutant/expression/method_spec.rb +++ b/spec/unit/mutant/expression/method_spec.rb @@ -30,6 +30,17 @@ context 'with an instance method' do let(:input) { instance_method } + it 'uses expected scope' do + expect(subject.matcher.matchers.map(&:scope)).to eql( + [ + Mutant::Scope.new( + expression: Mutant::Expression::Namespace::Exact.new(scope_name: 'TestApp::Literal'), + raw: TestApp::Literal + ) + ] + ) + end + it 'returns correct matcher' do expect(subject.call(env).map(&:expression)).to eql([object]) end diff --git a/spec/unit/mutant/expression/methods_spec.rb b/spec/unit/mutant/expression/methods_spec.rb index c7dd297ae..ccaf76e63 100644 --- a/spec/unit/mutant/expression/methods_spec.rb +++ b/spec/unit/mutant/expression/methods_spec.rb @@ -50,13 +50,20 @@ describe '#matcher' do subject { object.matcher } + let(:scope) do + Mutant::Scope.new( + expression: Mutant::Expression::Namespace::Exact.new(scope_name: 'TestApp::Literal'), + raw: TestApp::Literal + ) + end + context 'with an instance method' do let(:attributes) { { scope_name: 'TestApp::Literal', scope_symbol: '#' } } specify do should eql( Mutant::Matcher::Chain.new( - matchers: [Mutant::Matcher::Methods::Instance.new(scope: TestApp::Literal)] + matchers: [Mutant::Matcher::Methods::Instance.new(scope: scope)] ) ) end @@ -69,8 +76,8 @@ should eql( Mutant::Matcher::Chain.new( matchers: [ - Mutant::Matcher::Methods::Singleton.new(scope: TestApp::Literal), - Mutant::Matcher::Methods::Metaclass.new(scope: TestApp::Literal) + Mutant::Matcher::Methods::Singleton.new(scope: scope), + Mutant::Matcher::Methods::Metaclass.new(scope: scope) ] ) ) diff --git a/spec/unit/mutant/expression/namespace/exact_spec.rb b/spec/unit/mutant/expression/namespace/exact_spec.rb index ec4c61006..47644a3c4 100644 --- a/spec/unit/mutant/expression/namespace/exact_spec.rb +++ b/spec/unit/mutant/expression/namespace/exact_spec.rb @@ -7,6 +7,13 @@ describe '#matcher' do subject { object.matcher } + let(:scope) do + Mutant::Scope.new( + expression: Mutant::Expression::Namespace::Exact.new(scope_name: 'TestApp::Literal'), + raw: TestApp::Literal + ) + end + context 'when constant does not exist' do let(:input) { 'TestApp::DoesNotExist' } @@ -14,7 +21,7 @@ end context 'when constant exists' do - it { should eql(Mutant::Matcher::Scope.new(scope: TestApp::Literal)) } + it { should eql(Mutant::Matcher::Scope.new(scope: scope)) } end end diff --git a/spec/unit/mutant/matcher/descendants_spec.rb b/spec/unit/mutant/matcher/descendants_spec.rb index f7b0c46fa..ba1c6d8eb 100644 --- a/spec/unit/mutant/matcher/descendants_spec.rb +++ b/spec/unit/mutant/matcher/descendants_spec.rb @@ -15,7 +15,10 @@ def apply Mutant::Subject::Method::Instance.new( config: Mutant::Subject::Config::DEFAULT, context: Mutant::Context.new( - scope: TestApp::Foo::Bar::Baz, + scope: Mutant::Scope.new( + raw: TestApp::Foo::Bar::Baz, + expression: parse_expression('TestApp::Foo::Bar::Baz') + ), source_path: TestApp::ROOT.join('lib/test_app.rb') ), node: s(:def, :foo, s(:args), nil), diff --git a/spec/unit/mutant/matcher/method/instance_spec.rb b/spec/unit/mutant/matcher/method/instance_spec.rb index ff42410a2..dc985d073 100644 --- a/spec/unit/mutant/matcher/method/instance_spec.rb +++ b/spec/unit/mutant/matcher/method/instance_spec.rb @@ -4,7 +4,7 @@ subject { object.call(env) } let(:base) { TestApp::InstanceMethodTests } - let(:method) { scope.instance_method(method_name) } + let(:method) { scope.raw.instance_method(method_name) } let(:method_arity) { 0 } let(:method_name) { :foo } let(:object) { described_class.new(scope: scope, target_method: method) } @@ -40,7 +40,13 @@ def arguments end context 'when method is defined inside file that does not end with .rb' do - let(:scope) { base::WithMemoizer } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::WithMemoizer + ) + end + let(:source_location) { [file, instance_double(Integer)] } let(:file) { 'example.erb' } @@ -72,7 +78,13 @@ def arguments let(:expected_warnings) { [] } let(:method_line) { 13 } let(:method_name) { :bar } - let(:scope) { base::WithMemoizer } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::WithMemoizer + ) + end before do allow(diff_a).to receive(:touches_path?).with(source_path).and_return(diff_a_touches?) @@ -120,8 +132,14 @@ def arguments end context 'when method is defined inside eval' do - let(:scope) { base::WithMemoizer } - let(:method) { scope.instance_method(:boz) } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::WithMemoizer + ) + end + + let(:method) { scope.raw.instance_method(:boz) } let(:expected_warnings) do [ @@ -133,8 +151,14 @@ def arguments end context 'when method is defined without source location' do - let(:scope) { Module } - let(:method) { scope.instance_method(:object_id) } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: Module + ) + end + + let(:method) { scope.raw.instance_method(:object_id) } let(:expected_warnings) do [ @@ -146,7 +170,12 @@ def arguments end context 'in module eval' do - let(:scope) { base::InModuleEval } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::InModuleEval + ) + end let(:expected_warnings) do [ @@ -158,7 +187,12 @@ def arguments end context 'in class eval' do - let(:scope) { base::InClassEval } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::InClassEval + ) + end let(:expected_warnings) do [ @@ -170,15 +204,22 @@ def arguments end context 'when method is defined once' do - let(:method_name) { :bar } - let(:scope) { base::WithMemoizer } - let(:method_line) { 13 } + let(:method_name) { :bar } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::WithMemoizer + ) + end + + let(:method_line) { 13 } it_should_behave_like 'a method matcher' let(:context) do Mutant::Context.new( - scope: TestApp::InstanceMethodTests::WithMemoizer, + scope: scope, source_path: MutantSpec::ROOT.join('test_app', 'lib', 'test_app.rb') ) end @@ -198,7 +239,7 @@ def arguments context 'with %s visibility' % visibility do let(:expected_visibility) { visibility } - before { context.scope.__send__(visibility, method_name) } + before { context.scope.raw.__send__(visibility, method_name) } it 'returns expected subjects' do expect(subject).to eql(expected_subjects) @@ -208,15 +249,27 @@ def arguments end context 'when method is defined once with a memoizer' do - let(:scope) { base::WithMemoizer } - let(:method_line) { 15 } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::WithMemoizer + ) + end + + let(:method_line) { 15 } it_should_behave_like 'a method matcher' end context 'when method is defined multiple times' do context 'on different lines' do - let(:scope) { base::DefinedMultipleTimes::DifferentLines } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedMultipleTimes::DifferentLines + ) + end + let(:method_line) { 24 } let(:method_arity) { 1 } @@ -224,7 +277,13 @@ def arguments end context 'on the same line' do - let(:scope) { base::DefinedMultipleTimes::SameLineSameScope } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedMultipleTimes::SameLineSameScope + ) + end + let(:method_line) { 29 } let(:method_arity) { 1 } @@ -232,7 +291,13 @@ def arguments end context 'on the same line with different scope' do - let(:scope) { base::DefinedMultipleTimes::SameLineDifferentScope } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedMultipleTimes::SameLineDifferentScope + ) + end + let(:method_line) { 33 } let(:method_arity) { 1 } @@ -241,7 +306,13 @@ def arguments end context 'with sorbet signature' do - let(:scope) { base::WithSignature } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::WithSignature + ) + end + let(:method_line) { 116 } let(:method_arity) { 0 } @@ -249,7 +320,13 @@ def arguments end context 'on delegate class' do - let(:scope) { TestApp::DelegateTest } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: TestApp::DelegateTest + ) + end + let(:method_line) { 134 } let(:method_arity) { 0 } @@ -257,7 +334,13 @@ def arguments end context 'on inline disabled method' do - let(:scope) { TestApp::InlineDisabled } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: TestApp::InlineDisabled + ) + end + let(:method_line) { 148 } let(:method_arity) { 0 } diff --git a/spec/unit/mutant/matcher/method/metaclass_spec.rb b/spec/unit/mutant/matcher/method/metaclass_spec.rb index 13bd1605f..e5dd2f384 100644 --- a/spec/unit/mutant/matcher/method/metaclass_spec.rb +++ b/spec/unit/mutant/matcher/method/metaclass_spec.rb @@ -6,7 +6,7 @@ subject { object.call(env) } let(:object) { described_class.new(scope: scope, target_method: method) } - let(:method) { scope.public_method(method_name) } + let(:method) { scope.raw.public_method(method_name) } let(:type) { :def } let(:method_name) { :foo } let(:method_arity) { 0 } @@ -38,7 +38,13 @@ def arguments end context 'when also defined on lvar' do - let(:scope) { base::DefinedOnLvar } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnLvar + ) + end + let(:expected_warnings) do [ 'Can only match :def inside :sclass on :self or :const, got :sclass on :lvar unable to match' @@ -49,30 +55,54 @@ def arguments end context 'when defined on self' do - let(:scope) { base::DefinedOnSelf } - let(:method_line) { 7 } + let(:method_line) { 7 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnSelf + ) + end it_should_behave_like 'a method matcher' context 'when scope is a metaclass' do - let(:scope) { base::DefinedOnSelf::InsideMetaclass.metaclass } let(:method_line) { 28 } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnSelf::InsideMetaclass.metaclass + ) + end + it_should_behave_like 'a method matcher' end end context 'when defined on constant' do context 'inside namespace' do - let(:scope) { base::DefinedOnConstant::InsideNamespace } - let(:method_line) { 44 } + let(:method_line) { 44 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnConstant::InsideNamespace + ) + end it_should_behave_like 'a method matcher' end context 'outside namespace' do - let(:scope) { base::DefinedOnConstant::OutsideNamespace } - let(:method_line) { 52 } + let(:method_line) { 52 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnConstant::OutsideNamespace + ) + end it_should_behave_like 'a method matcher' end @@ -80,16 +110,28 @@ def arguments context 'when defined multiple times in the same line' do context 'with method on different scope' do - let(:scope) { base::DefinedMultipleTimes::SameLine::DifferentScope } - let(:method_line) { 76 } - let(:method_arity) { 1 } + let(:method_line) { 76 } + let(:method_arity) { 1 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedMultipleTimes::SameLine::DifferentScope + ) + end it_should_behave_like 'a method matcher' end context 'with different name' do - let(:scope) { base::DefinedMultipleTimes::SameLine::DifferentName } - let(:method_line) { 80 } + let(:method_line) { 80 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedMultipleTimes::SameLine::DifferentName + ) + end it_should_behave_like 'a method matcher' end @@ -98,8 +140,14 @@ def arguments # tests that the evaluator correctly returns nil when the metaclass doesn't # directly contain the method context 'when defined inside a class in a metaclass' do - let(:scope) { base::NotActuallyInAMetaclass } - let(:method) { scope.metaclass::SomeClass.new.public_method(:foo) } + let(:method) { scope.raw.metaclass::SomeClass.new.public_method(:foo) } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::NotActuallyInAMetaclass + ) + end it { is_expected.to be_empty } end diff --git a/spec/unit/mutant/matcher/method/singleton_spec.rb b/spec/unit/mutant/matcher/method/singleton_spec.rb index e31be05bb..b90aa16af 100644 --- a/spec/unit/mutant/matcher/method/singleton_spec.rb +++ b/spec/unit/mutant/matcher/method/singleton_spec.rb @@ -4,7 +4,7 @@ subject { object.call(env) } let(:object) { described_class.new(scope: scope, target_method: method) } - let(:method) { scope.method(method_name) } + let(:method) { scope.raw.method(method_name) } let(:type) { :defs } let(:method_name) { :foo } let(:method_arity) { 0 } @@ -36,7 +36,13 @@ def arguments end context 'when also defined on lvar' do - let(:scope) { base::DefinedOnLvar } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnLvar + ) + end + let(:expected_warnings) do [ 'Can only match :defs on :self or :const got :lvar unable to match' @@ -47,14 +53,20 @@ def arguments end context 'when defined on self' do - let(:scope) { base::DefinedOnSelf } - let(:method_line) { 61 } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnSelf + ) + end + + let(:method_line) { 61 } it_should_behave_like 'a method matcher' do %i[public protected private].each do |visibility| context 'with %s visibility' % visibility do before do - scope.singleton_class.__send__(visibility, method_name) + scope.raw.singleton_class.__send__(visibility, method_name) end it 'returns expected subjects' do @@ -67,15 +79,27 @@ def arguments context 'when defined on constant' do context 'inside namespace' do - let(:scope) { base::DefinedOnConstant::InsideNamespace } - let(:method_line) { 71 } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnConstant::InsideNamespace + ) + end + + let(:method_line) { 71 } it_should_behave_like 'a method matcher' end context 'outside namespace' do - let(:scope) { base::DefinedOnConstant::OutsideNamespace } - let(:method_line) { 78 } + let(:method_line) { 78 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedOnConstant::OutsideNamespace + ) + end it_should_behave_like 'a method matcher' end @@ -83,33 +107,57 @@ def arguments context 'when defined multiple times in the same line' do context 'with method on different scope' do - let(:scope) { base::DefinedMultipleTimes::SameLine::DifferentScope } - let(:method_line) { 97 } - let(:method_arity) { 1 } + let(:method_line) { 97 } + let(:method_arity) { 1 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedMultipleTimes::SameLine::DifferentScope + ) + end it_should_behave_like 'a method matcher' end context 'with different name' do - let(:scope) { base::DefinedMultipleTimes::SameLine::DifferentName } - let(:method_line) { 101 } + let(:method_line) { 101 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::DefinedMultipleTimes::SameLine::DifferentName + ) + end it_should_behave_like 'a method matcher' end end context 'with sorbet signature' do - let(:scope) { base::WithSignature } - let(:method_line) { 126 } - let(:method_arity) { 0 } + let(:method_line) { 126 } + let(:method_arity) { 0 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: base::WithSignature + ) + end it_should_behave_like 'a method matcher' end context 'on inline disabled method' do - let(:scope) { TestApp::InlineDisabled } - let(:method_line) { 152 } - let(:method_arity) { 0 } + let(:method_line) { 152 } + let(:method_arity) { 0 } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: TestApp::InlineDisabled + ) + end it_should_behave_like 'a method matcher' do it 'returns disabled inline config' do diff --git a/spec/unit/mutant/matcher/methods/instance_spec.rb b/spec/unit/mutant/matcher/methods/instance_spec.rb index 5d90a3cb8..8944e4a95 100644 --- a/spec/unit/mutant/matcher/methods/instance_spec.rb +++ b/spec/unit/mutant/matcher/methods/instance_spec.rb @@ -1,7 +1,14 @@ # frozen_string_literal: true RSpec.describe Mutant::Matcher::Methods::Instance, '#call' do - let(:object) { described_class.new(scope: class_under_test) } + let(:object) { described_class.new(scope: scope) } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: class_under_test + ) + end let(:env) do config = Fixtures::TEST_ENV.config @@ -76,7 +83,7 @@ def method_c; end expect(matcher).to receive(:call).with(env).and_return([subject]) expect(Mutant::Matcher::Method::Instance).to receive(:new) - .with(scope: class_under_test, target_method: class_under_test.instance_method(method)) + .with(scope: scope, target_method: class_under_test.instance_method(method)) .and_return(matcher) end end @@ -90,12 +97,15 @@ def method_c; end let(:object) { described_class.new(scope: scope) } let(:scope) do - Class.new do - def self.public_instance_methods(ancestors) - fail if ancestors - %i[foo] + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: Class.new do + def self.public_instance_methods(ancestors) + fail if ancestors + %i[foo] + end end - end + ) end def apply @@ -111,14 +121,16 @@ def apply method_name = :foo + candidate_scope = scope.raw + exception = begin - scope.instance_method(method_name) + candidate_scope.instance_method(method_name) rescue NameError => exception exception end - expect(capture_reporter.warnings).to eql([<<~'MESSAGE' % { scope: scope, exception: exception }]) + expect(capture_reporter.warnings).to eql([<<~'MESSAGE' % { scope: scope, exception: exception.inspect }]) Caught an exception while accessing a method with #instance_method that is part of #{public,private,protected}_instance_methods. diff --git a/spec/unit/mutant/matcher/methods/metaclass_spec.rb b/spec/unit/mutant/matcher/methods/metaclass_spec.rb index 7de869bca..ef4430469 100644 --- a/spec/unit/mutant/matcher/methods/metaclass_spec.rb +++ b/spec/unit/mutant/matcher/methods/metaclass_spec.rb @@ -1,8 +1,15 @@ # frozen_string_literal: true RSpec.describe Mutant::Matcher::Methods::Metaclass, '#call' do - let(:object) { described_class.new(scope: class_under_test) } - let(:env) { Fixtures::TEST_ENV } + let(:object) { described_class.new(scope: scope) } + let(:env) { Fixtures::TEST_ENV } + + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: class_under_test + ) + end let(:class_under_test) do parent = Module.new do @@ -51,7 +58,7 @@ def method_c; end method_c: subject_c }.each do |method, subject| allow(matcher).to receive(:new) - .with(scope: class_under_test, target_method: class_under_test.method(method)) + .with(scope: scope, target_method: class_under_test.method(method)) .and_return(Mutant::Matcher::Static.new(subjects: [subject])) end end diff --git a/spec/unit/mutant/matcher/methods/singleton_spec.rb b/spec/unit/mutant/matcher/methods/singleton_spec.rb index 8eb2bbf5a..0c37f26be 100644 --- a/spec/unit/mutant/matcher/methods/singleton_spec.rb +++ b/spec/unit/mutant/matcher/methods/singleton_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true RSpec.describe Mutant::Matcher::Methods::Singleton, '#call' do - let(:object) { described_class.new(scope: class_under_test) } - let(:env) { Fixtures::TEST_ENV } + let(:object) { described_class.new(scope: scope) } + let(:env) { Fixtures::TEST_ENV } let(:class_under_test) do parent = Module.new do @@ -31,6 +31,13 @@ def self.method_c; end let(:subjects) { [subject_a, subject_b, subject_c] } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: class_under_test + ) + end + before do matcher = Mutant::Matcher::Method::Singleton @@ -40,7 +47,7 @@ def self.method_c; end method_c: subject_c }.each do |method, subject| allow(matcher).to receive(:new) - .with(scope: class_under_test, target_method: class_under_test.method(method)) + .with(scope: scope, target_method: class_under_test.method(method)) .and_return(Mutant::Matcher::Static.new(subjects: [subject])) end end diff --git a/spec/unit/mutant/matcher/namespace_spec.rb b/spec/unit/mutant/matcher/namespace_spec.rb index bec0bc83b..b27698167 100644 --- a/spec/unit/mutant/matcher/namespace_spec.rb +++ b/spec/unit/mutant/matcher/namespace_spec.rb @@ -22,7 +22,7 @@ expect(matcher).to receive(:call).with(env).and_return(subjects) expect(klass).to receive(:new) - .with(scope: scope) + .with(scope: Mutant::Scope.new(raw: scope, expression: parse_expression(scope.name))) .and_return(matcher) end diff --git a/spec/unit/mutant/matcher/scope_spec.rb b/spec/unit/mutant/matcher/scope_spec.rb index 7e5e44224..84fbb8e6f 100644 --- a/spec/unit/mutant/matcher/scope_spec.rb +++ b/spec/unit/mutant/matcher/scope_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe Mutant::Matcher::Scope, '#call' do - let(:scope) { TestApp } let(:object) { described_class.new(scope: scope) } let(:env) { instance_double(Mutant::Env) } let(:matcher_a) { instance_double(Mutant::Matcher) } @@ -11,6 +10,13 @@ let(:subject_b) { instance_double(Mutant::Subject) } let(:subject_c) { instance_double(Mutant::Subject) } + let(:scope) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: TestApp + ) + end + subject { object.call(env) } before do diff --git a/spec/unit/mutant/meta/example_spec.rb b/spec/unit/mutant/meta/example_spec.rb index a4d87c0be..a1eca5514 100644 --- a/spec/unit/mutant/meta/example_spec.rb +++ b/spec/unit/mutant/meta/example_spec.rb @@ -53,7 +53,14 @@ describe '#context' do subject { object.context } - it { should eql(Mutant::Context.new(scope: Object, source_path: location.path)) } + let(:scope) do + Mutant::Scope.new( + expression: Mutant::Expression::Namespace::Exact.new(scope_name: 'Object'), + raw: Object + ) + end + + it { should eql(Mutant::Context.new(scope: scope, source_path: location.path)) } end describe '#identification' do diff --git a/spec/unit/mutant/subject/method/instance_spec.rb b/spec/unit/mutant/subject/method/instance_spec.rb index d858534b3..025e901d1 100644 --- a/spec/unit/mutant/subject/method/instance_spec.rb +++ b/spec/unit/mutant/subject/method/instance_spec.rb @@ -20,19 +20,22 @@ end let(:scope) do - Class.new do - attr_reader :bar + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: Class.new do + attr_reader :bar - def initialize - @bar = :boo - end + def initialize + @bar = :boo + end - def foo; end + def foo; end - def self.name - 'Test' + def self.name + 'Test' + end end - end + ) end describe '#expression' do @@ -56,7 +59,7 @@ def self.name it 'undefines method on scope' do expect { subject } - .to change { scope.instance_methods.include?(:foo) } + .to change { scope.raw.instance_methods.include?(:foo) } .from(true) .to(false) end @@ -69,7 +72,7 @@ def self.name it 'sets method visibility' do expect { subject } - .to change { scope.private_instance_methods.include?(:foo) } + .to change { scope.raw.private_instance_methods.include?(:foo) } .from(false) .to(true) end @@ -99,16 +102,19 @@ def self.name shared_context 'memoizable scope setup' do let(:scope) do - Class.new do - include Unparser::Adamantium + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: Class.new do + include Unparser::Adamantium - def self.name - 'MemoizableClass' - end + def self.name + 'MemoizableClass' + end - def foo; end - memoize :foo - end + def foo; end + memoize :foo + end + ) end end @@ -118,11 +124,11 @@ def foo; end subject { object.prepare } it 'undefines memoizer' do - expect { subject }.to change { scope.memoized?(:foo) }.from(true).to(false) + expect { subject }.to change { scope.raw.memoized?(:foo) }.from(true).to(false) end it 'undefines method on scope' do - expect { subject }.to change { scope.instance_methods.include?(:foo) }.from(true).to(false) + expect { subject }.to change { scope.raw.instance_methods.include?(:foo) }.from(true).to(false) end it_should_behave_like 'a command method' diff --git a/spec/unit/mutant/subject/method/metaclass_spec.rb b/spec/unit/mutant/subject/method/metaclass_spec.rb index c3b76eec9..4d47a2c9e 100644 --- a/spec/unit/mutant/subject/method/metaclass_spec.rb +++ b/spec/unit/mutant/subject/method/metaclass_spec.rb @@ -17,15 +17,18 @@ end let(:scope) do - Class.new do - class << self - def foo; end - - def name - 'Test' - end - end - end + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: Class.new do + class << self + def foo; end + + def name + 'Test' + end + end + end + ) end describe '#expression' do @@ -49,7 +52,7 @@ def name subject { object.prepare } it 'undefines method on scope' do - expect { subject }.to change { scope.public_methods.include?(:foo) }.from(true).to(false) + expect { subject }.to change { scope.raw.public_methods.include?(:foo) }.from(true).to(false) end it_should_behave_like 'a command method' diff --git a/spec/unit/mutant/subject/method/singleton_spec.rb b/spec/unit/mutant/subject/method/singleton_spec.rb index 3424f1be8..8cad954ec 100644 --- a/spec/unit/mutant/subject/method/singleton_spec.rb +++ b/spec/unit/mutant/subject/method/singleton_spec.rb @@ -17,13 +17,16 @@ end let(:scope) do - Class.new do - def self.foo; end - - def self.name - 'Test' + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: Class.new do + def self.foo; end + + def self.name + 'Test' + end end - end + ) end describe '#expression' do @@ -47,7 +50,7 @@ def self.name subject { object.prepare } it 'undefines method on scope' do - expect { subject }.to change { scope.public_methods.include?(:foo) }.from(true).to(false) + expect { subject }.to change { scope.raw.public_methods.include?(:foo) }.from(true).to(false) end it_should_behave_like 'a command method' @@ -58,7 +61,7 @@ def self.name it 'sets method visibility' do expect { subject } - .to change { scope.private_methods.include?(:foo) } + .to change { scope.raw.private_methods.include?(:foo) } .from(false) .to(true) end From 70cd94526e0426c507f7f9bec3ca72ebbbf7774f Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Fri, 8 Mar 2024 23:19:47 +0000 Subject: [PATCH 2/3] Change to methods on scope that do not depend on context --- lib/mutant/context.rb | 60 ++++++-------------------- lib/mutant/matcher/method/metaclass.rb | 2 +- lib/mutant/matcher/method/singleton.rb | 2 +- lib/mutant/scope.rb | 42 +++++++++++++++++- spec/unit/mutant/context_spec.rb | 27 ------------ spec/unit/mutant/scope_spec.rb | 34 +++++++++++++++ 6 files changed, 89 insertions(+), 78 deletions(-) create mode 100644 spec/unit/mutant/scope_spec.rb diff --git a/lib/mutant/context.rb b/lib/mutant/context.rb index 1828d1f3c..b10e1cad2 100644 --- a/lib/mutant/context.rb +++ b/lib/mutant/context.rb @@ -6,15 +6,8 @@ class Context include Adamantium, Anima.new(:scope, :source_path) extend AST::Sexp - NAMESPACE_DELIMITER = '::' - - # Return root node for mutation - # - # @return [Parser::AST::Node] - def root(node) - nesting.reverse.reduce(node) do |current, raw_scope| - self.class.wrap(raw_scope, current) - end + def match_expressions + scope.match_expressions end # Identification string @@ -24,9 +17,18 @@ def identification scope.raw.name 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 + end + # Wrap node into ast node def self.wrap(raw_scope, node) - name = s(:const, nil, raw_scope.name.split(NAMESPACE_DELIMITER).last.to_sym) + name = s(:const, nil, raw_scope.name.split(Scope::NAMESPACE_DELIMITER).last.to_sym) case raw_scope when Class s(:class, name, nil, node) @@ -34,43 +36,5 @@ def self.wrap(raw_scope, node) s(:module, name, node) end end - - # Nesting of scope - # - # @return [Enumerable] - def nesting - const = Object - name_nesting.map do |name| - const = const.const_get(name) - end - end - memoize :nesting - - # Unqualified name of scope - # - # @return [String] - def unqualified_name - name_nesting.last - end - - # Match expressions for scope - # - # @return [Enumerable] - 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 - end - memoize :match_expressions - - private - - def name_nesting - scope.raw.name.split(NAMESPACE_DELIMITER) - end - memoize :name_nesting - end # Context end # Mutant diff --git a/lib/mutant/matcher/method/metaclass.rb b/lib/mutant/matcher/method/metaclass.rb index e5256e01b..b49549776 100644 --- a/lib/mutant/matcher/method/metaclass.rb +++ b/lib/mutant/matcher/method/metaclass.rb @@ -62,7 +62,7 @@ def metaclass_target?(node) def sclass_const_name?(node) name = node.children.fetch(CONST_NAME_INDEX) - name.to_s.eql?(context.unqualified_name) + name.to_s.eql?(scope.unqualified_name) end end # Evaluator diff --git a/lib/mutant/matcher/method/singleton.rb b/lib/mutant/matcher/method/singleton.rb index 31c1c31a7..821e5afc5 100644 --- a/lib/mutant/matcher/method/singleton.rb +++ b/lib/mutant/matcher/method/singleton.rb @@ -46,7 +46,7 @@ def receiver?(node) def receiver_name?(node) name = node.children.fetch(NAME_INDEX) - name.to_s.eql?(context.unqualified_name) + name.to_s.eql?(scope.unqualified_name) end end # Evaluator diff --git a/lib/mutant/scope.rb b/lib/mutant/scope.rb index d943cb15b..bda39b583 100644 --- a/lib/mutant/scope.rb +++ b/lib/mutant/scope.rb @@ -3,6 +3,46 @@ module Mutant # Class or Module bound to an exact expression class Scope - include Anima.new(:raw, :expression) + include Adamantium, Anima.new(:raw, :expression) + + NAMESPACE_DELIMITER = '::' + + # Nesting of scope + # + # @return [Enumerable] + def nesting + const = Object + name_nesting.map do |name| + const = const.const_get(name) + end + end + memoize :nesting + + # Unqualified name of scope + # + # @return [String] + def unqualified_name + name_nesting.last + end + + # Match expressions for scope + # + # @return [Enumerable] + 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 + end + memoize :match_expressions + + private + + def name_nesting + raw.name.split(NAMESPACE_DELIMITER) + end + memoize :name_nesting + end # Scope end # Mutant diff --git a/spec/unit/mutant/context_spec.rb b/spec/unit/mutant/context_spec.rb index d9a05552f..b93365fd6 100644 --- a/spec/unit/mutant/context_spec.rb +++ b/spec/unit/mutant/context_spec.rb @@ -72,33 +72,6 @@ class Literal end end - describe '#unqualified_name' do - subject { object.unqualified_name } - - context 'with top level constant name' do - let(:scope) do - Mutant::Scope.new( - expression: instance_double(Mutant::Expression), - raw: TestApp - ) - end - - it 'should return the unqualified name' do - should eql('TestApp') - end - - it_should_behave_like 'an idempotent method' - end - - context 'with scoped constant name' do - it 'should return the unqualified name' do - should eql('Literal') - end - - it_should_behave_like 'an idempotent method' - end - end - describe '#match_expressions' do subject { object.match_expressions } diff --git a/spec/unit/mutant/scope_spec.rb b/spec/unit/mutant/scope_spec.rb new file mode 100644 index 000000000..c8bd4eabd --- /dev/null +++ b/spec/unit/mutant/scope_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe Mutant::Scope do + describe '#unqualified_name' do + subject { object.unqualified_name } + + let(:object) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: raw_scope + ) + end + + context 'with top level constant name' do + let(:raw_scope) { TestApp } + + it 'should return the unqualified name' do + should eql('TestApp') + end + + it_should_behave_like 'an idempotent method' + end + + context 'with scoped constant name' do + let(:raw_scope) { TestApp::Literal } + + it 'should return the unqualified name' do + should eql('Literal') + end + + it_should_behave_like 'an idempotent method' + end + end +end From 4250ea88a5abe13d3a011f6703fb3ef021f30d63 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Fri, 8 Mar 2024 04:01:20 +0000 Subject: [PATCH 3/3] Fix constant scope of inserted mutations [Fix #1422] --- Changelog.md | 6 +- lib/mutant/ast.rb | 17 ++- lib/mutant/context.rb | 57 +++++---- lib/mutant/matcher/method.rb | 30 ++++- lib/mutant/meta/example.rb | 5 +- spec/support/shared_context.rb | 5 +- spec/unit/mutant/ast_spec.rb | 14 ++- spec/unit/mutant/cli_spec.rb | 6 +- spec/unit/mutant/context_spec.rb | 119 +++++++++++------- spec/unit/mutant/matcher/descendants_spec.rb | 21 +++- .../mutant/matcher/method/instance_spec.rb | 18 ++- spec/unit/mutant/meta/example_spec.rb | 6 +- .../mutant/subject/method/instance_spec.rb | 24 +++- .../mutant/subject/method/metaclass_spec.rb | 10 +- .../mutant/subject/method/singleton_spec.rb | 6 +- 15 files changed, 247 insertions(+), 97 deletions(-) diff --git a/Changelog.md b/Changelog.md index 688f0e375..18cb792c3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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. diff --git a/lib/mutant/ast.rb b/lib/mutant/ast.rb index 9bccee62f..0c3ac7859 100644 --- a/lib/mutant/ast.rb +++ b/lib/mutant/ast.rb @@ -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 @@ -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 diff --git a/lib/mutant/context.rb b/lib/mutant/context.rb index b10e1cad2..2f1759db2 100644 --- a/lib/mutant/context.rb +++ b/lib/mutant/context.rb @@ -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 diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb index 4b9f07dfe..7fc811d18 100644 --- a/lib/mutant/matcher/method.rb +++ b/lib/mutant/matcher/method.rb @@ -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 @@ -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, @@ -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 @@ -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 diff --git a/lib/mutant/meta/example.rb b/lib/mutant/meta/example.rb index eaa547b36..836957780 100644 --- a/lib/mutant/meta/example.rb +++ b/lib/mutant/meta/example.rb @@ -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 diff --git a/spec/support/shared_context.rb b/spec/support/shared_context.rb index 324ddb22d..f1f9a0a55 100644 --- a/spec/support/shared_context.rb +++ b/spec/support/shared_context.rb @@ -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 diff --git a/spec/unit/mutant/ast_spec.rb b/spec/unit/mutant/ast_spec.rb index b0f36664a..c6f116962 100644 --- a/spec/unit/mutant/ast_spec.rb +++ b/spec/unit/mutant/ast_spec.rb @@ -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 @@ -27,7 +29,7 @@ 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 @@ -35,10 +37,10 @@ def apply(line) 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 diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index 2f2a1845f..90cec8900 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -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 ) diff --git a/spec/unit/mutant/context_spec.rb b/spec/unit/mutant/context_spec.rb index b93365fd6..9df6f3271 100644 --- a/spec/unit/mutant/context_spec.rb +++ b/spec/unit/mutant/context_spec.rb @@ -1,39 +1,15 @@ # frozen_string_literal: true RSpec.describe Mutant::Context do - describe '.wrap' do - subject { described_class.wrap(raw_scope, node) } - - let(:node) { s(:str, 'test') } - - context 'with Module as scope' do - let(:raw_scope) { Mutant } - - let(:expected) do - s(:module, - s(:const, nil, :Mutant), - s(:str, 'test')) - end - - it { should eql(expected) } - end - - context 'with Class as scope' do - let(:raw_scope) { Mutant::Context } - - let(:expected) do - s(:class, - s(:const, nil, :Context), - nil, - s(:str, 'test')) - end - - it { should eql(expected) } - end + let(:object) do + described_class.new( + constant_scope: constant_scope, + scope: scope, + source_path: source_path + ) end - let(:object) { described_class.new(scope: scope, source_path: source_path) } - let(:source_path) { instance_double(Pathname) } + let(:source_path) { instance_double(Pathname) } let(:scope) do Mutant::Scope.new( @@ -42,6 +18,16 @@ ) end + let(:constant_scope) do + described_class::ConstantScope::Module.new( + const: s(:const, nil, :TestApp), + descendant: described_class::ConstantScope::Class.new( + const: s(:const, nil, :Literal), + descendant: described_class::ConstantScope::None.new + ) + ) + end + describe '#identification' do subject { object.identification } @@ -51,24 +37,73 @@ describe '#root' do subject { object.root(node) } + let(:generated_source) do + Unparser.unparse(subject) + end + let(:node) { s(:sym, :node) } - let(:expected_source) do - generate(parse(<<-RUBY)) - module TestApp - class Literal - :node + context 'nested in module' do + let(:expected_source) do + generate(parse(<<-RUBY)) + module TestApp + class Literal + :node + end end - end - RUBY + RUBY + end + + it 'should create correct source' do + expect(generated_source).to eql(expected_source) + end end - let(:generated_source) do - Unparser.unparse(subject) + context 'nested in class' do + let(:constant_scope) do + described_class::ConstantScope::Class.new( + const: s(:const, nil, :TestApp), + descendant: described_class::ConstantScope::Module.new( + const: s(:const, nil, :Literal), + descendant: described_class::ConstantScope::None.new + ) + ) + end + + let(:expected_source) do + generate(parse(<<-RUBY)) + class TestApp + module Literal + :node + end + end + RUBY + end + + it 'should create correct source' do + expect(generated_source).to eql(expected_source) + end end - it 'should create correct source' do - expect(generated_source).to eql(expected_source) + context 'flat' do + let(:constant_scope) do + described_class::ConstantScope::Class.new( + const: s(:const, s(:const, nil, :TestApp), :Literal), + descendant: described_class::ConstantScope::None.new + ) + end + + let(:expected_source) do + generate(parse(<<~RUBY)) + class TestApp::Literal + :node + end + RUBY + end + + it 'should create correct source' do + expect(generated_source).to eql(expected_source) + end end end diff --git a/spec/unit/mutant/matcher/descendants_spec.rb b/spec/unit/mutant/matcher/descendants_spec.rb index ba1c6d8eb..184b8a19c 100644 --- a/spec/unit/mutant/matcher/descendants_spec.rb +++ b/spec/unit/mutant/matcher/descendants_spec.rb @@ -10,16 +10,33 @@ def apply subject.call(env) end + let(:constant_scope) do + Mutant::Context::ConstantScope::Module.new( + const: s(:const, nil, :TestApp), + descendant: Mutant::Context::ConstantScope::Class.new( + const: s(:const, nil, :Foo), + descendant: Mutant::Context::ConstantScope::Class.new( + const: s(:const, nil, :Bar), + descendant: Mutant::Context::ConstantScope::Class.new( + const: s(:const, nil, :Baz), + descendant: Mutant::Context::ConstantScope::None.new + ) + ) + ) + ) + end + let(:expected_subjects) do [ Mutant::Subject::Method::Instance.new( config: Mutant::Subject::Config::DEFAULT, context: Mutant::Context.new( - scope: Mutant::Scope.new( + constant_scope: constant_scope, + scope: Mutant::Scope.new( raw: TestApp::Foo::Bar::Baz, expression: parse_expression('TestApp::Foo::Bar::Baz') ), - source_path: TestApp::ROOT.join('lib/test_app.rb') + source_path: TestApp::ROOT.join('lib/test_app.rb') ), node: s(:def, :foo, s(:args), nil), visibility: :public diff --git a/spec/unit/mutant/matcher/method/instance_spec.rb b/spec/unit/mutant/matcher/method/instance_spec.rb index dc985d073..83ddc390a 100644 --- a/spec/unit/mutant/matcher/method/instance_spec.rb +++ b/spec/unit/mutant/matcher/method/instance_spec.rb @@ -217,10 +217,24 @@ def arguments it_should_behave_like 'a method matcher' + let(:constant_scope) do + Mutant::Context::ConstantScope::Module.new( + const: s(:const, nil, :TestApp), + descendant: Mutant::Context::ConstantScope::Module.new( + const: s(:const, nil, :InstanceMethodTests), + descendant: Mutant::Context::ConstantScope::Module.new( + const: s(:const, nil, :WithMemoizer), + descendant: Mutant::Context::ConstantScope::None.new + ) + ) + ) + end + let(:context) do Mutant::Context.new( - scope: scope, - source_path: MutantSpec::ROOT.join('test_app', 'lib', 'test_app.rb') + constant_scope: constant_scope, + scope: scope, + source_path: MutantSpec::ROOT.join('test_app', 'lib', 'test_app.rb') ) end diff --git a/spec/unit/mutant/meta/example_spec.rb b/spec/unit/mutant/meta/example_spec.rb index a1eca5514..388b50b21 100644 --- a/spec/unit/mutant/meta/example_spec.rb +++ b/spec/unit/mutant/meta/example_spec.rb @@ -50,6 +50,10 @@ it { should eql(Mutant::Meta::Example::Verification.new(example: object, mutations: mutations)) } end + let(:constant_scope) do + Mutant::Context::ConstantScope::None.new + end + describe '#context' do subject { object.context } @@ -60,7 +64,7 @@ ) end - it { should eql(Mutant::Context.new(scope: scope, source_path: location.path)) } + it { should eql(Mutant::Context.new(constant_scope: constant_scope, scope: scope, source_path: location.path)) } end describe '#identification' do diff --git a/spec/unit/mutant/subject/method/instance_spec.rb b/spec/unit/mutant/subject/method/instance_spec.rb index 025e901d1..a6576d4ff 100644 --- a/spec/unit/mutant/subject/method/instance_spec.rb +++ b/spec/unit/mutant/subject/method/instance_spec.rb @@ -12,10 +12,15 @@ let(:node) { Unparser.parse('def foo; end') } + let(:constant_scope) do + Mutant::Context::ConstantScope::None.new + end + let(:context) do Mutant::Context.new( - scope: scope, - source_path: instance_double(Pathname) + constant_scope: constant_scope, + scope: scope, + source_path: instance_double(Pathname) ) end @@ -97,8 +102,19 @@ def self.name ) end - let(:context) { Mutant::Context.new(scope: scope, source_path: double('Source Path')) } - let(:node) { Unparser.parse('def foo; end') } + let(:constant_scope) do + Mutant::Context::ConstantScope::None.new + end + + let(:context) do + Mutant::Context.new( + constant_scope: constant_scope, + scope: scope, + source_path: instance_double(Pathname) + ) + end + + let(:node) { Unparser.parse('def foo; end') } shared_context 'memoizable scope setup' do let(:scope) do diff --git a/spec/unit/mutant/subject/method/metaclass_spec.rb b/spec/unit/mutant/subject/method/metaclass_spec.rb index 4d47a2c9e..56a9ab72d 100644 --- a/spec/unit/mutant/subject/method/metaclass_spec.rb +++ b/spec/unit/mutant/subject/method/metaclass_spec.rb @@ -12,8 +12,16 @@ let(:node) { s(:def, :foo, s(:args)) } + let(:constant_scope) do + Mutant::Context::ConstantScope::None.new + end + let(:context) do - Mutant::Context.new(scope: scope, source_path: instance_double(Pathname)) + Mutant::Context.new( + constant_scope: constant_scope, + scope: scope, + source_path: instance_double(Pathname) + ) end let(:scope) do diff --git a/spec/unit/mutant/subject/method/singleton_spec.rb b/spec/unit/mutant/subject/method/singleton_spec.rb index 8cad954ec..b879bac2c 100644 --- a/spec/unit/mutant/subject/method/singleton_spec.rb +++ b/spec/unit/mutant/subject/method/singleton_spec.rb @@ -12,8 +12,12 @@ let(:node) { s(:defs, s(:self), :foo, s(:args)) } + let(:constant_scope) do + Mutant::Context::ConstantScope::None.new + end + let(:context) do - Mutant::Context.new(scope: scope, source_path: instance_double(Pathname)) + Mutant::Context.new(constant_scope: constant_scope, scope: scope, source_path: instance_double(Pathname)) end let(:scope) do