mutant 0.10.23 → 0.10.24

Sign up to get free protection for your applications and to get access to all the features.
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