mutant 0.11.9 → 0.11.12
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.
- checksums.yaml +4 -4
- data/lib/mutant/ast/pattern/lexer.rb +171 -0
- data/lib/mutant/ast/pattern/parser.rb +194 -0
- data/lib/mutant/ast/pattern/source.rb +39 -0
- data/lib/mutant/ast/pattern/token.rb +15 -0
- data/lib/mutant/ast/pattern.rb +125 -0
- data/lib/mutant/ast/structure.rb +890 -0
- data/lib/mutant/bootstrap.rb +8 -7
- data/lib/mutant/cli/command/environment.rb +5 -3
- data/lib/mutant/cli/command/util.rb +17 -2
- data/lib/mutant/config.rb +76 -40
- data/lib/mutant/env.rb +1 -1
- data/lib/mutant/license/subscription/opensource.rb +1 -1
- data/lib/mutant/matcher/method.rb +8 -1
- data/lib/mutant/meta/example.rb +4 -1
- data/lib/mutant/mutation/config.rb +36 -0
- data/lib/mutant/mutator/node/arguments.rb +1 -1
- data/lib/mutant/mutator/node/begin.rb +2 -1
- data/lib/mutant/mutator/node/define.rb +5 -2
- data/lib/mutant/mutator/node/kwargs.rb +2 -2
- data/lib/mutant/mutator/node/literal/regex.rb +1 -1
- data/lib/mutant/mutator/node/literal/symbol.rb +2 -2
- data/lib/mutant/mutator/node/named_value/constant_assignment.rb +1 -1
- data/lib/mutant/mutator/node/named_value/variable_assignment.rb +2 -2
- data/lib/mutant/mutator/node/regexp/capture_group.rb +3 -5
- data/lib/mutant/mutator/node/regexp/named_group.rb +5 -5
- data/lib/mutant/mutator/node/resbody.rb +0 -10
- data/lib/mutant/mutator/node.rb +47 -1
- data/lib/mutant/mutator/util/array.rb +0 -17
- data/lib/mutant/mutator.rb +2 -26
- data/lib/mutant/parallel/driver.rb +18 -2
- data/lib/mutant/reporter/cli/printer/config.rb +1 -1
- data/lib/mutant/runner.rb +4 -0
- data/lib/mutant/subject/config.rb +5 -4
- data/lib/mutant/subject.rb +4 -1
- data/lib/mutant/transform.rb +14 -0
- data/lib/mutant/version.rb +1 -1
- data/lib/mutant/world.rb +1 -0
- data/lib/mutant.rb +9 -1
- metadata +9 -2
data/lib/mutant/bootstrap.rb
CHANGED
@@ -24,16 +24,15 @@ module Mutant
|
|
24
24
|
|
25
25
|
# Run Bootstrap
|
26
26
|
#
|
27
|
-
# @param [
|
28
|
-
# @param [Config] config
|
27
|
+
# @param [Env] env
|
29
28
|
#
|
30
29
|
# @return [Either<String, Env>]
|
31
30
|
#
|
32
31
|
# rubocop:disable Metrics/MethodLength
|
33
|
-
def self.call(
|
34
|
-
env = load_hooks(
|
32
|
+
def self.call(env)
|
33
|
+
env = load_hooks(env)
|
35
34
|
.tap(&method(:infect))
|
36
|
-
.with(matchable_scopes: matchable_scopes(
|
35
|
+
.with(matchable_scopes: matchable_scopes(env))
|
37
36
|
|
38
37
|
subjects = start_subject(env, Matcher.from_config(env.config.matcher).call(env))
|
39
38
|
|
@@ -82,8 +81,10 @@ module Mutant
|
|
82
81
|
end
|
83
82
|
private_class_method :infect
|
84
83
|
|
85
|
-
def self.matchable_scopes(
|
86
|
-
|
84
|
+
def self.matchable_scopes(env)
|
85
|
+
config = env.config
|
86
|
+
|
87
|
+
scopes = env.world.object_space.each_object(Module).each_with_object([]) do |scope, aggregate|
|
87
88
|
expression = expression(config.reporter, config.expression_parser, scope) || next
|
88
89
|
aggregate << Scope.new(scope, expression)
|
89
90
|
end
|
@@ -23,9 +23,11 @@ module Mutant
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def bootstrap
|
26
|
-
|
26
|
+
env = Env.empty(world, @config)
|
27
|
+
|
28
|
+
Config.load_config_file(env)
|
27
29
|
.fmap(&method(:expand))
|
28
|
-
.bind { Bootstrap.call(
|
30
|
+
.bind { Bootstrap.call(env.with(config: @config)) }
|
29
31
|
end
|
30
32
|
|
31
33
|
def expand(file_config)
|
@@ -121,7 +123,7 @@ module Mutant
|
|
121
123
|
set(jobs: Integer(number))
|
122
124
|
end
|
123
125
|
parser.on('-t', '--mutation-timeout NUMBER', 'Per mutation analysis timeout') do |number|
|
124
|
-
set(
|
126
|
+
set(mutation: @config.mutation.with(timeout: Float(number)))
|
125
127
|
end
|
126
128
|
end
|
127
129
|
end # Run
|
@@ -14,6 +14,12 @@ module Mutant
|
|
14
14
|
OPTIONS = %i[add_target_options].freeze
|
15
15
|
|
16
16
|
def action
|
17
|
+
@ignore_patterns.map! do |syntax|
|
18
|
+
AST::Pattern.parse(syntax).from_right do |message|
|
19
|
+
return Either::Left.new(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
17
23
|
@targets.each(&method(:print_mutations))
|
18
24
|
Either::Right.new(nil)
|
19
25
|
end
|
@@ -50,18 +56,27 @@ module Mutant
|
|
50
56
|
def initialize(_arguments)
|
51
57
|
super
|
52
58
|
|
53
|
-
@targets
|
59
|
+
@targets = []
|
60
|
+
@ignore_patterns = []
|
54
61
|
end
|
55
62
|
|
56
63
|
def add_target_options(parser)
|
57
64
|
parser.on('-e', '--evaluate SOURCE') do |source|
|
58
65
|
@targets << Target::Source.new(source)
|
59
66
|
end
|
67
|
+
|
68
|
+
parser.on('-i', '--ignore-pattern AST_PATTERN') do |pattern|
|
69
|
+
@ignore_patterns << pattern
|
70
|
+
end
|
60
71
|
end
|
61
72
|
|
62
73
|
def print_mutations(target)
|
63
74
|
world.stdout.puts(target.identification)
|
64
|
-
|
75
|
+
|
76
|
+
Mutator::Node.mutate(
|
77
|
+
config: Mutant::Mutation::Config::DEFAULT.with(ignore_patterns: @ignore_patterns),
|
78
|
+
node: target.node
|
79
|
+
).each do |mutation|
|
65
80
|
Reporter::CLI::Printer::Mutation.call(
|
66
81
|
world.stdout,
|
67
82
|
Mutant::Mutation::Evil.new(target, mutation)
|
data/lib/mutant/config.rb
CHANGED
@@ -19,7 +19,7 @@ module Mutant
|
|
19
19
|
:isolation,
|
20
20
|
:jobs,
|
21
21
|
:matcher,
|
22
|
-
:
|
22
|
+
:mutation,
|
23
23
|
:reporter,
|
24
24
|
:requires,
|
25
25
|
:zombie
|
@@ -39,6 +39,20 @@ module Mutant
|
|
39
39
|
mutant.yml
|
40
40
|
].freeze
|
41
41
|
|
42
|
+
MUTATION_TIMEOUT_DEPRECATION = <<~'MESSAGE'
|
43
|
+
Deprecated configuration toplevel key `mutation_timeout` found.
|
44
|
+
|
45
|
+
This key will be removed in the next mayor version.
|
46
|
+
Instead place your mutation timeout configuration under the `mutation` key
|
47
|
+
like this:
|
48
|
+
|
49
|
+
```
|
50
|
+
# mutant.yml
|
51
|
+
mutation:
|
52
|
+
timeout: 10.0 # float here.
|
53
|
+
```
|
54
|
+
MESSAGE
|
55
|
+
|
42
56
|
private_constant(*constants(false))
|
43
57
|
|
44
58
|
# Merge with other config
|
@@ -59,7 +73,7 @@ module Mutant
|
|
59
73
|
integration: other.integration || integration,
|
60
74
|
jobs: other.jobs || jobs,
|
61
75
|
matcher: matcher.merge(other.matcher),
|
62
|
-
|
76
|
+
mutation: mutation.merge(other.mutation),
|
63
77
|
requires: requires + other.requires,
|
64
78
|
zombie: zombie || other.zombie
|
65
79
|
)
|
@@ -69,17 +83,16 @@ module Mutant
|
|
69
83
|
|
70
84
|
# Load config file
|
71
85
|
#
|
72
|
-
# @param [
|
73
|
-
# @param [Config] config
|
86
|
+
# @param [Env] env
|
74
87
|
#
|
75
88
|
# @return [Either<String,Config>]
|
76
|
-
def self.load_config_file(
|
89
|
+
def self.load_config_file(env)
|
77
90
|
files = CANDIDATES
|
78
|
-
.map(&world.pathname.public_method(:new))
|
91
|
+
.map(&env.world.pathname.public_method(:new))
|
79
92
|
.select(&:readable?)
|
80
93
|
|
81
94
|
if files.one?
|
82
|
-
load_contents(files.first).fmap(&DEFAULT.public_method(:with))
|
95
|
+
load_contents(env, files.first).fmap(&DEFAULT.public_method(:with))
|
83
96
|
elsif files.empty?
|
84
97
|
Either::Right.new(DEFAULT)
|
85
98
|
else
|
@@ -97,14 +110,30 @@ module Mutant
|
|
97
110
|
)
|
98
111
|
end
|
99
112
|
|
100
|
-
def self.load_contents(path)
|
113
|
+
def self.load_contents(env, path)
|
101
114
|
Transform::Named
|
102
|
-
.new(
|
115
|
+
.new(
|
116
|
+
path.to_s,
|
117
|
+
sequence(env.config.reporter)
|
118
|
+
)
|
103
119
|
.call(path)
|
104
120
|
.lmap(&:compact_message)
|
105
121
|
end
|
106
122
|
private_class_method :load_contents
|
107
123
|
|
124
|
+
def self.sequence(reporter)
|
125
|
+
Transform::Sequence.new(
|
126
|
+
[
|
127
|
+
Transform::Exception.new(SystemCallError, :read.to_proc),
|
128
|
+
Transform::Exception.new(YAML::SyntaxError, YAML.public_method(:safe_load)),
|
129
|
+
Transform::Primitive.new(Hash),
|
130
|
+
Transform::Success.new(->(hash) { deprecations(reporter, hash) }),
|
131
|
+
*TRANSFORMS
|
132
|
+
]
|
133
|
+
)
|
134
|
+
end
|
135
|
+
private_class_method :sequence
|
136
|
+
|
108
137
|
# The configuration from the environment
|
109
138
|
#
|
110
139
|
# @return [Config]
|
@@ -139,38 +168,45 @@ module Mutant
|
|
139
168
|
|
140
169
|
Either::Right.new(hash)
|
141
170
|
end
|
142
|
-
TRANSFORM = Transform::Sequence.new(
|
143
|
-
[
|
144
|
-
Transform::Exception.new(SystemCallError, :read.to_proc),
|
145
|
-
Transform::Exception.new(YAML::SyntaxError, YAML.public_method(:safe_load)),
|
146
|
-
Transform::Hash.new(
|
147
|
-
optional: [
|
148
|
-
Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
|
149
|
-
Transform::Hash::Key.new(
|
150
|
-
'environment_variables',
|
151
|
-
Transform::Sequence.new(
|
152
|
-
[
|
153
|
-
Transform::Primitive.new(Hash),
|
154
|
-
Transform::Block.capture(:environment_variables, &method(:parse_environment_variables))
|
155
|
-
]
|
156
|
-
)
|
157
|
-
),
|
158
|
-
Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
|
159
|
-
Transform::Hash::Key.new('hooks', PATHNAME_ARRAY),
|
160
|
-
Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
|
161
|
-
Transform::Hash::Key.new('integration', Transform::STRING),
|
162
|
-
Transform::Hash::Key.new('jobs', Transform::INTEGER),
|
163
|
-
Transform::Hash::Key.new('matcher', Matcher::Config::LOADER),
|
164
|
-
Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
|
165
|
-
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
|
166
|
-
],
|
167
|
-
required: []
|
168
|
-
),
|
169
|
-
Transform::Hash::Symbolize.new
|
170
|
-
]
|
171
|
-
)
|
172
171
|
|
173
|
-
|
172
|
+
def self.deprecations(reporter, hash)
|
173
|
+
if hash.key?('mutation_timeout')
|
174
|
+
reporter.warn(MUTATION_TIMEOUT_DEPRECATION)
|
175
|
+
|
176
|
+
(hash['mutation'] ||= {})['timeout'] ||= hash.delete('mutation_timeout')
|
177
|
+
end
|
178
|
+
|
179
|
+
hash
|
180
|
+
end
|
181
|
+
|
182
|
+
TRANSFORMS = [
|
183
|
+
Transform::Hash.new(
|
184
|
+
optional: [
|
185
|
+
Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
|
186
|
+
Transform::Hash::Key.new(
|
187
|
+
'environment_variables',
|
188
|
+
Transform::Sequence.new(
|
189
|
+
[
|
190
|
+
Transform::Primitive.new(Hash),
|
191
|
+
Transform::Block.capture(:environment_variables, &method(:parse_environment_variables))
|
192
|
+
]
|
193
|
+
)
|
194
|
+
),
|
195
|
+
Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
|
196
|
+
Transform::Hash::Key.new('hooks', PATHNAME_ARRAY),
|
197
|
+
Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
|
198
|
+
Transform::Hash::Key.new('integration', Transform::STRING),
|
199
|
+
Transform::Hash::Key.new('jobs', Transform::INTEGER),
|
200
|
+
Transform::Hash::Key.new('matcher', Matcher::Config::LOADER),
|
201
|
+
Transform::Hash::Key.new('mutation', Mutation::Config::TRANSFORM),
|
202
|
+
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
|
203
|
+
],
|
204
|
+
required: []
|
205
|
+
),
|
206
|
+
Transform::Hash::Symbolize.new
|
207
|
+
].freeze
|
208
|
+
|
209
|
+
private_constant(:TRANSFORMS)
|
174
210
|
end # Config
|
175
211
|
# rubocop:enable Metrics/ClassLength
|
176
212
|
end # Mutant
|
data/lib/mutant/env.rb
CHANGED
@@ -137,7 +137,7 @@ module Mutant
|
|
137
137
|
private
|
138
138
|
|
139
139
|
def run_mutation_tests(mutation, tests)
|
140
|
-
config.isolation.call(config.
|
140
|
+
config.isolation.call(config.mutation.timeout) do
|
141
141
|
hooks.run(:mutation_insert_pre, mutation)
|
142
142
|
result = mutation.insert(world.kernel)
|
143
143
|
hooks.run(:mutation_insert_post, mutation)
|
@@ -99,13 +99,20 @@ module Mutant
|
|
99
99
|
node = matched_node_path.last || return
|
100
100
|
|
101
101
|
self.class::SUBJECT_CLASS.new(
|
102
|
-
config:
|
102
|
+
config: subject_config(node),
|
103
103
|
context: context,
|
104
104
|
node: node,
|
105
105
|
visibility: visibility
|
106
106
|
)
|
107
107
|
end
|
108
108
|
|
109
|
+
def subject_config(node)
|
110
|
+
Subject::Config.parse(
|
111
|
+
comments: ast.comment_associations.fetch(node, []),
|
112
|
+
mutation: env.config.mutation
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
109
116
|
def matched_node_path
|
110
117
|
AST.find_last_path(ast.node, &method(:match?))
|
111
118
|
end
|
data/lib/mutant/meta/example.rb
CHANGED
@@ -55,7 +55,10 @@ module Mutant
|
|
55
55
|
#
|
56
56
|
# @return [Enumerable<Mutant::Mutation>]
|
57
57
|
def generated
|
58
|
-
Mutator.mutate(
|
58
|
+
Mutator::Node.mutate(
|
59
|
+
config: Mutation::Config::DEFAULT,
|
60
|
+
node: node
|
61
|
+
).map do |node|
|
59
62
|
Mutation::Evil.new(self, node)
|
60
63
|
end
|
61
64
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mutant
|
4
|
+
class Mutation
|
5
|
+
class Config
|
6
|
+
include Anima.new(:ignore_patterns, :timeout)
|
7
|
+
|
8
|
+
DEFAULT = new(
|
9
|
+
timeout: nil,
|
10
|
+
ignore_patterns: []
|
11
|
+
)
|
12
|
+
|
13
|
+
ignore_pattern = Transform::Block.capture('ignore pattern', &AST::Pattern.method(:parse))
|
14
|
+
|
15
|
+
TRANSFORM = Transform::Sequence.new(
|
16
|
+
[
|
17
|
+
Transform::Hash.new(
|
18
|
+
optional: [
|
19
|
+
Transform::Hash::Key.new('ignore_patterns', Transform::Array.new(ignore_pattern)),
|
20
|
+
Transform::Hash::Key.new('timeout', Transform::FLOAT)
|
21
|
+
],
|
22
|
+
required: []
|
23
|
+
),
|
24
|
+
Transform::Hash::Symbolize.new,
|
25
|
+
Transform::Success.new(DEFAULT.method(:with))
|
26
|
+
]
|
27
|
+
)
|
28
|
+
|
29
|
+
def merge(other)
|
30
|
+
with(
|
31
|
+
timeout: other.timeout || timeout
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end # Config
|
35
|
+
end # Mutation
|
36
|
+
end # Mutant
|
@@ -50,7 +50,7 @@ module Mutant
|
|
50
50
|
|
51
51
|
def emit_argument_mutations
|
52
52
|
children.each_with_index do |child, index|
|
53
|
-
|
53
|
+
mutate(node: child).each do |mutant|
|
54
54
|
next if invalid_argument_replacement?(mutant, index)
|
55
55
|
emit_child_update(index, mutant)
|
56
56
|
end
|
@@ -13,8 +13,11 @@ module Mutant
|
|
13
13
|
emit_optarg_body_assignments
|
14
14
|
emit_body(N_RAISE)
|
15
15
|
emit_body(N_ZSUPER)
|
16
|
-
|
17
|
-
|
16
|
+
|
17
|
+
return unless body && !ignore?(body)
|
18
|
+
|
19
|
+
emit_body(nil) unless body.children.any?(&method(:ignore?))
|
20
|
+
emit_body_mutations
|
18
21
|
end
|
19
22
|
|
20
23
|
def emit_optarg_body_assignments
|
@@ -20,14 +20,14 @@ module Mutant
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def emit_argument_presence
|
23
|
-
Util::Array::Presence.call(children).each do |children|
|
23
|
+
Util::Array::Presence.call(input: children, parent: nil).each do |children|
|
24
24
|
emit_type(*children) unless children.empty?
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
28
|
def emit_argument_mutations
|
29
29
|
children.each_with_index do |child, index|
|
30
|
-
|
30
|
+
mutate(node: child).each do |mutant|
|
31
31
|
unless forbid_argument?(mutant)
|
32
32
|
emit_child_update(index, mutant)
|
33
33
|
end
|
@@ -33,7 +33,7 @@ module Mutant
|
|
33
33
|
# Regular expressions with interpolation are skipped.
|
34
34
|
return unless (body_ast = AST::Regexp.expand_regexp_ast(input))
|
35
35
|
|
36
|
-
|
36
|
+
mutate(node: body_ast).each do |mutation|
|
37
37
|
source = AST::Regexp.to_expression(mutation).to_s
|
38
38
|
emit_type(s(:str, source), options)
|
39
39
|
end
|
@@ -34,11 +34,11 @@ module Mutant
|
|
34
34
|
def mutate_name
|
35
35
|
prefix, regexp = MAP.fetch(node.type)
|
36
36
|
stripped = name.to_s.sub(regexp, EMPTY_STRING)
|
37
|
-
|
37
|
+
|
38
|
+
Util::Symbol.call(input: stripped, parent: nil).each do |name|
|
38
39
|
emit_name(:"#{prefix}#{name}")
|
39
40
|
end
|
40
41
|
end
|
41
|
-
|
42
42
|
end # VariableAssignment
|
43
43
|
end # NamedValue
|
44
44
|
end # Node
|
@@ -8,15 +8,13 @@ module Mutant
|
|
8
8
|
class CaptureGroup < Node
|
9
9
|
handle(:regexp_capture_group)
|
10
10
|
|
11
|
-
children :group
|
12
|
-
|
13
11
|
private
|
14
12
|
|
15
13
|
def dispatch
|
16
|
-
return
|
14
|
+
return if children.empty?
|
17
15
|
|
18
|
-
emit(s(:regexp_passive_group,
|
19
|
-
|
16
|
+
emit(s(:regexp_passive_group, *children))
|
17
|
+
children.each_index(&method(:mutate_child))
|
20
18
|
end
|
21
19
|
end # EndOfLineAnchor
|
22
20
|
end # Regexp
|
@@ -8,25 +8,25 @@ module Mutant
|
|
8
8
|
class NamedGroup < Node
|
9
9
|
handle(:regexp_named_group)
|
10
10
|
|
11
|
-
children :name
|
11
|
+
children :name
|
12
12
|
|
13
13
|
private
|
14
14
|
|
15
15
|
def dispatch
|
16
|
-
return
|
16
|
+
return if remaining_children.empty?
|
17
17
|
|
18
|
-
|
18
|
+
remaining_children_indices.each(&method(:mutate_child))
|
19
19
|
|
20
20
|
# Allows unused captures to be kept and named if they are explicitly prefixed with an
|
21
21
|
# underscore, like we allow with unused local variables.
|
22
22
|
return if name_underscored?
|
23
23
|
|
24
|
-
emit(s(:regexp_passive_group,
|
24
|
+
emit(s(:regexp_passive_group, *remaining_children))
|
25
25
|
emit_name_underscore_mutation
|
26
26
|
end
|
27
27
|
|
28
28
|
def emit_name_underscore_mutation
|
29
|
-
emit_type("_#{name}",
|
29
|
+
emit_type("_#{name}", *remaining_children)
|
30
30
|
end
|
31
31
|
|
32
32
|
def name_underscored?
|
@@ -15,17 +15,7 @@ module Mutant
|
|
15
15
|
def dispatch
|
16
16
|
emit_assignment(nil)
|
17
17
|
emit_body_mutations if body
|
18
|
-
mutate_captures
|
19
18
|
end
|
20
|
-
|
21
|
-
def mutate_captures
|
22
|
-
return unless captures
|
23
|
-
Util::Array::Element.call(captures.children).each do |matchers|
|
24
|
-
next if matchers.any?(&method(:n_nil?))
|
25
|
-
emit_captures(s(:array, *matchers))
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
19
|
end # Resbody
|
30
20
|
end # Node
|
31
21
|
end # Mutator
|
data/lib/mutant/mutator/node.rb
CHANGED
@@ -10,8 +10,37 @@ module Mutant
|
|
10
10
|
include AbstractType, Unparser::Constants
|
11
11
|
include AST::NamedChildren, AST::NodePredicates, AST::Sexp, AST::Nodes
|
12
12
|
|
13
|
+
include anima.add(:config)
|
14
|
+
|
13
15
|
TAUTOLOGY = ->(_input) { true }
|
14
16
|
|
17
|
+
REGISTRY = Registry.new(->(_) { Node::Generic })
|
18
|
+
|
19
|
+
# Lookup and invoke dedicated AST mutator
|
20
|
+
#
|
21
|
+
# @param input [Parser::AST::Node]
|
22
|
+
# @param parent [nil,Mutant::Mutator::Node]
|
23
|
+
#
|
24
|
+
# @return [Set<Parser::AST::Node>]
|
25
|
+
def self.mutate(config:, node:, parent: nil)
|
26
|
+
config.ignore_patterns.each do |pattern|
|
27
|
+
return Set.new if pattern.match?(node)
|
28
|
+
end
|
29
|
+
|
30
|
+
self::REGISTRY.lookup(node.type).call(
|
31
|
+
config: config,
|
32
|
+
input: node,
|
33
|
+
parent: parent
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.handle(*types)
|
38
|
+
types.each do |type|
|
39
|
+
self::REGISTRY.register(type, self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
private_class_method :handle
|
43
|
+
|
15
44
|
# Helper to define a named child
|
16
45
|
#
|
17
46
|
# @param [Parser::AST::Node] node
|
@@ -37,9 +66,13 @@ module Mutant
|
|
37
66
|
alias_method :node, :input
|
38
67
|
alias_method :dup_node, :dup_input
|
39
68
|
|
69
|
+
def mutate(node:, parent: nil)
|
70
|
+
self.class.mutate(config: config, node: node, parent: parent)
|
71
|
+
end
|
72
|
+
|
40
73
|
def mutate_child(index, &block)
|
41
74
|
block ||= TAUTOLOGY
|
42
|
-
|
75
|
+
mutate(node: children.fetch(index), parent: self).each do |mutation|
|
43
76
|
next unless block.call(mutation)
|
44
77
|
emit_child_update(index, mutation)
|
45
78
|
end
|
@@ -96,6 +129,19 @@ module Mutant
|
|
96
129
|
end
|
97
130
|
end
|
98
131
|
|
132
|
+
def run(mutator)
|
133
|
+
mutator.call(
|
134
|
+
config: config,
|
135
|
+
input: input,
|
136
|
+
parent: nil
|
137
|
+
).each(&method(:emit))
|
138
|
+
end
|
139
|
+
|
140
|
+
def ignore?(node)
|
141
|
+
config.ignore_patterns.any? do |pattern|
|
142
|
+
pattern.match?(node)
|
143
|
+
end
|
144
|
+
end
|
99
145
|
end # Node
|
100
146
|
end # Mutator
|
101
147
|
end # Mutant
|
@@ -21,23 +21,6 @@ module Mutant
|
|
21
21
|
end
|
22
22
|
|
23
23
|
end # Presence
|
24
|
-
|
25
|
-
# Array element mutator
|
26
|
-
class Element < Util
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def dispatch
|
31
|
-
input.each_with_index do |element, index|
|
32
|
-
Mutator.mutate(element).each do |mutation|
|
33
|
-
dup = dup_input
|
34
|
-
dup[index] = mutation
|
35
|
-
emit(dup)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
end # Element
|
41
24
|
end # Array
|
42
25
|
end # Util
|
43
26
|
end # Mutator
|