evilution 0.28.0 → 0.29.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +52 -0
  3. data/CHANGELOG.md +7 -0
  4. data/lib/evilution/ast/constant_names.rb +28 -11
  5. data/lib/evilution/ast/pattern/parser.rb +29 -17
  6. data/lib/evilution/cli/commands/session_diff.rb +6 -4
  7. data/lib/evilution/cli/commands/subjects.rb +6 -3
  8. data/lib/evilution/cli/commands/util_mutation.rb +24 -19
  9. data/lib/evilution/cli/parser/command_extractor.rb +9 -11
  10. data/lib/evilution/cli/parser/file_args.rb +3 -1
  11. data/lib/evilution/cli/parser/options_builder.rb +29 -1
  12. data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
  13. data/lib/evilution/cli/parser.rb +18 -20
  14. data/lib/evilution/cli/printers/environment.rb +19 -19
  15. data/lib/evilution/cli/printers/session_diff.rb +8 -8
  16. data/lib/evilution/compare/normalizer.rb +10 -5
  17. data/lib/evilution/config.rb +10 -10
  18. data/lib/evilution/disable_comment.rb +21 -12
  19. data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
  20. data/lib/evilution/integration/minitest.rb +25 -16
  21. data/lib/evilution/integration/rspec.rb +4 -0
  22. data/lib/evilution/isolation/fork.rb +27 -17
  23. data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
  24. data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
  25. data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
  26. data/lib/evilution/mcp/info_tool.rb +7 -1
  27. data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
  28. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
  29. data/lib/evilution/mcp/mutate_tool.rb +27 -14
  30. data/lib/evilution/mcp/session_tool.rb +27 -18
  31. data/lib/evilution/mutation.rb +13 -15
  32. data/lib/evilution/mutator/base.rb +17 -15
  33. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
  34. data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
  35. data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
  36. data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
  37. data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
  38. data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
  39. data/lib/evilution/mutator/operator/case_when.rb +7 -5
  40. data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
  41. data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
  42. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
  43. data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
  44. data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
  45. data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
  46. data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
  47. data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
  48. data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
  49. data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
  50. data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
  51. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
  52. data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
  53. data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
  54. data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
  55. data/lib/evilution/parallel/work_queue/worker.rb +10 -7
  56. data/lib/evilution/parallel/work_queue.rb +35 -18
  57. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
  58. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
  59. data/lib/evilution/reporter/json.rb +52 -18
  60. data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
  61. data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
  62. data/lib/evilution/reporter/suggestion/templates/minitest.rb +20 -14
  63. data/lib/evilution/reporter/suggestion/templates/rspec.rb +19 -13
  64. data/lib/evilution/runner/baseline_runner.rb +15 -8
  65. data/lib/evilution/runner/diagnostics.rb +13 -9
  66. data/lib/evilution/runner/isolation_resolver.rb +11 -9
  67. data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -1
  68. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +32 -10
  69. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +1 -1
  70. data/lib/evilution/runner/mutation_executor.rb +2 -0
  71. data/lib/evilution/runner/mutation_planner.rb +37 -17
  72. data/lib/evilution/runner/subject_pipeline.rb +21 -11
  73. data/lib/evilution/runner.rb +3 -3
  74. data/lib/evilution/session/diff.rb +15 -6
  75. data/lib/evilution/spec_ast_cache.rb +26 -12
  76. data/lib/evilution/version.rb +1 -1
  77. data/script/memory_check +11 -5
  78. data/scripts/benchmark_density +10 -9
  79. data/scripts/compare_mutations +38 -21
  80. data/scripts/mutant_json_adapter +7 -4
  81. metadata +3 -2
@@ -74,22 +74,17 @@ class Evilution::Mutation
74
74
  private
75
75
 
76
76
  def compute_diff
77
- original_lines = original_source.lines
78
- mutated_lines = mutated_source.lines
79
- diffs = ::Diff::LCS.diff(original_lines, mutated_lines)
80
-
77
+ diffs = ::Diff::LCS.diff(original_source.lines, mutated_source.lines)
81
78
  return "" if diffs.empty?
82
79
 
