mutant 0.5.23 → 0.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +10 -0
  3. data/config/flay.yml +1 -1
  4. data/config/reek.yml +19 -19
  5. data/lib/mutant.rb +12 -39
  6. data/lib/mutant/ast.rb +5 -0
  7. data/lib/mutant/ast/meta.rb +131 -0
  8. data/lib/mutant/ast/named_children.rb +98 -0
  9. data/lib/mutant/ast/node_predicates.rb +19 -0
  10. data/lib/mutant/ast/nodes.rb +21 -0
  11. data/lib/mutant/ast/sexp.rb +34 -0
  12. data/lib/mutant/ast/types.rb +48 -0
  13. data/lib/mutant/cache.rb +3 -2
  14. data/lib/mutant/cli.rb +11 -151
  15. data/lib/mutant/config.rb +22 -2
  16. data/lib/mutant/context/scope.rb +11 -21
  17. data/lib/mutant/delegator.rb +2 -0
  18. data/lib/mutant/diff.rb +7 -3
  19. data/lib/mutant/env.rb +49 -0
  20. data/lib/mutant/expression.rb +36 -8
  21. data/lib/mutant/expression/methods.rb +62 -0
  22. data/lib/mutant/expression/namespace.rb +41 -28
  23. data/lib/mutant/{strategy.rb → integration.rb} +12 -31
  24. data/lib/mutant/isolation.rb +1 -1
  25. data/lib/mutant/matcher.rb +1 -21
  26. data/lib/mutant/matcher/builder.rb +142 -0
  27. data/lib/mutant/matcher/method.rb +3 -7
  28. data/lib/mutant/matcher/method/instance.rb +6 -5
  29. data/lib/mutant/matcher/method/singleton.rb +2 -7
  30. data/lib/mutant/matcher/methods.rb +11 -14
  31. data/lib/mutant/matcher/namespace.rb +31 -39
  32. data/lib/mutant/matcher/scope.rb +13 -2
  33. data/lib/mutant/meta.rb +0 -1
  34. data/lib/mutant/meta/example/dsl.rb +5 -1
  35. data/lib/mutant/mutator/node.rb +16 -44
  36. data/lib/mutant/mutator/node/or_asgn.rb +1 -1
  37. data/lib/mutant/mutator/node/send.rb +5 -60
  38. data/lib/mutant/mutator/node/super.rb +2 -5
  39. data/lib/mutant/mutator/registry.rb +1 -1
  40. data/lib/mutant/reporter.rb +10 -0
  41. data/lib/mutant/reporter/cli.rb +13 -0
  42. data/lib/mutant/reporter/cli/printer.rb +2 -0
  43. data/lib/mutant/reporter/cli/progress/config.rb +1 -1
  44. data/lib/mutant/reporter/cli/progress/noop.rb +2 -0
  45. data/lib/mutant/reporter/cli/registry.rb +2 -0
  46. data/lib/mutant/reporter/null.rb +12 -0
  47. data/lib/mutant/reporter/trace.rb +4 -0
  48. data/lib/mutant/require_highjack.rb +2 -2
  49. data/lib/mutant/rspec.rb +0 -0
  50. data/lib/mutant/runner.rb +2 -0
  51. data/lib/mutant/runner/config.rb +8 -8
  52. data/lib/mutant/runner/killer.rb +5 -0
  53. data/lib/mutant/runner/subject.rb +1 -1
  54. data/lib/mutant/subject.rb +8 -8
  55. data/lib/mutant/subject/method.rb +3 -2
  56. data/lib/mutant/subject/method/instance.rb +1 -1
  57. data/lib/mutant/test.rb +7 -65
  58. data/lib/mutant/test/report.rb +59 -0
  59. data/lib/mutant/version.rb +1 -1
  60. data/lib/mutant/warning_filter.rb +2 -0
  61. data/lib/mutant/zombifier.rb +3 -0
  62. data/lib/mutant/zombifier/file.rb +1 -1
  63. data/meta/or_asgn.rb +11 -0
  64. data/meta/send.rb +1 -1
  65. data/mutant-rspec.gemspec +1 -1
  66. data/mutant.gemspec +1 -1
  67. data/spec/integration/mutant/corpus_spec.rb +2 -0
  68. data/spec/integration/mutant/test_mutator_handles_types_spec.rb +2 -2
  69. data/spec/spec_helper.rb +4 -3
  70. data/spec/unit/mutant/cli_new_spec.rb +11 -11
  71. data/spec/unit/mutant/expression/methods_spec.rb +43 -0
  72. data/spec/unit/mutant/expression/namespace/flat_spec.rb +1 -1
  73. data/spec/unit/mutant/expression/namespace/recursive_spec.rb +19 -5
  74. data/spec/unit/mutant/{strategy_spec.rb → integration_spec.rb} +1 -1
  75. data/spec/unit/mutant/isolation_spec.rb +3 -1
  76. data/spec/unit/mutant/matcher/method/instance_spec.rb +5 -5
  77. data/spec/unit/mutant/matcher/method/singleton_spec.rb +8 -8
  78. data/spec/unit/mutant/matcher/methods/instance_spec.rb +5 -8
  79. data/spec/unit/mutant/matcher/methods/singleton_spec.rb +5 -5
  80. data/spec/unit/mutant/matcher/namespace_spec.rb +18 -23
  81. data/spec/unit/mutant/mutation_spec.rb +1 -1
  82. data/spec/unit/mutant/runner/config_spec.rb +4 -5
  83. data/spec/unit/mutant/runner/mutation_spec.rb +21 -21
  84. data/spec/unit/mutant/runner/subject_spec.rb +6 -6
  85. data/spec/unit/mutant/subject/method/instance_spec.rb +0 -4
  86. data/spec/unit/mutant/subject/method/singleton_spec.rb +0 -1
  87. data/spec/unit/mutant/subject_spec.rb +3 -3
  88. metadata +20 -6
  89. data/lib/mutant/node_helpers.rb +0 -52
