mutant 0.10.23 → 0.10.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant.rb +20 -4
  3. data/lib/mutant/ast/regexp.rb +37 -0
  4. data/lib/mutant/ast/regexp/transformer.rb +150 -0
  5. data/lib/mutant/ast/regexp/transformer/direct.rb +121 -0
  6. data/lib/mutant/ast/regexp/transformer/named_group.rb +50 -0
  7. data/lib/mutant/ast/regexp/transformer/options_group.rb +68 -0
  8. data/lib/mutant/ast/regexp/transformer/quantifier.rb +90 -0
  9. data/lib/mutant/ast/regexp/transformer/recursive.rb +56 -0
  10. data/lib/mutant/ast/regexp/transformer/root.rb +28 -0
  11. data/lib/mutant/ast/regexp/transformer/text.rb +58 -0
  12. data/lib/mutant/ast/types.rb +115 -2
  13. data/lib/mutant/cli/command/environment.rb +9 -3
  14. data/lib/mutant/config.rb +8 -54
  15. data/lib/mutant/config/coverage_criteria.rb +61 -0
  16. data/lib/mutant/expression.rb +0 -12
  17. data/lib/mutant/matcher.rb +2 -2
  18. data/lib/mutant/matcher/config.rb +25 -6
  19. data/lib/mutant/matcher/method.rb +2 -3
  20. data/lib/mutant/meta/example/dsl.rb +6 -1
  21. data/lib/mutant/mutator/node/kwargs.rb +2 -2
  22. data/lib/mutant/mutator/node/literal/regex.rb +26 -0
  23. data/lib/mutant/mutator/node/regexp.rb +31 -0
  24. data/lib/mutant/mutator/node/regexp/alternation_meta.rb +20 -0
  25. data/lib/mutant/mutator/node/regexp/capture_group.rb +25 -0
  26. data/lib/mutant/mutator/node/regexp/character_type.rb +31 -0
  27. data/lib/mutant/mutator/node/regexp/end_of_line_anchor.rb +20 -0
  28. data/lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb +20 -0
  29. data/lib/mutant/mutator/node/regexp/greedy_zero_or_more.rb +24 -0
  30. data/lib/mutant/subject.rb +1 -3
  31. data/lib/mutant/subject/method/instance.rb +1 -3
  32. data/lib/mutant/version.rb +1 -1
  33. data/lib/mutant/world.rb +1 -2
  34. metadata +40 -4
  35. data/lib/mutant/warnings.rb +0 -106
@@ -37,54 +37,6 @@ module Mutant
37
37
 
38
38
  private_constant(*constants(false))
39
39
 
40
- class CoverageCriteria
41
- include Anima.new(:process_abort, :test_result, :timeout)
42
-
43
- EMPTY = new(
44
- process_abort: nil,
45
- test_result: nil,
46
- timeout: nil
47
- )
48
-
49
- DEFAULT = new(
50
- process_abort: false,
51
- test_result: true,
52
- timeout: false
53
- )
54
-
55
- TRANSFORM =
56
- Transform::Sequence.new(
57
- [
58
- Transform::Hash.new(
59
- optional: [
60
- Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
61
- Transform::Hash::Key.new('test_result', Transform::BOOLEAN),
62
- Transform::Hash::Key.new('timeout', Transform::BOOLEAN)
63
- ],
64
- required: []
65
- ),
66
- Transform::Hash::Symbolize.new,
67
- ->(value) { Either::Right.new(DEFAULT.with(**value)) }
68
- ]
69
- )
70
-
71
- def merge(other)
72
- self.class.new(
73
- process_abort: overwrite(other, :process_abort),
74
- test_result: overwrite(other, :test_result),
75
- timeout: overwrite(other, :timeout)
76
- )
77
- end
78
-
79
- private
80
-
81
- def overwrite(other, attribute_name)
82
- other_value = other.public_send(attribute_name)
83
-
84
- other_value.nil? ? public_send(attribute_name) : other_value
85
- end
86
- end # CoverageCriteria
87
-
88
40
  # Merge with other config
89
41
  #
90
42
  # @param [Config] other
@@ -116,13 +68,14 @@ module Mutant
116
68
  #
117
69
  # @return [Either<String,Config>]