83
- result = []
84
- diffs.flatten(1).each do |change|
85
- case change.action
86
- when "-"
87
- result << "- #{change.element.chomp}"
88
- when "+"
89
- result << "+ #{change.element.chomp}"
90
- end
80
+ diffs.flatten(1).filter_map { |change| format_diff_change(change) }.join("\n")
81
+ end
82
+
83
+ def format_diff_change(change)
84
+ case change.action
85
+ when "-" then "- #{change.element.chomp}"
86
+ when "+" then "+ #{change.element.chomp}"
91
87
  end
92
- result.join("\n")
93
88
  end
94
89
 
95
90
  def compute_unified_diff
@@ -97,15 +92,18 @@ class Evilution::Mutation
97
92
 
98
93
  original_lines = @slice.original.lines
99
94
  mutated_lines = @slice.mutated.lines
100
- body = ::Diff::LCS.sdiff(original_lines, mutated_lines).map { |c| format_sdiff_change(c) }.join("\n")
101
95
  [
102
96
  "--- a/#{file_path}",
103
97
  "+++ b/#{file_path}",
104
98
  "@@ -#{line},#{original_lines.length} +#{line},#{mutated_lines.length} @@",
105
- body
99
+ unified_diff_body(original_lines, mutated_lines)
106
100
  ].reject(&:empty?).join("\n")
107
101
  end
108
102
 
103
+ def unified_diff_body(original_lines, mutated_lines)
104
+ ::Diff::LCS.sdiff(original_lines, mutated_lines).map { |c| format_sdiff_change(c) }.join("\n")
105
+ end
106
+
109
107
  def format_sdiff_change(change)
110
108
  case change.action
111
109
  when "=" then " #{change.old_element.chomp}"
@@ -5,6 +5,9 @@ require "prism"
5
5
  require_relative "../mutator"
6
6
 
7
7
  class Evilution::Mutator::Base < Prism::Visitor
8
+ AffectedSlices = Data.define(:original, :mutated)
9
+ private_constant :AffectedSlices
10
+
8
11
  attr_reader :mutations
9
12
 
10
13
  def initialize(**_options)
@@ -28,25 +31,24 @@ class Evilution::Mutator::Base < Prism::Visitor
28
31
  return if @filter && @filter.skip?(node)
29
32
 
30
33
  surgery = Evilution::AST::SourceSurgeon.apply(
31
- @file_source,
32
- offset: offset,
33
- length: length,
34
- replacement: replacement
34
+ @file_source, offset: offset, length: length, replacement: replacement
35
35
  )