@@ -0,0 +1,142 @@
1
+ module Mutant
2
+ class Matcher
3
+ # Builder for complex matchers
4
+ class Builder
5
+ include Concord.new(:env), AST::Sexp
6
+
7
+ # Initalize object
8
+ #
9
+ # @param [Cache] env
10
+ #
11
+ # @return [undefined]
12
+ #
13
+ # @api private
14
+ #
15
+ def initialize(env)
16
+ super
17
+ @matchers = []
18
+ @subject_ignores = []
19
+ @subject_selectors = []
20
+ end
21
+
22
+ # Add a subject ignore
23
+ #
24
+ # @param [Expression] expression
25
+ #
26
+ # @return [self]
27
+ #
28
+ # @api private
29
+ #
30
+ def add_subject_ignore(expression)
31
+ @subject_ignores << expression.matcher(env)
32
+ self
33
+ end
34
+
35
+ # Add a subject selector
36
+ #
37
+ # @param [#call] selector
38
+ #
39
+ # @return [self]
40
+ #
41
+ # @api private
42
+ #
43
+ def add_subject_selector(attribute, value)
44
+ @subject_selectors << Morpher.compile(s(:eql, s(:attribute, attribute), s(:static, value)))
45
+ self
46
+ end
47
+
48
+ # Add a match expression
49
+ #
50
+ # @param [Expression] expression
51
+ #
52
+ # @return [self]
53
+ #
54
+ # @api private
55
+ #
56
+ def add_match_expression(expression)
57
+ @matchers << expression.matcher(env)
58
+ self
59
+ end
60
+
61
+ # Return generated matcher
62
+ #
63
+ # @return [Mutant::Matcher]
64
+ #
65
+ # @api private
66
+ #
67
+ def matcher
68
+ if @matchers.empty?
69
+ return Matcher::Null.new
70
+ end
71
+
72
+ matcher = Matcher::Chain.build(@matchers)
73
+
74
+ if predicate
75
+ Matcher::Filter.new(matcher, predicate)
76
+ else
77
+ matcher
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # Return subject selector
84
+ #
85
+ # @return [#call]
86
+ # if selector is present
87
+ #
88
+ # @return [nil]
89
+ # otherwise
90
+ #
91
+ # @api private
92
+ #
93
+ def subject_selector
94
+ Morpher::Evaluator::Predicate::Boolean::Or.new(@subject_selectors) if @subject_selectors.any?
95
+ end
96
+
97
+ # Return predicate
98
+ #
99
+ # @return [#call]
100
+ # if filter is needed
101
+ #
102
+ # @return [nil]
103
+ # othrwise
104
+ #
105
+ # @api private
106
+ #
107
+ def predicate
108
+ if subject_selector && subject_rejector
109
+ Morpher::Evaluator::Predicate::Boolean::And.new([
110
+ subject_selector,
111
+ Morpher::Evaluator::Predicate::Negation.new(subject_rejector)
112
+ ])
113
+ elsif subject_selector
114
+ subject_selector
115
+ elsif subject_rejector
116
+ Morpher::Evaluator::Predicate::Negation.new(subject_rejector)
117
+ else
118
+ nil
119
+ end
120
+ end
121
+
122
+ # Return subject rejector
123
+ #
124
+ # @return [#call]
125
+ # if there is a subject rejector
126
+ #
127
+ # @return [nil]
128
+ # otherwise
129
+ #
130
+ # @api private
131
+ #
132
+ def subject_rejector
133
+ rejectors = @subject_ignores.flat_map(&:to_a).map do |subject|
134
+ Morpher.compile(s(:eql, s(:attribute, :identification), s(:static, subject.identification)))
135
+ end
136
+
137
+ Morpher::Evaluator::Predicate::Boolean::Or.new(rejectors) if rejectors.any?
138
+ end
139
+
140
+ end # Builder
141
+ end # Matcher
142
+ end # Mutant
@@ -2,7 +2,7 @@ module Mutant
2
2
  class Matcher
