From d52ff5607906d33f7d3651a75b4f86d66c1fbef7 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Mon, 25 Apr 2022 02:27:50 +0000 Subject: [PATCH] Add per subject inline disable configuration * This allows to place `mutant:disable` comments next to subjects to mark them as disabled. * Inheritance is not supported currently, so a comment on a class will not make this entire class disabled. --- Changelog.md | 16 +++ Gemfile.lock | 2 +- docs/configuration.md | 28 +++++ lib/mutant.rb | 1 + lib/mutant/matcher.rb | 2 +- lib/mutant/matcher/method.rb | 1 + lib/mutant/subject.rb | 6 +- lib/mutant/subject/config.rb | 25 ++++ lib/mutant/version.rb | 2 +- spec/unit/mutant/cli_spec.rb | 6 +- spec/unit/mutant/matcher/descendants_spec.rb | 1 + .../mutant/matcher/method/instance_spec.rb | 13 +++ .../mutant/matcher/method/singleton_spec.rb | 12 ++ spec/unit/mutant/matcher_spec.rb | 26 ++++- spec/unit/mutant/selector/expression_spec.rb | 1 + spec/unit/mutant/subject/config_spec.rb | 108 ++++++++++++++++++ .../mutant/subject/method/instance_spec.rb | 2 + .../mutant/subject/method/metaclass_spec.rb | 1 + .../mutant/subject/method/singleton_spec.rb | 1 + spec/unit/mutant/subject_spec.rb | 17 +++ test_app/lib/test_app.rb | 10 ++ 21 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 lib/mutant/subject/config.rb create mode 100644 spec/unit/mutant/subject/config_spec.rb diff --git a/Changelog.md b/Changelog.md index cca3b9cf0..208a4f9e6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,19 @@ +# v0.11.8 2022-04-25 + +* [#1320](https://github.com/mbj/mutant/pull/1320) + + Add inline mutant disable configuration. This allows individual subjects to be marked as + disbled directly in the code. + + Use: + +` ``` + class Something + # mutant:disable + def some_method + end + end + # v0.11.7 2022-04-24 * [#1319](https://github.com/mbj/mutant/pull/1319) diff --git a/Gemfile.lock b/Gemfile.lock index 170f883e6..fbc672886 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - mutant (0.11.7) + mutant (0.11.8) diff-lcs (~> 1.3) parser (~> 3.1.0) regexp_parser (~> 2.3, >= 2.3.1) diff --git a/docs/configuration.md b/docs/configuration.md index 233f33f1a..86ed7eb30 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,5 +1,30 @@ # Configuration +There are 3 ways of configuring mutant: + +1. In the source code via processing comments. +2. Via the CLI +3. Via a config file. + +### Processing Comments + +Mutant currently only supports the `mutant:disable` directive that can be added in a +source code comment to ignore a specific subject. + +Example: + +```ruby +class SomeClass + # mutant:disable + def some_method + end +end +``` + +More inline configuration will be made available over time. + +### Configuration File + Mutant can be configured with a config file that can be named one of the following: `.mutant.yml`, `config/mutant.yml`, or `mutant.yml` The following options can be configured through the config file: @@ -92,6 +117,9 @@ matcher: # # Note that subject ignores from the command line are added to the subject ignores # configured on the command line! + # + # Also matcher ignores generally shold be used for entire namespaces, and individual + # methods be disabled directly in source code via `mutant:disable` directives. ignore: - Your::App::Namespace::Dirty # ignore all subjects on a specific constant - Your::App::Namespace::Dirty* # ignore all subjects on a specific constant, recursively diff --git a/lib/mutant.rb b/lib/mutant.rb index 110ccdd71..5f9adb819 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -164,6 +164,7 @@ module Mutant require 'mutant/context' require 'mutant/scope' require 'mutant/subject' +require 'mutant/subject/config' require 'mutant/subject/method' require 'mutant/subject/method/instance' require 'mutant/subject/method/singleton' diff --git a/lib/mutant/matcher.rb b/lib/mutant/matcher.rb index faeb70e09..984ea2087 100644 --- a/lib/mutant/matcher.rb +++ b/lib/mutant/matcher.rb @@ -26,7 +26,7 @@ def self.from_config(config) end def self.allowed_subject?(config, subject) - select_subject?(config, subject) && !ignore_subject?(config, subject) + select_subject?(config, subject) && !ignore_subject?(config, subject) && !subject.inline_disabled? end private_class_method :allowed_subject? diff --git a/lib/mutant/matcher/method.rb b/lib/mutant/matcher/method.rb index 15a2ae80f..1982dfcd7 100644 --- a/lib/mutant/matcher/method.rb +++ b/lib/mutant/matcher/method.rb @@ -99,6 +99,7 @@ def subject node = matched_node_path.last || return self.class::SUBJECT_CLASS.new( + config: Subject::Config.parse(ast.comment_associations.fetch(node, [])), context: context, node: node, visibility: visibility diff --git a/lib/mutant/subject.rb b/lib/mutant/subject.rb index 8bbe79cb4..f651e850d 100644 --- a/lib/mutant/subject.rb +++ b/lib/mutant/subject.rb @@ -4,7 +4,7 @@ module Mutant # Subject of a mutation class Subject include AbstractType, Adamantium, Enumerable - include Anima.new(:context, :node) + include Anima.new(:config, :context, :node) # Mutations for this subject # @@ -19,6 +19,10 @@ def mutations end memoize :mutations + def inline_disabled? + config.inline_disable + end + # Source path # # @return [Pathname] diff --git a/lib/mutant/subject/config.rb b/lib/mutant/subject/config.rb new file mode 100644 index 000000000..005268816 --- /dev/null +++ b/lib/mutant/subject/config.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutant + class Subject + class Config + include Adamantium, Anima.new(:inline_disable) + + DEFAULT = new(inline_disable: false) + + DISABLE_REGEXP = /(\s|^)mutant:disable(?:\s|$)/.freeze + SYNTAX_REGEXP = /\A(?:#|=begin\n)/.freeze + + def self.parse(comments) + new( + inline_disable: comments.any? { |comment| DISABLE_REGEXP.match?(comment_body(comment)) } + ) + end + + def self.comment_body(comment) + comment.text.sub(SYNTAX_REGEXP, '') + end + private_class_method :comment_body + end # Config + end # Subject +end # Mutant diff --git a/lib/mutant/version.rb b/lib/mutant/version.rb index e7f11ce5c..c2b8b8ba4 100644 --- a/lib/mutant/version.rb +++ b/lib/mutant/version.rb @@ -2,5 +2,5 @@ module Mutant # Current mutant version - VERSION = '0.11.7' + VERSION = '0.11.8' end # Mutant diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index 6e11e1933..27e8117c5 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -496,10 +496,8 @@ def self.main_body let(:subject_a) do Mutant::Subject::Method::Instance.new( - context: Mutant::Context.new( - Object, - 'subject.rb' - ), + config: Mutant::Subject::Config::DEFAULT, + context: Mutant::Context.new(Object, 'subject.rb'), node: s(:def, :send, s(:args), nil), visibility: :public ) diff --git a/spec/unit/mutant/matcher/descendants_spec.rb b/spec/unit/mutant/matcher/descendants_spec.rb index 4ac5447b3..35cb6e1a6 100644 --- a/spec/unit/mutant/matcher/descendants_spec.rb +++ b/spec/unit/mutant/matcher/descendants_spec.rb @@ -13,6 +13,7 @@ def apply let(:expected_subjects) do [ Mutant::Subject::Method::Instance.new( + config: Mutant::Subject::Config::DEFAULT, context: Mutant::Context.new( TestApp::Foo::Bar::Baz, TestApp::ROOT.join('lib/test_app.rb') diff --git a/spec/unit/mutant/matcher/method/instance_spec.rb b/spec/unit/mutant/matcher/method/instance_spec.rb index 86157cf1c..4d7de2f09 100644 --- a/spec/unit/mutant/matcher/method/instance_spec.rb +++ b/spec/unit/mutant/matcher/method/instance_spec.rb @@ -145,6 +145,7 @@ def arguments let(:expected_subjects) do [ Mutant::Subject::Method::Instance.new( + config: Mutant::Subject::Config::DEFAULT, context: context, node: s(:def, :bar, s(:args), nil), visibility: expected_visibility @@ -213,4 +214,16 @@ def arguments it_should_behave_like 'a method matcher' end + + context 'on inline disabled method' do + let(:scope) { TestApp::InlineDisabled } + let(:method_line) { 148 } + let(:method_arity) { 0 } + + it_should_behave_like 'a method matcher' do + it 'returns disabled inline config' do + expect(mutation_subject.config.inline_disable).to be(true) + end + end + end end diff --git a/spec/unit/mutant/matcher/method/singleton_spec.rb b/spec/unit/mutant/matcher/method/singleton_spec.rb index 869804c7d..641929a2d 100644 --- a/spec/unit/mutant/matcher/method/singleton_spec.rb +++ b/spec/unit/mutant/matcher/method/singleton_spec.rb @@ -107,4 +107,16 @@ def arguments 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 } + + it_should_behave_like 'a method matcher' do + it 'returns disabled inline config' do + expect(mutation_subject.config.inline_disable).to be(true) + end + end + end end diff --git a/spec/unit/mutant/matcher_spec.rb b/spec/unit/mutant/matcher_spec.rb index 93f13616c..c1c2e8268 100644 --- a/spec/unit/mutant/matcher_spec.rb +++ b/spec/unit/mutant/matcher_spec.rb @@ -45,23 +45,43 @@ def apply end let(:subject_a) do - instance_double(Mutant::Subject, 'subject a', expression: expression('Foo::Bar#a')) + instance_double( + Mutant::Subject, + 'subject a', + expression: expression('Foo::Bar#a'), + inline_disabled?: false + ) end let(:subject_b) do - instance_double(Mutant::Subject, 'subject b', expression: expression('Foo::Bar#b')) + instance_double( + Mutant::Subject, + 'subject b', + expression: expression('Foo::Bar#b'), + inline_disabled?: false + ) end def expression(input, matcher = anon_matcher) expression_class.new(parse_expression(input), matcher) end - context 'empty ignores and empty filter' do + context 'no restrictions of any kinds' do it 'returns expected subjects' do expect(apply.call(env)).to eql([subject_a, subject_b]) end end + context 'with explicit disable' do + before do + allow(subject_b).to receive_messages(inline_disabled?: true) + end + + it 'returns expected subjects' do + expect(apply.call(env)).to eql([subject_a]) + end + end + context 'with ignore matching a subject' do let(:ignore_expressions) { [subject_b.expression] } diff --git a/spec/unit/mutant/selector/expression_spec.rb b/spec/unit/mutant/selector/expression_spec.rb index de3ad9c90..cdab6ce6b 100644 --- a/spec/unit/mutant/selector/expression_spec.rb +++ b/spec/unit/mutant/selector/expression_spec.rb @@ -19,6 +19,7 @@ let(:mutation_subject) do subject_class.new( + config: Mutant::Subject::Config::DEFAULT, context: context, node: node ) diff --git a/spec/unit/mutant/subject/config_spec.rb b/spec/unit/mutant/subject/config_spec.rb new file mode 100644 index 000000000..b2026438c --- /dev/null +++ b/spec/unit/mutant/subject/config_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +RSpec.describe Mutant::Subject::Config do + describe '.parse' do + def apply + described_class.parse(comments) + end + + let(:comments) do + node, comments = Unparser.parse_with_comments(source) + + ::Parser::Source::Comment.associate_by_identity(node, comments).fetch(node, []) + end + + shared_examples 'returns default config' do + it 'returns default config' do + expect(apply).to eql(described_class::DEFAULT) + end + end + + shared_examples 'returns disabled config' do + it 'returns default config' do + expect(apply).to eql(described_class.new(inline_disable: true)) + end + end + + context 'on empty comments' do + let(:source) do + <<~'RUBY' + def foo + end + RUBY + end + + include_examples 'returns default config' + end + + context 'on comment not mentioning a mutant disable' do + context 'in a line comment' do + let(:source) do + <<~'RUBY' + # rubocop:disable Metrics/Something + def foo + end + RUBY + end + + include_examples 'returns default config' + end + + context 'in a block comment' do + let(:source) do + <<~'RUBY' + =begin + rubocop:disable Metrics/Something + =end + def foo + end + RUBY + end + + include_examples 'returns default config' + end + end + + context 'on comment mentioning a mutant disable' do + context 'in a block comment' do + let(:source) do + <<~'RUBY' + =begin + mutant:disable + =end + def foo + end + RUBY + end + + include_examples 'returns disabled config' + end + + context 'in a line comment' do + context 'with space' do + let(:source) do + <<~'RUBY' + # mutant:disable + def foo + end + RUBY + end + + include_examples 'returns disabled config' + end + + context 'without space' do + let(:source) do + <<~'RUBY' + #mutant:disable + def foo + end + RUBY + end + + include_examples 'returns disabled config' + end + end + end + end +end diff --git a/spec/unit/mutant/subject/method/instance_spec.rb b/spec/unit/mutant/subject/method/instance_spec.rb index aa73710e9..289819031 100644 --- a/spec/unit/mutant/subject/method/instance_spec.rb +++ b/spec/unit/mutant/subject/method/instance_spec.rb @@ -3,6 +3,7 @@ RSpec.describe Mutant::Subject::Method::Instance do let(:object) do described_class.new( + config: Mutant::Subject::Config::DEFAULT, context: context, node: node, visibility: :private @@ -86,6 +87,7 @@ def self.name RSpec.describe Mutant::Subject::Method::Instance::Memoized do let(:object) do described_class.new( + config: Mutant::Subject::Config::DEFAULT, context: context, node: node, visibility: :public diff --git a/spec/unit/mutant/subject/method/metaclass_spec.rb b/spec/unit/mutant/subject/method/metaclass_spec.rb index d85be04ca..dd4bb3aad 100644 --- a/spec/unit/mutant/subject/method/metaclass_spec.rb +++ b/spec/unit/mutant/subject/method/metaclass_spec.rb @@ -3,6 +3,7 @@ RSpec.describe Mutant::Subject::Method::Metaclass do let(:object) do described_class.new( + config: Mutant::Subject::Config::DEFAULT, context: context, node: node, visibility: :public diff --git a/spec/unit/mutant/subject/method/singleton_spec.rb b/spec/unit/mutant/subject/method/singleton_spec.rb index 30fab4e20..42e4c05ad 100644 --- a/spec/unit/mutant/subject/method/singleton_spec.rb +++ b/spec/unit/mutant/subject/method/singleton_spec.rb @@ -3,6 +3,7 @@ RSpec.describe Mutant::Subject::Method::Singleton do let(:object) do described_class.new( + config: Mutant::Subject::Config::DEFAULT, context: context, node: node, visibility: :private diff --git a/spec/unit/mutant/subject_spec.rb b/spec/unit/mutant/subject_spec.rb index d46e1dca8..12c7b4405 100644 --- a/spec/unit/mutant/subject_spec.rb +++ b/spec/unit/mutant/subject_spec.rb @@ -18,6 +18,7 @@ def match_expressions let(:object) do class_under_test.new( + config: Mutant::Subject::Config::DEFAULT, context: context, node: node ) @@ -102,4 +103,20 @@ def foo ]) end end + + describe '#inline_disabled?' do + subject { object.inline_disabled? } + + context 'on default config' do + it { should be(false) } + end + + context 'when config has an inline disable' do + let(:object) do + super().with(config: super().config.with(inline_disable: true)) + end + + it { should be(true) } + end + end end diff --git a/test_app/lib/test_app.rb b/test_app/lib/test_app.rb index e7f6961e3..424444516 100644 --- a/test_app/lib/test_app.rb +++ b/test_app/lib/test_app.rb @@ -143,6 +143,16 @@ def foo end end + class InlineDisabled + # mutant:disable + def foo + end + + # mutant:disable + def self.foo + end + end + ROOT = Pathname.new(__dir__).parent end