118
70
  def self.load_config_file(world)
119
- config = DEFAULT
120
- files = CANDIDATES.map(&world.pathname.public_method(:new)).select(&:readable?)
71
+ files = CANDIDATES
72
+ .map(&world.pathname.public_method(:new))
73
+ .select(&:readable?)
121
74
 
122
75
  if files.one?
123
- load_contents(files.first).fmap(&config.public_method(:with))
76
+ load_contents(files.first).fmap(&DEFAULT.public_method(:with))
124
77
  elsif files.empty?
125
- Either::Right.new(config)
78
+ Either::Right.new(DEFAULT)
126
79
  else
127
80
  Either::Left.new(MORE_THAN_ONE_CONFIG_FILE % files.join(', '))
128
81
  end
@@ -159,13 +112,14 @@ module Mutant
159
112
  Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
160
113
  Transform::Hash.new(
161
114
  optional: [
162
- Transform::Hash::Key.new('coverage_criteria', CoverageCriteria::TRANSFORM),
115
+ Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
163
116
  Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
164
117
  Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
165
118
  Transform::Hash::Key.new('integration', Transform::STRING),
166
119
  Transform::Hash::Key.new('jobs', Transform::INTEGER),
167
120
  Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
168
- Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
121
+ Transform::Hash::Key.new('requires', Transform::STRING_ARRAY),
122
+ Transform::Hash::Key.new('matcher', Matcher::Config::LOADER)
169
123
  ],
170
124
  required: []
171
125
  ),
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Config
5
+ # Configuration of coverge conditions
6
+ class CoverageCriteria
7
+ include Anima.new(:process_abort, :test_result, :timeout)
8
+
9
+ EMPTY = new(
10
+ process_abort: nil,
11
+ test_result: nil,
12
+ timeout: nil
13
+ )
14
+
15
+ DEFAULT = new(
16
+ process_abort: false,
17
+ test_result: true,
18
+ timeout: false
19
+ )
20
+
21
+ TRANSFORM =
22
+ Transform::Sequence.new(
23
+ [
24
+ Transform::Hash.new(
25
+ optional: [
26
+ Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
27
+ Transform::Hash::Key.new('test_result', Transform::BOOLEAN),
28
+ Transform::Hash::Key.new('timeout', Transform::BOOLEAN)
29
+ ],
30
+ required: []
31
+ ),
32
+ Transform::Hash::Symbolize.new,
33
+ ->(value) { Either::Right.new(DEFAULT.with(**value)) }
34
+ ]
35
+ )
36
+
37
+ # Merge coverage criteria with other instance
38
+ #
39
+ # Values from the other instance have precedence.
40
+ #
41
+ # @param [CoverageCriteria] other
42
+ #
43
+ # @return [CoverageCriteria]
44
+ def merge(other)
45
+ self.class.new(
46
+ process_abort: overwrite(other, :process_abort),
47
+ test_result: overwrite(other, :test_result),
48
+ timeout: overwrite(other, :timeout)
49
+ )
50
+ end
51
+
52
+ private
53
+
54
+ def overwrite(other, attribute_name)
55
+ other_value = other.public_send(attribute_name)
56
+
57
+ other_value.nil? ? public_send(attribute_name) : other_value
58
+ end
59
+ end # CoverageCriteria
60
+ end # Config
61
+ end # Mutant
@@ -4,8 +4,6 @@ module Mutant
4
4
 
5
5
  # Abstract base class for match expression
6
6
  class Expression
7
- include AbstractType
8
-
9
7
  fragment = /[A-Za-z][A-Za-z\d_]*/.freeze