3
3
  # Matcher for subjects that are a specific method
4
4
  class Method < self
5
- include Adamantium::Flat, Concord::Public.new(:cache, :scope, :method)
5
+ include Adamantium::Flat, Concord::Public.new(:env, :scope, :method)
6
6
  include Equalizer.new(:identification)
7
7
 
8
8
  # Methods within rbx kernel directory are precompiled and their source
@@ -40,11 +40,7 @@ module Mutant
40
40
  def skip?
41
41
  location = source_location
42
42
  if location.nil? || BLACKLIST.match(location.first)
43
- message = format(
44
- '%s does not have valid source location unable to emit matcher',
45
- method.inspect
46
- )
47
- $stderr.puts(message)
43
+ env.warn(format('%s does not have valid source location unable to emit matcher', method.inspect))
48
44
  true
49
45
  else
50
46
  false
@@ -78,7 +74,7 @@ module Mutant
78
74
  # @api private
79
75
  #
80
76
  def ast
81
- cache.parse(source_path)
77
+ env.cache.parse(source_path)
82
78
  end
83
79
 
84
80
  # Return path to source
@@ -7,7 +7,7 @@ module Mutant
7
7
 
8
8
  # Dispatching builder, detects memoizable case
9
9
  #
10
- # @param [Cache] cache
10
+ # @param [Env] env
11
11
  # @param [Class, Module] scope
12
12
  # @param [UnboundMethod] method
13
13
  #
@@ -15,10 +15,10 @@ module Mutant
15
15
  #
16
16
  # @api private
17
17
  #
18
- def self.build(cache, scope, method)
18
+ def self.build(env, scope, method)
19
19
  name = method.name
20
20
  if scope.ancestors.include?(::Memoizable) && scope.memoized?(name)
21
- return Memoized.new(cache, scope, method)
21
+ return Memoized.new(env, scope, method)
22
22
  end
23
23
  super
24
24
  end
@@ -47,8 +47,9 @@ module Mutant
47
47
  # @api private
48
48
  #
49
49
  def match?(node)
50
- location = node.location || return
51
- expression = location.expression || return
50
+ location = node.location || return
51
+ expression = location.expression || return
52
+
52
53
  expression.line.equal?(source_line) &&
53
54
  node.type.equal?(:def) &&
54
55
  node.children[NAME_INDEX].equal?(method_name)
@@ -18,7 +18,6 @@ module Mutant
18
18
 
