evilution 1.2.0 → 1.4.0
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/.beads/interactions.jsonl +100 -0
- data/.rubocop_todo.yml +14 -0
- data/CHANGELOG.md +124 -0
- data/README.md +142 -28
- data/docs/architecture.md +123 -9
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +39 -2
- data/docs/migration-from-mutant.md +1 -1
- data/lib/evilution/ast/aasm_declaration.rb +133 -0
- data/lib/evilution/ast/callback_declaration.rb +91 -0
- data/lib/evilution/ast/included_block.rb +27 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/parser.rb +120 -16
- data/lib/evilution/ast/pattern/filter.rb +1 -0
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +15 -15
- data/lib/evilution/ast/pattern.rb +2 -0
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
- data/lib/evilution/ast/uncovered_code.rb +88 -0
- data/lib/evilution/ast/value_object_definition.rb +25 -0
- data/lib/evilution/baseline/failure_formatter.rb +27 -0
- data/lib/evilution/baseline/report.rb +66 -0
- data/lib/evilution/baseline/spec_failure.rb +38 -0
- data/lib/evilution/baseline.rb +101 -42
- data/lib/evilution/child_output.rb +1 -1
- data/lib/evilution/cli/command.rb +1 -0
- data/lib/evilution/cli/commands/tests_list.rb +4 -3
- data/lib/evilution/cli/commands.rb +2 -0
- data/lib/evilution/cli/dispatcher.rb +2 -0
- data/lib/evilution/cli/exit_guard.rb +2 -0
- data/lib/evilution/cli/parsed_args.rb +2 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -0
- data/lib/evilution/cli/parser/file_args.rb +4 -1
- data/lib/evilution/cli/parser/options_builder.rb +4 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
- data/lib/evilution/cli/parser.rb +1 -0
- data/lib/evilution/cli/printers/tests_list.rb +5 -4
- data/lib/evilution/cli/printers.rb +2 -0
- data/lib/evilution/cli/result.rb +2 -0
- data/lib/evilution/cli.rb +8 -2
- data/lib/evilution/config/builders.rb +2 -0
- data/lib/evilution/config/env_loader.rb +2 -0
- data/lib/evilution/config/file_loader.rb +1 -0
- data/lib/evilution/config/sources.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
- data/lib/evilution/config/validators/fail_fast.rb +1 -0
- data/lib/evilution/config/validators/hooks.rb +1 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
- data/lib/evilution/config/validators/integration.rb +1 -0
- data/lib/evilution/config/validators/isolation.rb +1 -0
- data/lib/evilution/config/validators/jobs.rb +1 -0
- data/lib/evilution/config/validators/preload.rb +1 -0
- data/lib/evilution/config/validators/profile.rb +1 -0
- data/lib/evilution/config/validators/spec_mappings.rb +3 -1
- data/lib/evilution/config/validators/spec_pattern.rb +1 -0
- data/lib/evilution/config/validators/warmup.rb +17 -0
- data/lib/evilution/config/validators.rb +2 -0
- data/lib/evilution/config.rb +7 -5
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +1 -1
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
- data/lib/evilution/example_filter.rb +46 -1
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +2 -0
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +139 -12
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/loading/test_load_path.rb +21 -13
- data/lib/evilution/integration/minitest/test_ids.rb +21 -0
- data/lib/evilution/integration/minitest.rb +45 -6
- data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
- data/lib/evilution/integration/rspec/example_ids.rb +35 -0
- data/lib/evilution/integration/rspec/result_builder.rb +6 -1
- data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard.rb +10 -1
- data/lib/evilution/integration/rspec.rb +22 -4
- data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
- data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
- data/lib/evilution/integration/test_unit.rb +37 -9
- data/lib/evilution/isolation/fork.rb +2 -1
- data/lib/evilution/isolation/in_process.rb +21 -4
- data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
- data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
- data/lib/evilution/memory/leak_check.rb +27 -3
- data/lib/evilution/mutation.rb +8 -3
- data/lib/evilution/mutator/base.rb +49 -5
- data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
- data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
- data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
- data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
- data/lib/evilution/mutator/operator/bang_method.rb +11 -3
- data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
- data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
- data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
- data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
- data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
- data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
- data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
- data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
- data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
- data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
- data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
- data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
- data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
- data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
- data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
- data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
- data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
- data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
- data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
- data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
- data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
- data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
- data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
- data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
- data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
- data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
- data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
- data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
- data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +44 -6
- data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +54 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/rails_warmup.rb +61 -0
- data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/html/report.rb +1 -0
- data/lib/evilution/reporter/json/baseline.rb +15 -0
- data/lib/evilution/reporter/json.rb +7 -2
- data/lib/evilution/result/baseline_neutralization.rb +10 -0
- data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/subject_scorer.rb +1 -0
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +26 -11
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +35 -14
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
- data/lib/evilution/runner/mutation_executor.rb +1 -1
- data/lib/evilution/runner/mutation_planner.rb +11 -2
- data/lib/evilution/runner/report_publisher.rb +3 -2
- data/lib/evilution/runner/subject_pipeline.rb +46 -1
- data/lib/evilution/runner.rb +4 -1
- data/lib/evilution/source_ast_cache.rb +1 -1
- data/lib/evilution/spec_ast_cache.rb +1 -1
- data/lib/evilution/subject.rb +8 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +51 -0
- data/script/memory_check +62 -16
- metadata +108 -4
|
@@ -9,6 +9,22 @@ require_relative "../result/mutation_result"
|
|
|
9
9
|
require_relative "../isolation"
|
|
10
10
|
|
|
11
11
|
class Evilution::Isolation::InProcess
|
|
12
|
+
# Raised into the run when its time is up. A SignalException, because the
|
|
13
|
+
# timeout has to end the whole test run and a test framework rescues nearly
|
|
14
|
+
# everything a test raises, Timeout::Error included: it would record one
|
|
15
|
+
# failed test and go on to the next, which may reach the same endless loop
|
|
16
|
+
# with no timer left to stop it. Minitest, Test::Unit and RSpec all let a
|
|
17
|
+
# SignalException through.
|
|
18
|
+
#
|
|
19
|
+
# Not an Interrupt, though it is one of those: tooling that handles the
|
|
20
|
+
# user's Ctrl-C (maxitest) rescues Interrupt and skips every test from then
|
|
21
|
+
# on, for the rest of the process.
|
|
22
|
+
class Expired < SignalException
|
|
23
|
+
def initialize(_message = nil)
|
|
24
|
+
super("SIGALRM")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
12
28
|
@null_out = File.open(File::NULL, "w")
|
|
13
29
|
@null_err = File.open(File::NULL, "w")
|
|
14
30
|
|
|
@@ -33,7 +49,7 @@ class Evilution::Isolation::InProcess
|
|
|
33
49
|
private
|
|
34
50
|
|
|
35
51
|
# The Dir.chdir block is inside the Timeout.timeout block so that a
|
|
36
|
-
#
|
|
52
|
+
# timeout raised mid-call still unwinds through Dir.chdir's ensure
|
|
37
53
|
# and restores the parent CWD before the rescue clause runs. The sandbox
|
|
38
54
|
# contains any relative-path writes from path-relativizing mutations
|
|
39
55
|
# (EV-wqxu / GH #1278). Evilution.with_isolated_worker signals the rest of
|
|
@@ -41,7 +57,7 @@ class Evilution::Isolation::InProcess
|
|
|
41
57
|
# SourceEvaluator/Integration) to anchor project-relative paths to
|
|
42
58
|
# PROJECT_ROOT for the duration of the call.
|
|
43
59
|
def execute_with_timeout(mutation, test_command, timeout, sandbox_dir)
|
|
44
|
-
result = Timeout.timeout(timeout) do
|
|
60
|
+
result = Timeout.timeout(timeout, Expired) do
|
|
45
61
|
Evilution.with_isolated_worker do
|
|
46
62
|
Dir.chdir(sandbox_dir) do
|
|
47
63
|
suppress_output { test_command.call(mutation) }
|
|
@@ -49,7 +65,7 @@ class Evilution::Isolation::InProcess
|
|
|
49
65
|
end
|
|
50
66
|
end
|
|
51
67
|
{ timeout: false }.merge(result)
|
|
52
|
-
rescue
|
|
68
|
+
rescue Expired
|
|
53
69
|
{ timeout: true }
|
|
54
70
|
rescue ScriptError, StandardError => e
|
|
55
71
|
{
|
|
@@ -106,7 +122,8 @@ class Evilution::Isolation::InProcess
|
|
|
106
122
|
message: result[:error],
|
|
107
123
|
klass: result[:error_class],
|
|
108
124
|
backtrace: result[:error_backtrace]
|
|
109
|
-
)
|
|
125
|
+
),
|
|
126
|
+
known_failures_only: result[:known_failures_only]
|
|
110
127
|
)
|
|
111
128
|
end
|
|
112
129
|
end
|
|
@@ -32,8 +32,8 @@ module Evilution::MCP::InfoTool::StatusGlossary
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"status" => "neutral",
|
|
35
|
-
"meaning" => "
|
|
36
|
-
"
|
|
35
|
+
"meaning" => "No verdict: the tests failed only on examples that were already failing before " \
|
|
36
|
+
"the mutation was applied, or the test process crashed on infrastructure. " \
|
|
37
37
|
"Not a meaningful mutation signal.",
|
|
38
38
|
"counted_in_score" => false
|
|
39
39
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "../mutate_tool"
|
|
5
5
|
require_relative "../../reporter/suggestion"
|
|
6
|
+
require_relative "../../diagnostic"
|
|
6
7
|
|
|
7
8
|
module Evilution::MCP::MutateTool::ProgressStreamer
|
|
8
9
|
def self.build(server_context:, suggest_tests:, integration:)
|
|
@@ -21,7 +22,7 @@ module Evilution::MCP::MutateTool::ProgressStreamer
|
|
|
21
22
|
detail = build_suggestion_detail(result.mutation, suggestion)
|
|
22
23
|
server_context.report_progress(survivor_index, message: ::JSON.generate(detail))
|
|
23
24
|
rescue StandardError => e
|
|
24
|
-
warn
|
|
25
|
+
Evilution::Diagnostic.warn("[evilution] progress stream disabled after error: #{e.class}: #{e.message}")
|
|
25
26
|
disabled = true
|
|
26
27
|
end
|
|
27
28
|
end
|
|
@@ -9,9 +9,11 @@ class Evilution::Memory::LeakCheck
|
|
|
9
9
|
|
|
10
10
|
attr_reader :samples
|
|
11
11
|
|
|
12
|
-
def initialize(iterations: DEFAULT_ITERATIONS, max_growth_kb: DEFAULT_MAX_GROWTH_KB
|
|
12
|
+
def initialize(iterations: DEFAULT_ITERATIONS, max_growth_kb: DEFAULT_MAX_GROWTH_KB,
|
|
13
|
+
warmup_iterations: WARMUP_ITERATIONS)
|
|
13
14
|
@iterations = iterations
|
|
14
15
|
@max_growth_kb = max_growth_kb
|
|
16
|
+
@warmup_iterations = warmup_iterations
|
|
15
17
|
@samples = []
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -32,8 +34,23 @@ class Evilution::Memory::LeakCheck
|
|
|
32
34
|
samples.last - samples.first
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
# Growth from the first sample to the last, less the largest rise between two
|
|
38
|
+
# consecutive samples.
|
|
39
|
+
#
|
|
40
|
+
# RSS does not grow by the byte. The allocator takes memory in chunks, so a
|
|
41
|
+
# workload that has stopped growing can still step up once, by several MB,
|
|
42
|
+
# wherever the heap happens to cross a boundary, and that one step decided
|
|
43
|
+
# the endpoint reading. A leak is not one step: it grows across the run, and
|
|
44
|
+
# taking out its largest step leaves the rest of it.
|
|
45
|
+
def sustained_growth_kb
|
|
36
46
|
kb = growth_kb
|
|
47
|
+
return kb if kb.nil? || samples.size < 2
|
|
48
|
+
|
|
49
|
+
kb - [largest_step_kb, 0].max
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def passed?
|
|
53
|
+
kb = sustained_growth_kb
|
|
37
54
|
return false if kb.nil?
|
|
38
55
|
|
|
39
56
|
kb <= @max_growth_kb
|
|
@@ -41,8 +58,12 @@ class Evilution::Memory::LeakCheck
|
|
|
41
58
|
|
|
42
59
|
private
|
|
43
60
|
|
|
61
|
+
def largest_step_kb
|
|
62
|
+
samples.each_cons(2).map { |before, after| after - before }.max
|
|
63
|
+
end
|
|
64
|
+
|
|
44
65
|
def warmup(&block)
|
|
45
|
-
|
|
66
|
+
@warmup_iterations.times { block.call }
|
|
46
67
|
GC.start
|
|
47
68
|
GC.compact if GC.respond_to?(:compact)
|
|
48
69
|
end
|
|
@@ -74,10 +95,13 @@ class Evilution::Memory::LeakCheck
|
|
|
74
95
|
end
|
|
75
96
|
|
|
76
97
|
def result
|
|
98
|
+
sustained_kb = sustained_growth_kb
|
|
77
99
|
{
|
|
78
100
|
passed: passed?,
|
|
79
101
|
growth_kb: growth_kb,
|
|
80
102
|
growth_mb: growth_kb ? growth_kb / 1024.0 : nil,
|
|
103
|
+
sustained_growth_kb: sustained_kb,
|
|
104
|
+
sustained_growth_mb: sustained_kb ? sustained_kb / 1024.0 : nil,
|
|
81
105
|
samples: samples,
|
|
82
106
|
iterations: @iterations,
|
|
83
107
|
max_growth_kb: @max_growth_kb,
|
data/lib/evilution/mutation.rb
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "diff/lcs"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "version"
|
|
5
5
|
|
|
6
6
|
class Evilution::Mutation
|
|
7
7
|
Sources = Data.define(:original, :mutated)
|
|
8
8
|
Slice = Data.define(:original, :mutated)
|
|
9
9
|
Location = Data.define(:file_path, :line, :column)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# restore_source: the source that puts back what applying this mutation
|
|
12
|
+
# changed and re-evaluating another mutation of the file would not -- a
|
|
13
|
+
# scope declaration (see Mutator::Base#build_restore_source). nil when
|
|
14
|
+
# nothing needs restoring.
|
|
15
|
+
attr_reader :subject, :operator_name, :parse_status, :location, :restore_source
|
|
12
16
|
|
|
13
17
|
def initialize(subject:, operator_name:, sources:, location:,
|
|
14
|
-
slice: nil, parse_status: :ok, eval_source: nil)
|
|
18
|
+
slice: nil, parse_status: :ok, eval_source: nil, restore_source: nil)
|
|
15
19
|
@subject = subject
|
|
16
20
|
@operator_name = operator_name
|
|
17
21
|
@sources = sources
|
|
@@ -19,6 +23,7 @@ class Evilution::Mutation
|
|
|
19
23
|
@slice = slice
|
|
20
24
|
@parse_status = parse_status
|
|
21
25
|
@eval_source = eval_source
|
|
26
|
+
@restore_source = restore_source
|
|
22
27
|
@diff = nil
|
|
23
28
|
end
|
|
24
29
|
|
|
@@ -84,12 +84,12 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
84
84
|
replacement_bytesize: replacement.bytesize
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
mutation = build_mutation_record(node, surgery, slices)
|
|
87
|
+
mutation = build_mutation_record(node, surgery, slices, offset)
|
|
88
88
|
@mutations << mutation
|
|
89
89
|
mutation
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
def build_mutation_record(node, surgery, slices)
|
|
92
|
+
def build_mutation_record(node, surgery, slices, offset)
|
|
93
93
|
Evilution::Mutation.new(
|
|
94
94
|
subject: @subject,
|
|
95
95
|
operator_name: self.class.operator_name,
|
|
@@ -101,7 +101,8 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
101
101
|
column: node.location.start_column
|
|
102
102
|
),
|
|
103
103
|
parse_status: surgery.status,
|
|
104
|
-
eval_source: build_eval_source(surgery)
|
|
104
|
+
eval_source: build_eval_source(surgery, offset),
|
|
105
|
+
restore_source: build_restore_source(offset)
|
|
105
106
|
)
|
|
106
107
|
end
|
|
107
108
|
|
|
@@ -112,10 +113,44 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
112
113
|
# them at apply time. Passing the subject's file path lets the neutralizer
|
|
113
114
|
# skip files the parent never preloaded — those are lazy plugin files whose
|
|
114
115
|
# DSL calls are still needed for the child fork's first-time load.
|
|
115
|
-
|
|
116
|
+
# A scope or aasm subject's mutation sits inside a class-body declaration,
|
|
117
|
+
# which must run again for the mutated body to replace the loaded one, so
|
|
118
|
+
# that one declaration is kept.
|
|
119
|
+
def build_eval_source(surgery, offset)
|
|
116
120
|
return surgery.source unless surgery.ok?
|
|
117
121
|
|
|
118
|
-
@body_call_neutralizer.call(surgery.source, file_path: @subject.file_path)
|
|
122
|
+
@body_call_neutralizer.call(surgery.source, file_path: @subject.file_path, **redeclaration(offset))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Re-evaluating any mutation of a file redefines all its methods, which is
|
|
126
|
+
# how an in-process run puts a mutated method back. Class-body calls are
|
|
127
|
+
# neutralized in that source, so a mutated scope or aasm callable would stay
|
|
128
|
+
# installed; the original file with just that declaration kept replaces it
|
|
129
|
+
# again.
|
|
130
|
+
def build_restore_source(offset)
|
|
131
|
+
return nil unless redeclared_subject?
|
|
132
|
+
|
|
133
|
+
@body_call_neutralizer.call(@file_source, file_path: @subject.file_path, **redeclaration(offset))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Subjects whose body is written inside a class-body declaration and only
|
|
137
|
+
# takes effect when that declaration runs again.
|
|
138
|
+
REDECLARED_KINDS = %i[scope aasm callback].freeze
|
|
139
|
+
private_constant :REDECLARED_KINDS
|
|
140
|
+
|
|
141
|
+
def redeclared_subject?
|
|
142
|
+
REDECLARED_KINDS.include?(@subject.kind)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# What the neutralizer needs to keep a redeclared subject's declaration:
|
|
146
|
+
# where the mutation is, and the lines the declaration takes up in the
|
|
147
|
+
# file as it was loaded -- which is where the callbacks it registered then
|
|
148
|
+
# say they come from, whatever the mutation does to its length.
|
|
149
|
+
def redeclaration(offset)
|
|
150
|
+
return {} unless redeclared_subject?
|
|
151
|
+
|
|
152
|
+
first = @subject.line_number
|
|
153
|
+
{ keep_offset: offset, keep_lines: first..(first + @subject.source.count("\n")) }
|
|
119
154
|
end
|
|
120
155
|
|
|
121
156
|
NEWLINE_BYTE = 10
|
|
@@ -157,6 +192,15 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
157
192
|
.downcase
|
|
158
193
|
end
|
|
159
194
|
|
|
195
|
+
# The kinds of Subject the operator mutates. Operators work on method
|
|
196
|
+
# bodies, and on the bodies of callables handed to a scope, aasm or
|
|
197
|
+
# callback declaration, which run as part of the class's behaviour; one that also has
|
|
198
|
+
# something to say about a constant subject (a definition outside any
|
|
199
|
+
# method) adds :constant.
|
|
200
|
+
def self.subject_kinds
|
|
201
|
+
%i[method scope aasm callback]
|
|
202
|
+
end
|
|
203
|
+
|
|
160
204
|
@parse_cache = {}
|
|
161
205
|
|
|
162
206
|
def self.parsed_tree_for(file_path, file_source)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
|
|
7
|
+
# Drop an alias declaration from a class or module body: `alias length size`
|
|
8
|
+
# and `alias_method :count, :size` are removed.
|
|
9
|
+
#
|
|
10
|
+
# A survivor means no example calls the method by its alias, so the alias is
|
|
11
|
+
# a surface the suite never exercises — dead, or reached only where nothing
|
|
12
|
+
# asserts.
|
|
13
|
+
#
|
|
14
|
+
# Declarations sit in the class body, outside every method, where no subject
|
|
15
|
+
# reaches them. They are attributed to the first method of the innermost
|
|
16
|
+
# enclosing class, module or `class << self`, the way DataStructMember and
|
|
17
|
+
# MixinRemoval attribute theirs. An `alias_method` call inside a method is
|
|
18
|
+
# left to the generic call operators, and global-variable aliases
|
|
19
|
+
# (`alias $new $old`) are not method surface.
|
|
20
|
+
class Evilution::Mutator::Operator::AliasRemoval < Evilution::Mutator::Base
|
|
21
|
+
def call(subject, filter: nil)
|
|
22
|
+
@subject = subject
|
|
23
|
+
@file_source = File.read(subject.file_path)
|
|
24
|
+
@mutations = []
|
|
25
|
+
@filter = filter
|
|
26
|
+
|
|
27
|
+
declarations_for(subject).each { |declaration| remove(declaration) }
|
|
28
|
+
@mutations
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def remove(declaration)
|
|
34
|
+
add_mutation(
|
|
35
|
+
offset: declaration.location.start_offset,
|
|
36
|
+
length: declaration.location.length,
|
|
37
|
+
replacement: "",
|
|
38
|
+
node: declaration
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def declarations_for(subject)
|
|
43
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
44
|
+
collector = DeclarationCollector.new
|
|
45
|
+
collector.visit(tree)
|
|
46
|
+
|
|
47
|
+
collector.declarations.filter_map do |declaration, scope|
|
|
48
|
+
declaration if first_method_line(scope) == subject.line_number
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def first_method_line(scope)
|
|
53
|
+
finder = FirstMethodFinder.new
|
|
54
|
+
scope.compact_child_nodes.each { |child| finder.visit(child) }
|
|
55
|
+
finder.line
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Collects alias declarations written in a class, module or singleton class
|
|
59
|
+
# body, each paired with the innermost such scope. Method bodies are not
|
|
60
|
+
# searched, and a top-level alias has no scope to attribute it to.
|
|
61
|
+
class DeclarationCollector < Prism::Visitor
|
|
62
|
+
attr_reader :declarations
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
super
|
|
66
|
+
@declarations = []
|
|
67
|
+
@scopes = []
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def visit_class_node(node)
|
|
71
|
+
within(node) { super }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def visit_module_node(node)
|
|
75
|
+
within(node) { super }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def visit_singleton_class_node(node)
|
|
79
|
+
within(node) { super }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visit_def_node(_node); end
|
|
83
|
+
|
|
84
|
+
def visit_alias_method_node(node)
|
|
85
|
+
record(node)
|
|
86
|
+
super
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Blocks in the body (`included do ... end`) are searched too: an alias
|
|
90
|
+
# there still belongs to the enclosing scope.
|
|
91
|
+
def visit_call_node(node)
|
|
92
|
+
record(node) if node.name == :alias_method && node.receiver.nil?
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def record(node)
|
|
99
|
+
@declarations << [node, @scopes.last] unless @scopes.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def within(scope)
|
|
103
|
+
@scopes.push(scope)
|
|
104
|
+
yield
|
|
105
|
+
ensure
|
|
106
|
+
@scopes.pop
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Finds the line of the first method written in a scope. Nested classes,
|
|
111
|
+
# modules and singleton classes are not searched: their methods are subjects
|
|
112
|
+
# of that inner scope.
|
|
113
|
+
class FirstMethodFinder < Prism::Visitor
|
|
114
|
+
attr_reader :line
|
|
115
|
+
|
|
116
|
+
def visit_class_node(_node); end
|
|
117
|
+
|
|
118
|
+
def visit_module_node(_node); end
|
|
119
|
+
|
|
120
|
+
def visit_singleton_class_node(_node); end
|
|
121
|
+
|
|
122
|
+
def visit_def_node(node)
|
|
123
|
+
@line = node.location.start_line if @line.nil?
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Drop a call's whole argument list: `combine(a, b)` becomes `combine`,
|
|
6
|
+
# `normalize(value)` becomes `normalize`. Every argument kind goes —
|
|
7
|
+
# positional, splat, keywords and `...` — while a block-pass stays attached
|
|
8
|
+
# (`each_slice(2, &block)` becomes `each_slice(&block)`) and so does a
|
|
9
|
+
# literal block.
|
|
10
|
+
#
|
|
11
|
+
# A kill by ArgumentError proves arity is asserted; a survivor means the
|
|
12
|
+
# arguments never mattered. argument_removal drops one argument at a time and
|
|
13
|
+
# only from calls with two or more.
|
|
14
|
+
#
|
|
15
|
+
# Operator methods (`a + b`, `a[i]`) are left to binary operand promotion and
|
|
16
|
+
# attribute writes to attribute_write_to_read.
|
|
17
|
+
class Evilution::Mutator::Operator::ArgumentListRemoval < Evilution::Mutator::Base
|
|
18
|
+
IDENTIFIER = /\A[[:alpha:]_]/
|
|
19
|
+
private_constant :IDENTIFIER
|
|
20
|
+
|
|
21
|
+
def visit_call_node(node)
|
|
22
|
+
remove_argument_list(node) if removable?(node)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def removable?(node)
|
|
29
|
+
node.arguments && node.message_loc && node.name.match?(IDENTIFIER) && !node.attribute_write?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def remove_argument_list(node)
|
|
33
|
+
block_pass = node.block if node.block.is_a?(Prism::BlockArgumentNode)
|
|
34
|
+
start = node.message_loc.end_offset
|
|
35
|
+
|
|
36
|
+
add_mutation(
|
|
37
|
+
offset: start,
|
|
38
|
+
length: argument_list_end(node, block_pass) - start,
|
|
39
|
+
replacement: block_pass ? "(#{block_pass.slice})" : "",
|
|
40
|
+
node: node
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Without parentheses the list ends at its last element: the block-pass
|
|
45
|
+
# when there is one (it always comes last), else the last argument. The
|
|
46
|
+
# call node itself would reach past a literal `do ... end` block.
|
|
47
|
+
def argument_list_end(node, block_pass)
|
|
48
|
+
return node.closing_loc.end_offset if node.closing_loc
|
|
49
|
+
|
|
50
|
+
(block_pass || node.arguments).end_offset
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap neighbouring positional arguments: `compute(a, b)` becomes
|
|
6
|
+
# `compute(b, a)`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example tells the two values apart — typically a test
|
|
9
|
+
# whose fixtures are symmetric, so the method could take them in either order
|
|
10
|
+
# unnoticed.
|
|
11
|
+
#
|
|
12
|
+
# Keyword arguments and a block argument stay where they are, and a splat
|
|
13
|
+
# keeps its neighbours in place: it stands for an unknown number of
|
|
14
|
+
# arguments, so a value swapped with it would not land where the original
|
|
15
|
+
# stood. Identical neighbours are not swapped, since that would reproduce the
|
|
16
|
+
# original call.
|
|
17
|
+
class Evilution::Mutator::Operator::ArgumentOrderPermutation < Evilution::Mutator::Base
|
|
18
|
+
# `raise ArgumentError, "message"` reversed raises TypeError wherever it is
|
|
19
|
+
# reached, which only shows that the line ran.
|
|
20
|
+
RAISING_METHODS = %i[raise fail].freeze
|
|
21
|
+
|
|
22
|
+
# The format string swapped with a value raises TypeError the same way;
|
|
23
|
+
# what the string renders is FormatSpecifierSwap's job.
|
|
24
|
+
FORMAT_METHODS = %i[format sprintf printf].freeze
|
|
25
|
+
|
|
26
|
+
# Core methods that treat their arguments as an unordered set of
|
|
27
|
+
# alternatives, so any order gives the same answer.
|
|
28
|
+
ORDER_FREE_METHODS = %i[start_with? end_with?].freeze
|
|
29
|
+
|
|
30
|
+
# Arguments whose position does not map to a single value.
|
|
31
|
+
POSITIONLESS_TYPES = [Prism::SplatNode, Prism::ForwardingArgumentsNode].freeze
|
|
32
|
+
|
|
33
|
+
def visit_call_node(node)
|
|
34
|
+
swap_arguments(node) if swappable_call?(node)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_super_node(node)
|
|
39
|
+
swap_arguments(node)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def visit_yield_node(node)
|
|
44
|
+
swap_arguments(node)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# The last argument of an index write is the value being stored, not a peer
|
|
51
|
+
# of the index.
|
|
52
|
+
def swappable_call?(node)
|
|
53
|
+
return false if node.name == :[]=
|
|
54
|
+
return false if ORDER_FREE_METHODS.include?(node.name)
|
|
55
|
+
return false if set_literal?(node)
|
|
56
|
+
|
|
57
|
+
!(node.receiver.nil? && (RAISING_METHODS.include?(node.name) || FORMAT_METHODS.include?(node.name)))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `Set[a, b]` builds a set, where the order of elements does not count.
|
|
61
|
+
def set_literal?(node)
|
|
62
|
+
return false unless node.name == :[]
|
|
63
|
+
|
|
64
|
+
receiver = node.receiver
|
|
65
|
+
case receiver
|
|
66
|
+
when Prism::ConstantReadNode then receiver.name == :Set
|
|
67
|
+
when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Set
|
|
68
|
+
else false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def swap_arguments(node)
|
|
73
|
+
return if node.arguments.nil?
|
|
74
|
+
|
|
75
|
+
positionals = node.arguments.arguments.grep_v(Prism::KeywordHashNode)
|
|
76
|
+
positionals.each_cons(2) do |left, right|
|
|
77
|
+
emit_swap(node, left, right) if swappable_pair?(left, right)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def swappable_pair?(left, right)
|
|
82
|
+
return false if [left, right].any? { |argument| POSITIONLESS_TYPES.include?(argument.class) }
|
|
83
|
+
|
|
84
|
+
left.slice != right.slice
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Whatever separates the two arguments — the comma, line breaks, comments —
|
|
88
|
+
# stays between them, so the call keeps its layout.
|
|
89
|
+
def emit_swap(node, left, right)
|
|
90
|
+
start_offset = left.location.start_offset
|
|
91
|
+
end_offset = right.location.end_offset
|
|
92
|
+
separator = byteslice_source(left.location.end_offset, right.location.start_offset - left.location.end_offset)
|
|
93
|
+
|
|
94
|
+
add_mutation(
|
|
95
|
+
offset: start_offset,
|
|
96
|
+
length: end_offset - start_offset,
|
|
97
|
+
replacement: "#{right.slice}#{separator}#{left.slice}",
|
|
98
|
+
node: node
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a call with its only argument: `normalize(value)` becomes `value`.
|
|
6
|
+
#
|
|
7
|
+
# A survivor means the method's transformation of its input is never
|
|
8
|
+
# observed — the tests would pass if the value went through untouched.
|
|
9
|
+
# Distinct from method_call_removal, which keeps the receiver instead.
|
|
10
|
+
#
|
|
11
|
+
# Only identifier-named methods with a single positional argument qualify.
|
|
12
|
+
# Operator methods (`a + b`, `a[i]`) are left to binary operand promotion,
|
|
13
|
+
# attribute writes to attribute_write_to_read, and a call in void statement
|
|
14
|
+
# position to statement_deletion, since there the promoted argument's value
|
|
15
|
+
# is discarded anyway.
|
|
16
|
+
class Evilution::Mutator::Operator::ArgumentPropagation < Evilution::Mutator::Base
|
|
17
|
+
NON_POSITIONAL = [
|
|
18
|
+
Prism::SplatNode,
|
|
19
|
+
Prism::KeywordHashNode,
|
|
20
|
+
Prism::BlockArgumentNode,
|
|
21
|
+
Prism::ForwardingArgumentsNode
|
|
22
|
+
].freeze
|
|
23
|
+
private_constant :NON_POSITIONAL
|
|
24
|
+
|
|
25
|
+
IDENTIFIER = /\A[[:alpha:]_]/
|
|
26
|
+
private_constant :IDENTIFIER
|
|
27
|
+
|
|
28
|
+
def call(subject, **)
|
|
29
|
+
@void_statements = Set.new
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_statements_node(node)
|
|
34
|
+
@void_statements.merge(node.body[...-1])
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_call_node(node)
|
|
39
|
+
promote_child(node, sole_positional_argument(node)) if propagatable?(node)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def sole_positional_argument(node)
|
|
46
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [argument]]
|
|
47
|
+
|
|
48
|
+
argument unless NON_POSITIONAL.any? { |type| argument.is_a?(type) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def propagatable?(node)
|
|
52
|
+
node.name.match?(IDENTIFIER) && !node.attribute_write? && !@void_statements.include?(node)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../operator"
|
|
4
|
+
require_relative "../../ast/value_object_definition"
|
|
4
5
|
|
|
5
6
|
class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
6
7
|
SKIP_TYPES = [
|
|
@@ -30,7 +31,14 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def mutable?(node, args)
|
|
33
|
-
args && args.length >= 2 && positional_only?(args) && node.name != :[]=
|
|
34
|
+
args && args.length >= 2 && positional_only?(args) && node.name != :[]= && !member_list?(node)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# DataStructMember owns the member list of `Data.define` / `Struct.new` and
|
|
38
|
+
# drops each member itself; removing an argument here would emit the same
|
|
39
|
+
# mutant a second time.
|
|
40
|
+
def member_list?(node)
|
|
41
|
+
Evilution::AST::ValueObjectDefinition.match?(node)
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
def positional_only?(args)
|