36
- mutated_source = surgery.source
37
-
38
- original_slice, mutated_slice = slice_affected_lines(
39
- mutated_source: mutated_source,
36
+ slices = slice_affected_lines(
37
+ mutated_source: surgery.source,
40
38
  offset: offset,
41
39
  length: length,
42
40
  replacement_bytesize: replacement.bytesize
43
41
  )
44
42
 
45
- @mutations << Evilution::Mutation.new(
43
+ @mutations << build_mutation_record(node, surgery, slices)
44
+ end
45
+
46
+ def build_mutation_record(node, surgery, slices)
47
+ Evilution::Mutation.new(
46
48
  subject: @subject,
47
49
  operator_name: self.class.operator_name,
48
- sources: Evilution::Mutation::Sources.new(original: @file_source, mutated: mutated_source),
49
- slice: Evilution::Mutation::Slice.new(original: original_slice, mutated: mutated_slice),
50
+ sources: Evilution::Mutation::Sources.new(original: @file_source, mutated: surgery.source),
51
+ slice: Evilution::Mutation::Slice.new(original: slices.original, mutated: slices.mutated),
50
52
  location: Evilution::Mutation::Location.new(
51
53
  file_path: @subject.file_path,
52
54
  line: node.location.start_line,
@@ -64,10 +66,10 @@ class Evilution::Mutator::Base < Prism::Visitor
64
66
  orig_line_end = line_end_byte(@file_source, [offset + length - 1, line_start].max)
65
67
  mut_line_end = line_end_byte(mutated_source, [offset + replacement_bytesize - 1, line_start].max)
66
68
 
67
- [
68
- @file_source.byteslice(line_start, orig_line_end - line_start),
69
- mutated_source.byteslice(line_start, mut_line_end - line_start)
70
- ]
69
+ AffectedSlices.new(
70
+ original: @file_source.byteslice(line_start, orig_line_end - line_start),
71
+ mutated: mutated_source.byteslice(line_start, mut_line_end - line_start)
72
+ )
71
73
  end
72
74
 
73
75
  def line_start_byte(source, offset)
@@ -12,26 +12,23 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
12
12
 
13
13
  def visit_call_node(node)
14
14
  args = node.arguments&.arguments
15
-
16
- if mutable?(node, args)
17
- args.each_index do |i|
18
- parts = args.each_with_index.map { |a, j| j == i ? "nil" : a.slice }
19
- replacement = parts.join(", ")
20
-
21
- add_mutation(
22
- offset: node.arguments.location.start_offset,
23
- length: node.arguments.location.length,
24
- replacement: replacement,
25
- node: node
26
- )
27
- end
28
- end
15
+ args.each_index { |i| emit_nil_substitution(node, args, i) } if mutable?(node, args)
29
16
 
30
17
  super
31
18
  end
32
19
 
33
20
  private
34
21
 
22
+ def emit_nil_substitution(node, args, i)
23
+ parts = args.each_with_index.map { |a, j| j == i ? "nil" : a.slice }
24
+ add_mutation(
25
+ offset: node.arguments.location.start_offset,
26
+ length: node.arguments.location.length,
27
+ replacement: parts.join(", "),
28
+ node: node
29
+ )
30
+ end
31
+
35
32
  def mutable?(node, args)
36
33
  args && args.length >= 1 && positional_only?(args) && node.name != :[]=
37
34
  end
@@ -12,26 +12,23 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
12
12
 
13
13
  def visit_call_node(node)
14
14
  args = node.arguments&.arguments
15
-
16
- if mutable?(node, args)
17
- args.each_index do |i|
18
- remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
19
- replacement = remaining.join(", ")
20
-
21
- add_mutation(
22
- offset: node.arguments.location.start_offset,
23
- length: node.arguments.location.length,
24
- replacement:,
25
- node:
26
- )
27
- end
28
- end
15
+ args.each_index { |i| emit_argument_removal(node, args, i) } if mutable?(node, args)
29
16
 
30
17
  super
31
18
  end
32
19
 
33
20
  private
34
21
 
22
+ def emit_argument_removal(node, args, i)
23
+ remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
24
+ add_mutation(
25
+ offset: node.arguments.location.start_offset,
26
+ length: node.arguments.location.length,
27
+ replacement: remaining.join(", "),
28
+ node: node
29
+ )
30
+ end
31
+
35
32
  def mutable?(node, args)
36
33
  args && args.length >= 2 && positional_only?(args) && node.name != :[]=
37
34
  end
@@ -4,18 +4,30 @@ require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::BeginUnwrap < Evilution::Mutator::Base
6
6
  def visit_begin_node(node)
7
- return super if node.rescue_clause || node.else_clause || node.ensure_clause
8
- return super if node.statements.nil?
9
- return super if node.begin_keyword_loc.nil?
7
+ return super unless unwrappable?(node)
10
8
 
11
- body_text = @file_source.byteslice(node.statements.location.start_offset, node.statements.location.length)
12
9
  add_mutation(
13
10
  offset: node.location.start_offset,
14
11
  length: node.location.length,
15
- replacement: body_text,
12
+ replacement: body_text(node),
16
13
  node: node
17
14
  )
18
15
 
19
16
  super
20
17
  end
18
+
19
+ private
20
+
21
+ def unwrappable?(node)
22
+ return false if node.rescue_clause || node.else_clause || node.ensure_clause
23
+ return false if node.statements.nil?
24
+ return false if node.begin_keyword_loc.nil?
25
+
26
+ true
27
+ end
28
+
29
+ def body_text(node)
30
+ loc = node.statements.location
31
+ @file_source.byteslice(loc.start_offset, loc.length)
32
+ end
21
33
  end
@@ -5,27 +5,34 @@ require_relative "../operator"
5
5
  class Evilution::Mutator::Operator::BitwiseComplement < Evilution::Mutator::Base
6
6
  def visit_call_node(node)
7
7
  if node.name == :~ && node.receiver && node.arguments.nil?
8
- loc = node.message_loc
9
- receiver_loc = node.receiver.location
10
-
11
- # Remove ~: replace entire ~expr with just the receiver expression
12
- receiver_source = byteslice_source(receiver_loc.start_offset, receiver_loc.length)
13
- add_mutation(
14
- offset: node.location.start_offset,
15
- length: node.location.length,
16
- replacement: receiver_source,
17
- node: node
18
- )
19
-
20
- # Swap ~ with unary minus
21
- add_mutation(
22
- offset: loc.start_offset,
23
- length: loc.length,
24
- replacement: "-",
25
- node: node
26
- )
8
+ emit_remove_complement(node)
9
+ emit_swap_to_minus(node)
27
10
  end
28
11
 
29
12
  super
30
13
  end
14
+
15
+ private
16
+
17
+ # Replace `~expr` with just `expr`.
18
+ def emit_remove_complement(node)
19
+ receiver_loc = node.receiver.location
20
+ add_mutation(
21
+ offset: node.location.start_offset,
22
+ length: node.location.length,
23
+ replacement: byteslice_source(receiver_loc.start_offset, receiver_loc.length),
24
+ node: node
25
+ )
26
+ end
27
+
28
+ # Swap `~` with unary minus.
29
+ def emit_swap_to_minus(node)
30
+ loc = node.message_loc
31
+ add_mutation(
32
+ offset: loc.start_offset,
33
+ length: loc.length,
34
+ replacement: "-",
35
+ node: node
36
+ )
37
+ end
31
38
  end
@@ -38,14 +38,7 @@ class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
38
38
  end
39
39
 
40
40
  def remove_block_param(node)
41
- block_loc = node.parameters.block.location
42
- params_text = @file_source.byteslice(node.parameters.location.start_offset, node.parameters.location.length)
43
- block_rel = block_loc.start_offset - node.parameters.location.start_offset
44
-
45
- # Find the comma before the block param and remove ", &block"
46
- comma_pos = params_text.rindex(",", block_rel - 1)
47
- remove_start = node.parameters.location.start_offset + comma_pos
48
- remove_end = block_loc.start_offset + block_loc.length
41
+ remove_start, remove_end = block_param_removal_range(node)
49
42
 
50
43
  add_mutation(
51
44
  offset: remove_start,
@@ -54,4 +47,21 @@ class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
54
47
  node: node
55
48
  )
56
49
  end
50
+
51
+ # Range covering ", &block" — from the comma before the block param to the end of the block param.
52
+ def block_param_removal_range(node)
53
+ params_loc = node.parameters.location
54
+ block_loc = node.parameters.block.location
55
+ comma_pos = params_text(params_loc).rindex(",", block_loc.start_offset - params_loc.start_offset - 1)
56
+
57
+ [params_loc.start_offset + comma_pos, end_offset(block_loc)]
58
+ end
59
+
60
+ def params_text(params_loc)
61
+ @file_source.byteslice(params_loc.start_offset, params_loc.length)
62
+ end
63
+
64
+ def end_offset(loc)
65
+ loc.start_offset + loc.length
66
+ end
57
67
  end
@@ -4,27 +4,31 @@ require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::BlockPassRemoval < Evilution::Mutator::Base
6
6
  def visit_call_node(node)
7
- if node.block.is_a?(Prism::BlockArgumentNode)
8
- block_node = node.block
9
- call_start = node.location.start_offset
10
- node_end = call_start + node.location.length
11
- block_end = block_node.location.start_offset + block_node.location.length
12
-
13
- prefix = @file_source.byteslice(call_start...block_node.location.start_offset).rstrip
14
- suffix = @file_source.byteslice(block_end...node_end)
15
-
16
- # Clean up: remove trailing comma from prefix, remove empty parens
17
- prefix = prefix.sub(/,\s*\z/, "")
18
- replacement = "#{prefix}#{suffix}".sub(/\(\s*\)/, "")
19
-
7
+ block_node = node.block
8
+ if block_node.is_a?(Prism::BlockArgumentNode)
20
9
  add_mutation(
21
- offset: call_start,
10
+ offset: node.location.start_offset,
22
11
  length: node.location.length,
23
- replacement: replacement,
12
+ replacement: build_replacement(node, block_node),
24
13
  node: node
25
14
  )
26
15
  end
27
16
 
28
17
  super
29
18
  end
19
+
20
+ private
21
+
22
+ # Drop the block-pass argument plus the trailing comma it leaves behind, and
23
+ # collapse the resulting `()` if the block-pass was the only argument.
24
+ def build_replacement(node, block_node)
25
+ call_start = node.location.start_offset
26
+ node_end = call_start + node.location.length
27
+ block_start = block_node.location.start_offset
28
+ block_end = block_start + block_node.location.length
29
+
30
+ prefix = @file_source.byteslice(call_start...block_start).rstrip.sub(/,\s*\z/, "")
31
+ suffix = @file_source.byteslice(block_end...node_end)
32
+ "#{prefix}#{suffix}".sub(/\(\s*\)/, "")
33
+ end
30
34
  end
@@ -40,16 +40,18 @@ class Evilution::Mutator::Operator::CaseWhen < Evilution::Mutator::Base
40
40
  end
41
41
 
42
42
  def remove_else_branch(node)
43
- return if node.else_clause.nil?
44
- return if node.else_clause.statements.nil?
43
+ else_clause = node.else_clause
44
+ return if else_clause.nil? || else_clause.statements.nil?
45
+
46
+ start_offset = else_clause.else_keyword_loc.start_offset
47
+ stmts_loc = else_clause.statements.location
48
+ end_offset = stmts_loc.start_offset + stmts_loc.length
45
49
 
46
- start_offset = node.else_clause.else_keyword_loc.start_offset
47
- end_offset = node.else_clause.statements.location.start_offset + node.else_clause.statements.location.length
48
50
  add_mutation(
49
51
  offset: start_offset,
50
52
  length: end_offset - start_offset,
51
53
  replacement: "",
52
- node: node.else_clause
54
+ node: else_clause
53
55
  )
54
56
  end
55
57
  end
@@ -4,29 +4,29 @@ require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::ConditionalBranch < Evilution::Mutator::Base
6
6
  def visit_if_node(node)
7
- if node.statements && node.subsequent.nil?
8
- add_mutation(
9
- offset: node.statements.location.start_offset,
10
- length: node.statements.location.length,
11
- replacement: "nil",
12
- node: node
13
- )
14
- elsif node.statements && node.subsequent&.statements
15
- add_mutation(
16
- offset: node.statements.location.start_offset,
17
- length: node.statements.location.length,
18
- replacement: "nil",
19
- node: node
20
- )
21
-
22
- add_mutation(
23
- offset: node.subsequent.statements.location.start_offset,
24
- length: node.subsequent.statements.location.length,
25
- replacement: "nil",
26
- node: node
27
- )
28
- end
7
+ return super unless node.statements
8
+
9
+ add_nil_mutation(node.statements, node)
10
+ add_nil_mutation_to_else(node.subsequent, node)
29
11
 
30
12
  super
31
13
  end
14
+
15
+ private
16
+
17
+ def add_nil_mutation_to_else(subsequent, node)
18
+ return unless subsequent.is_a?(Prism::ElseNode)
19
+ return if subsequent.statements.nil?
20
+
21
+ add_nil_mutation(subsequent.statements, node)
22
+ end
23
+
24
+ def add_nil_mutation(statements, node)
25
+ add_mutation(
26
+ offset: statements.location.start_offset,
27
+ length: statements.location.length,
28
+ replacement: "nil",
29
+ node: node
30
+ )
31
+ end
32
32
  end
@@ -5,9 +5,8 @@ require_relative "../operator"
5
5
  class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Base
6
6
  def visit_call_node(node)
7
7
  if node.name == :== && node.receiver && node.arguments
8
- receiver_text = @file_source.byteslice(node.receiver.location.start_offset, node.receiver.location.length)
9
- arg = node.arguments.arguments.first
10
- arg_text = @file_source.byteslice(arg.location.start_offset, arg.location.length)
8
+ receiver_text = loc_text(node.receiver.location)
9
+ arg_text = loc_text(node.arguments.arguments.first.location)
11
10
 
12
11
  add_mutation(
13
12
  offset: node.location.start_offset,
@@ -19,4 +18,10 @@ class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Bas
19
18
 
20
19
  super
21
20
  end
21
+
22
+ private
23
+
24
+ def loc_text(loc)
25
+ @file_source.byteslice(loc.start_offset, loc.length)
26
+ end
22
27
  end
@@ -23,25 +23,29 @@ class Evilution::Mutator::Operator::ExplicitSuperMutation < Evilution::Mutator::
23
23
  end
24
24
 
25
25
  def mutate_arguments(node, args)
26
- # Remove all arguments: super(a, b) -> super()
26
+ emit_remove_all_args(node)
27
+ return unless args.length >= 2
28
+
29
+ args.each_index { |i| emit_remove_arg_at(node, args, i) }
30
+ end
31
+
32
+ # super(a, b) -> super()
33
+ def emit_remove_all_args(node)
27
34
  add_mutation(
28
35
  offset: node.arguments.location.start_offset,
29
36
  length: node.arguments.location.length,
30
37
  replacement: "",
31
38
  node: node
32
39
  )
40
+ end
33
41
 
34
- return unless args.length >= 2
35
-
36
- # Remove individual arguments
37
- args.each_index do |i|
38
- remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
39
- add_mutation(
40
- offset: node.arguments.location.start_offset,
41
- length: node.arguments.location.length,
42
- replacement: remaining.join(", "),
43
- node: node
44
- )
45
- end
42
+ def emit_remove_arg_at(node, args, i)
43
+ remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
44
+ add_mutation(
45
+ offset: node.arguments.location.start_offset,
46
+ length: node.arguments.location.length,
47
+ replacement: remaining.join(", "),
48
+ node: node
49
+ )
46
50
  end
47
51
  end
@@ -5,13 +5,10 @@ require_relative "../operator"
5
5
  class Evilution::Mutator::Operator::IndexToAt < Evilution::Mutator::Base
6
6
  def visit_call_node(node)
7
7
  if indexable?(node)
8
- receiver_source = @file_source.byteslice(node.receiver.location.start_offset, node.receiver.location.length)
9
- arg_source = @file_source.byteslice(node.arguments.location.start_offset, node.arguments.location.length)
10
-
11
8
  add_mutation(
12
9
  offset: node.location.start_offset,
13
10
  length: node.location.length,
14
- replacement: "#{receiver_source}.at(#{arg_source})",
11
+ replacement: "#{loc_text(node.receiver.location)}.at(#{loc_text(node.arguments.location)})",
15
12
  node: node
16
13
  )
17
14
  end
@@ -21,6 +18,10 @@ class Evilution::Mutator::Operator::IndexToAt < Evilution::Mutator::Base
21
18
 
22
19
  private
23
20
 
21
+ def loc_text(loc)
22
+ @file_source.byteslice(loc.start_offset, loc.length)
23
+ end
24
+
24
25
  def indexable?(node)
25
26
  node.name == :[] &&
26
27
  node.receiver &&
@@ -3,6 +3,9 @@
3
3
  require_relative "../operator"
4
4
 
5
5
  class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
6
+ Chain = Data.define(:root, :args)
7
+ private_constant :Chain
8
+
6
9
  def initialize(**options)
7
10
  super
8
11
  @consumed = Set.new
@@ -10,14 +13,11 @@ class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
10
13
 
11
14
  def visit_call_node(node)
12
15
  if chain_head?(node)
13
- root, args = collect_chain(node)
14
- root_source = byteslice_source(root.location.start_offset, root.location.length)
15
- arg_sources = args.map { |a| byteslice_source(a.location.start_offset, a.location.length) }
16
-
16
+ chain = collect_chain(node)
17
17
  add_mutation(
18
18
  offset: node.location.start_offset,
19
19
  length: node.location.length,
20
- replacement: "#{root_source}.dig(#{arg_sources.join(", ")})",
20
+ replacement: dig_replacement(chain),
21
21
  node: node
22
22
  )
23
23
  end
@@ -27,6 +27,12 @@ class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
27
27
 
28
28
  private
29
29
 
30
+ def dig_replacement(chain)
31
+ root_source = byteslice_source(chain.root.location.start_offset, chain.root.location.length)
32
+ arg_sources = chain.args.map { |a| byteslice_source(a.location.start_offset, a.location.length) }
33
+ "#{root_source}.dig(#{arg_sources.join(", ")})"
34
+ end
35
+
30
36
  def chain_head?(node)
31
37
  return false if @consumed.include?(node.object_id)
32
38
  return false unless single_arg_index?(node)
@@ -53,6 +59,6 @@ class Evilution::Mutator::Operator::IndexToDig < Evilution::Mutator::Base
53
59
  current = current.receiver
54
60
  end
55
61
 
56
- [current, args]
62
+ Chain.new(root: current, args: args)
57
63
  end
58
64
  end
@@ -5,13 +5,10 @@ require_relative "../operator"
5
5
  class Evilution::Mutator::Operator::IndexToFetch < Evilution::Mutator::Base
6
6
  def visit_call_node(node)
7
7
  if indexable?(node)
8
- receiver_source = byteslice_source(node.receiver.location.start_offset, node.receiver.location.length)
9
- arg_source = byteslice_source(node.arguments.location.start_offset, node.arguments.location.length)
10
-
11
8
  add_mutation(
12
9
  offset: node.location.start_offset,
13
10
  length: node.location.length,
14
- replacement: "#{receiver_source}.fetch(#{arg_source})",
11
+ replacement: "#{loc_text(node.receiver.location)}.fetch(#{loc_text(node.arguments.location)})",
15
12
  node: node
16
13
  )
17
14
  end
@@ -21,6 +18,10 @@ class Evilution::Mutator::Operator::IndexToFetch < Evilution::Mutator::Base
21
18
 
22
19
  private
23
20
 
21
+ def loc_text(loc)
22
+ byteslice_source(loc.start_offset, loc.length)
23
+ end
24
+
24
25
  def indexable?(node)
25
26
  node.name == :[] &&
26
27
  node.receiver &&
@@ -56,36 +56,41 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
56
56
  return unless kr.is_a?(Prism::KeywordRestParameterNode)
57
57
 
58
58
  all_params = collect_all_params(params)
59
-
60
59
  if all_params.length < 2
61
- add_mutation(
62
- offset: kr.location.start_offset,
63
- length: kr.location.length,
64
- replacement: "",
65
- node: kr
66
- )
60
+ emit_remove_only_kr(kr)
67
61
  else
68
- remaining = all_params.reject { |p| p.equal?(kr) }
69
- replacement = remaining.map(&:slice).join(", ")
70
-
71
- add_mutation(
72
- offset: params.location.start_offset,
73
- length: params.location.length,
74
- replacement: replacement,
75
- node: kr
76
- )
62
+ emit_remove_kr_with_remaining(params, all_params, kr)
77
63
  end
78
64
  end
79
65
 
66
+ def emit_remove_only_kr(kr)
67
+ add_mutation(
68
+ offset: kr.location.start_offset,
69
+ length: kr.location.length,
70
+ replacement: "",
71
+ node: kr
72
+ )
73
+ end
74
+
75
+ def emit_remove_kr_with_remaining(params, all_params, kr)
76
+ remaining = all_params.reject { |p| p.equal?(kr) }
77
+ add_mutation(
78
+ offset: params.location.start_offset,
79
+ length: params.location.length,
80
+ replacement: remaining.map(&:slice).join(", "),
81
+ node: kr
82
+ )
83
+ end
84
+
80
85
  def collect_all_params(params)
81
- result = []
82
- result.concat(params.requireds)
83
- result.concat(params.optionals)
84
- result << params.rest if params.rest
85
- result.concat(params.posts)
86
- result.concat(params.keywords)
87
- result << params.keyword_rest if params.keyword_rest
88
- result << params.block if params.block
89
- result
86
+ [
87
+ *params.requireds,
88
+ *params.optionals,
89
+ params.rest,
90
+ *params.posts,
91
+ *params.keywords,
92
+ params.keyword_rest,
93
+ params.block
94
+ ].compact
90
95
  end
91
96
  end