10
8
  SCOPE_NAME_PATTERN = /(?<scope_name>#{fragment}(?:#{SCOPE_OPERATOR}#{fragment})*)/.freeze
11
9
  SCOPE_SYMBOL_PATTERN = '(?<scope_symbol>[.#])'
@@ -16,16 +14,6 @@ module Mutant
16
14
  super.freeze
17
15
  end
18
16
 
19
- # Syntax of expression
20
- #
21
- # @return [Matcher]
22
- abstract_method :matcher
23
-
24
- # Syntax of expression
25
- #
26
- # @return [String]
27
- abstract_method :syntax
28
-
29
17
  # Match length with other expression
30
18
  #
31
19
  # @param [Expression] other
@@ -20,7 +20,7 @@ module Mutant
20
20
  # @return [Matcher]
21
21
  def self.from_config(config)
22
22
  Filter.new(
23
- Chain.new(config.match_expressions.map(&:matcher)),
23
+ Chain.new(config.subjects.map(&:matcher)),
24
24
  method(:allowed_subject?).curry.call(config)
25
25
  )
26
26
  end
@@ -42,7 +42,7 @@ module Mutant
42
42
  #
43
43
  # @return [Boolean]
44
44
  def self.ignore_subject?(config, subject)
45
- config.ignore_expressions.any? do |expression|
45
+ config.ignore.any? do |expression|
46
46
  expression.prefix?(subject.expression)
47
47
  end
48
48
  end
@@ -5,8 +5,8 @@ module Mutant
5
5
  # Subject matcher configuration
6
6
  class Config
7
7
  include Adamantium, Anima.new(
8
- :ignore_expressions,
9
- :match_expressions,
8
+ :ignore,
9
+ :subjects,
10
10
  :start_expressions,
11
11
  :subject_filters
12
12
  )
@@ -17,15 +17,34 @@ module Mutant
17
17
  ENUM_DELIMITER = ','
18
18
  EMPTY_ATTRIBUTES = 'empty'
19
19
  PRESENTATIONS = IceNine.deep_freeze(
20
- ignore_expressions: :syntax,
21
- match_expressions: :syntax,
22
- start_expressions: :syntax,
23
- subject_filters: :inspect
20
+ ignore: :syntax,
21
+ start_expressions: :syntax,
22
+ subject_filters: :inspect,
23
+ subjects: :syntax
24
24
  )
25
25
  private_constant(*constants(false))
26
26
 
27
27
  DEFAULT = new(Hash[anima.attribute_names.map { |name| [name, []] }])
28
28
 
29
+ expression = ->(input) { Mutant::Config::DEFAULT.expression_parser.call(input) }
30
+
31
+ expression_array = Transform::Array.new(expression)
32
+
33
+ LOADER =
34
+ Transform::Sequence.new(
35
+ [
36
+ Transform::Hash.new(
37
+ optional: [
38
+ Transform::Hash::Key.new('subjects', expression_array),
39
+ Transform::Hash::Key.new('ignore', expression_array)
40
+ ],
41
+ required: []
42
+ ),
43
+ Transform::Hash::Symbolize.new,
44
+ ->(attributes) { Either::Right.new(DEFAULT.with(attributes)) }
45
+ ]
46
+ )
47
+
29
48
  # Inspection string
30
49
  #
31
50
  # @return [String]
@@ -87,9 +87,8 @@ module Mutant
87
87
  node = matched_node_path.last || return
88
88
 
89
89
  self.class::SUBJECT_CLASS.new(
90
- context: context,
91
- node: node,
92
- warnings: env.world.warnings
90
+ context: context,
91
+ node: node
93
92
  )
94
93
  end
95
94
  memoize :subject
@@ -37,7 +37,7 @@ module Mutant
37
37
  # @return [Example]
38
38
  #
39
39
  # @raise [RuntimeError]
40
- # in case example cannot be build
40
+ # in case the example cannot be built
41
41
  def example
42
42
  fail 'source not defined' unless @source
43
43
 
@@ -82,6 +82,11 @@ module Mutant
82
82
  mutation('self')
83
83
  end
84
84
 
85
+ def regexp_mutations
86
+ mutation('//')
87
+ mutation('/nomatch\A/')
88
+ end
89
+
85
90
  def node(input)
86
91
  case input
87
92
  when String
@@ -28,7 +28,7 @@ module Mutant
28
28
  def emit_argument_mutations
29
29
  children.each_with_index do |child, index|
30
30
  Mutator.mutate(child).each do |mutant|
31
- if forbid_argument?(mutant)
31
+ unless forbid_argument?(mutant)
32
32
  emit_child_update(index, mutant)
33
33
  end
34
34
  end
@@ -36,7 +36,7 @@ module Mutant
36
36
  end
37
37
 
38
38
  def forbid_argument?(node)
39
- !(n_pair?(node) && DISALLOW.include?(node.children.first.type))
39
+ n_pair?(node) && DISALLOW.include?(node.children.first.type)
40
40
  end
41
41
  end # Kwargs
42
42
  end # Node
@@ -19,6 +19,7 @@ module Mutant
19
19
  end
20
20
 
21
21
  def dispatch
22
+ mutate_body
22
23
  emit_singletons unless parent_node
23
24
  children.each_with_index do |child, index|
24
25
  mutate_child(index) unless n_str?(child)
@@ -27,6 +28,31 @@ module Mutant
27
28
  emit_type(s(:str, NULL_REGEXP_SOURCE), options)
28
29
  end
29
30
 
31
+ # NOTE: will only mutate parts of regexp body if the
32
+ # body is composed of only strings. Regular expressions
33
+ # with interpolation are skipped
34
+ def mutate_body
35
+ return unless body.all?(&method(:n_str?))
36
+
37
+ Mutator.mutate(body_ast).each do |mutation|
38
+ source = AST::Regexp.to_expression(mutation).to_s
39
+ emit_type(s(:str, source), options)
40
+ end
41
+ end
42
+
43
+ def body_ast
44
+ AST::Regexp.to_ast(body_expression)
45
+ end
46
+
47
+ def body_expression
48
+ AST::Regexp.parse(body.map(&:children).join)
49
+ end
50
+ memoize :body_expression
51
+
52
+ def body
53
+ children.slice(0...-1)
54
+ end
55
+
30
56
  end # Regex
31
57
  end # Literal
32
58
  end # Node
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ module Regexp
7
+ # Mutator for root expression regexp wrapper
8
+ class RootExpression < Node
9
+ handle(:regexp_root_expression)
10
+
11
+ private
12
+
13
+ def dispatch
14
+ children.each_index(&method(:mutate_child))
15
+ end
16
+ end # RootExpression
17
+
18
+ # Mutator for beginning of line anchor `^`
19
+ class BeginningOfLineAnchor < Node
20
+ handle(:regexp_bol_anchor)
21
+
22
+ private
23
+
24
+ def dispatch
25
+ emit(s(:regexp_bos_anchor))
26
+ end
27
+ end # BeginningOfLineAnchor
28
+ end # Regexp
29
+ end # Node
30
+ end # Mutator
31
+ end # Mutant
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ module Regexp
7
+ # Mutator for pipe in `/foo|bar/` regexp
8
+ class AlternationMeta < Node
9
+ handle(:regexp_alternation_meta)
10
+
11
+ private
12
+
13
+ def dispatch
14
+ children.each_index(&method(:delete_child))
15
+ end
16
+ end # AlternationMeta
17
+ end # Regexp
18
+ end # Node
19
+ end # Mutator
20
+ end # Mutant
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ module Regexp
7
+ # Mutator for regexp capture groups, such as `/(foo)/`
8
+ class CaptureGroup < Node
9
+ handle(:regexp_capture_group)
10
+
11
+ children :group
12
+
13
+ private
14
+
15
+ def dispatch
16
+ return unless group
17
+
18
+ emit(s(:regexp_passive_group, group))
19
+ emit_group_mutations
20
+ end
21
+ end # EndOfLineAnchor
22
+ end # Regexp
23
+ end # Node
24
+ end # Mutator
25
+ end # Mutant
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ module Regexp
7
+ # Character type mutator
8
+ class CharacterType < Node
9
+ map = {
10
+ regexp_digit_type: :regexp_nondigit_type,
11
+ regexp_hex_type: :regexp_nonhex_type,
12
+ regexp_space_type: :regexp_nonspace_type,
13
+ regexp_word_boundary_anchor: :regexp_nonword_boundary_anchor,
14
+ regexp_word_type: :regexp_nonword_type,
15
+ regexp_xgrapheme_type: :regexp_linebreak_type
16
+ }
17
+
18
+ MAP = IceNine.deep_freeze(map.merge(map.invert))
19
+
20
+ handle(*MAP.keys)
21
+
22
+ private
23
+
24
+ def dispatch
25
+ emit(s(MAP.fetch(node.type)))
26
+ end
27
+ end # CharacterType
28
+ end # Regexp
29
+ end # Node
30
+ end # Mutator
31
+ end # Mutant