19
19
  RECEIVER_INDEX = 0
20
20
  NAME_INDEX = 1
21
- CONST_NAME_INDEX = 1
22
21
 
23
22
  private
24
23
 
@@ -76,11 +75,7 @@ module Mutant
76
75
  when :const
77
76
  receiver_name?(receiver)
78
77
  else
79
- message = format(
80
- 'Can only match :defs on :self or :const got %s unable to match',
81
- receiver.type.inspect
82
- )
83
- $stderr.puts(message)
78
+ env.warn(format('Can only match :defs on :self or :const got %s unable to match', receiver.type.inspect))
84
79
  false
85
80
  end
86
81
  end
@@ -94,7 +89,7 @@ module Mutant
94
89
  # @api private
95
90
  #
96
91
  def receiver_name?(node)
97
- name = node.children[CONST_NAME_INDEX]
92
+ name = node.children[NAME_INDEX]
98
93
  name.to_s.eql?(context.unqualified_name)
99
94
  end
100
95
 
@@ -2,7 +2,7 @@ module Mutant
2
2
  class Matcher
3
3
  # Abstract base class for matcher that returns method subjects from scope
4
4
  class Methods < self
5
- include AbstractType, Concord::Public.new(:cache, :scope)
5
+ include AbstractType, Concord::Public.new(:env, :scope)
6
6
 
7
7
  # Enumerate subjects
8
8
  #
@@ -17,9 +17,7 @@ module Mutant
17
17
  def each(&block)
18
18
  return to_enum unless block_given?
19
19
 
20
- methods.each do |method|
21
- emit_matches(method, &block)
22
- end
20
+ subjects.each(&block)
23
21
 
24
22
  self
25
23
  end
@@ -50,19 +48,18 @@ module Mutant
50
48
 
51
49
  private
52
50
 
53
- # Emit matches for method
54
- #
55
- # @param [UnboundMethod, Method] method
51
+ # Return subjects
56
52
  #
57
- # @return [undefined]
53
+ # @return [Array<Subject>]
58
54
  #
59
55
  # @api private
60
56
  #
61
- def emit_matches(method)
62
- matcher.build(cache, scope, method).each do |subject|
63
- yield subject
64
- end
57
+ def subjects
58
+ methods.map do |method|
59
+ matcher.build(env, scope, method)
60
+ end.flat_map(&:to_a)
65
61
  end
62
+ memoize :subjects
66
63
 
67
64
  # Return candidate names
68
65
  #
@@ -73,11 +70,11 @@ module Mutant
73
70
  # @api private
74
71
  #
75
72
  def candidate_names
76
- names =
73
+ (
77
74
  candidate_scope.public_instance_methods(false) +
78
75
  candidate_scope.private_instance_methods(false) +
79
76
  candidate_scope.protected_instance_methods(false)
80
- names.sort
77
+ ).sort
81
78
  end
82
79
 
83
80
  # Return candidate scope
@@ -2,8 +2,10 @@ module Mutant
2
2
  class Matcher
3
3
 
4
4
  # Matcher for specific namespace
5
+ #
6
+ # rubocop:disable LineLength
5
7
  class Namespace < self
6
- include Concord::Public.new(:cache, :namespace)
8
+ include Concord::Public.new(:env, :expression)
7
9
 
8
10
  # Enumerate subjects
9
11
  #
@@ -17,9 +19,8 @@ module Mutant
17
19
  #
18
20
  def each(&block)
19
21
  return to_enum unless block_given?
20
-
21
22
  scopes.each do |scope|
22
- Scope.each(cache, scope, &block)
23
+ scope.each(&block)
23
24
  end
24
25
 
25
26
  self
@@ -27,30 +28,18 @@ module Mutant
27
28
 
28
29
  private
29
30
 
30
- # Return pattern
31
- #
32
- # @return [Regexp]
33
- #
34
- # @api private
35
- #
36
- def pattern
37
- /\A#{Regexp.escape(namespace)}(?:\z|::)/
38
- end
39
- memoize :pattern
40
-
41
31
  # Return scope enumerator
42
32
  #
43
- # @return [Enumerable<Object>]
33
+ # @return [Array<Class, Module>]
44
34
  #
45
35
  # @api private
46
36
  #
47
- def scopes(&block)
48
- return to_enum(__method__) unless block_given?
49
-
50
- ::ObjectSpace.each_object(Module).each do |scope|
51
- emit_scope(scope, &block)
52
- end
37
+ def scopes
38
+ ::ObjectSpace.each_object(Module).each_with_object([]) do |scope, aggregate|
39
+ aggregate << Scope.new(env, scope) if match?(scope)
40
+ end.sort_by(&:identification)
53
41
  end
42
+ memoize :scopes
54
43
 
55
44
  # Return scope name
56
45
  #
@@ -64,36 +53,39 @@ module Mutant
64
53
  #
65
54
  # @api private
66
55
  #
67
- def self.scope_name(scope)
56
+ # rubocop:disable LineLength
57
+ #
58
+ def scope_name(scope)
68
59
  scope.name
69
60
  rescue => exception
70
- $stderr.puts <<-MESSAGE
71
- WARNING:
72
- While optaining #{scope.class}#name from: #{scope.inspect}
73
- It raised an error: #{exception.inspect} fix your lib!
74
- MESSAGE
61
+ env.warn("While optaining #{scope.class}#name from: #{scope.inspect} It raised an error: #{exception.inspect} fix your lib!")
75
62
  nil
76
63
  end
77
64
 
78
- # Yield scope if name matches pattern
65
+ # Test scope if name matches expresion
79
66
  #
80
67
  # @param [Module,Class] scope
81
68
  #
82
- # @return [undefined]
69
+ # @return [Boolean]
83
70
  #
84
71
  # @api private
85
72
  #
86
- def emit_scope(scope)
87
- name = self.class.scope_name(scope)
88
- unless name.nil? or name.kind_of?(String)
89
- $stderr.puts <<-MESSAGE
90
- WARNING:
91
- #{scope.class}#name from: #{scope.inspect} did not return a String or nil.
92
- Fix your lib to support normal ruby semantics!
93
- MESSAGE
94
- return
73
+ def match?(scope)
74
+ name = scope_name(scope) or return false
75
+
76
+ unless name.kind_of?(String)
77
+ env.warn("#{scope.class}#name from: #{scope.inspect} did not return a String or nil. Fix your lib to support normal ruby semantics!")
78
+ return false
95
79
  end
96
- yield scope if pattern =~ name
80
+
81
+ scope_expression = Expression.try_parse(name)
82
+
83
+ unless scope_expression
84
+ $stderr.puts("WARNING: #{name.inspect} is not an identifiable ruby class name.")
85
+ return false
86
+ end
87
+
88
+ expression.prefix?(scope_expression)
97
89
  end
98
90
 
99
91
  end # Namespace
@@ -2,13 +2,24 @@ module Mutant
2
2
  class Matcher
3
3
  # Matcher for specific namespace
4
4
  class Scope < self
5
- include Concord::Public.new(:cache, :scope)
5
+ include Concord::Public.new(:env, :scope)
6
6
 
7
7
  MATCHERS = [
8
8
  Matcher::Methods::Singleton,
9
9
  Matcher::Methods::Instance
10
10
  ].freeze
11
11
 
12
+ # Return identification
13
+ #
14
+ # @return [String]
15
+ #
16
+ # @api private
17
+ #
18
+ def identification
19
+ scope.name
20
+ end
21
+ memoize :identification
22
+
12
23
  # Enumerate subjects
13
24
  #
14
25
  # @return [self]
@@ -23,7 +34,7 @@ module Mutant
23
34
  return to_enum unless block_given?
24
35
 
25
36
  MATCHERS.each do |matcher|
26
- matcher.each(cache, scope, &block)
37
+ matcher.new(env, scope).each(&block)
27
38
  end
28
39
 
29
40
  self
@@ -1,7 +1,6 @@
1
1
  module Mutant
2
2
  # Namespace for mutant metadata
3
3
  module Meta
4
-
5
4
  require 'mutant/meta/example'
6
5
  require 'mutant/meta/example/dsl'
7
6