evilution 0.13.0 → 0.14.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +8 -8
  4. data/CHANGELOG.md +17 -0
  5. data/lib/evilution/ast/parser.rb +69 -68
  6. data/lib/evilution/ast/source_surgeon.rb +7 -9
  7. data/lib/evilution/ast.rb +4 -0
  8. data/lib/evilution/baseline.rb +73 -75
  9. data/lib/evilution/cache.rb +75 -77
  10. data/lib/evilution/cli.rb +408 -173
  11. data/lib/evilution/config.rb +141 -136
  12. data/lib/evilution/equivalent/detector.rb +25 -27
  13. data/lib/evilution/equivalent/heuristic/alias_swap.rb +29 -33
  14. data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
  15. data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
  16. data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
  17. data/lib/evilution/equivalent/heuristic.rb +6 -0
  18. data/lib/evilution/equivalent.rb +4 -0
  19. data/lib/evilution/git/changed_files.rb +35 -37
  20. data/lib/evilution/git.rb +4 -0
  21. data/lib/evilution/integration/base.rb +5 -7
  22. data/lib/evilution/integration/rspec.rb +114 -116
  23. data/lib/evilution/integration.rb +4 -0
  24. data/lib/evilution/isolation/fork.rb +98 -100
  25. data/lib/evilution/isolation/in_process.rb +59 -61
  26. data/lib/evilution/isolation.rb +4 -0
  27. data/lib/evilution/mcp/mutate_tool.rb +172 -143
  28. data/lib/evilution/mcp/server.rb +12 -11
  29. data/lib/evilution/mcp/session_diff_tool.rb +89 -0
  30. data/lib/evilution/mcp/session_list_tool.rb +46 -0
  31. data/lib/evilution/mcp/session_show_tool.rb +53 -0
  32. data/lib/evilution/mcp.rb +4 -0
  33. data/lib/evilution/memory/leak_check.rb +80 -84
  34. data/lib/evilution/memory.rb +34 -36
  35. data/lib/evilution/mutation.rb +40 -42
  36. data/lib/evilution/mutator/base.rb +46 -48
  37. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
  38. data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
  39. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
  40. data/lib/evilution/mutator/operator/array_literal.rb +18 -22
  41. data/lib/evilution/mutator/operator/block_removal.rb +16 -20
  42. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
  43. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
  44. data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
  45. data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
  46. data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
  47. data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
  48. data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
  49. data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
  50. data/lib/evilution/mutator/operator/float_literal.rb +22 -26
  51. data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
  52. data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
  53. data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
  54. data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
  55. data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
  56. data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
  57. data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
  58. data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
  59. data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
  60. data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
  61. data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
  62. data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
  63. data/lib/evilution/mutator/operator/string_literal.rb +18 -22
  64. data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
  65. data/lib/evilution/mutator/operator.rb +6 -0
  66. data/lib/evilution/mutator/registry.rb +54 -56
  67. data/lib/evilution/mutator.rb +4 -0
  68. data/lib/evilution/parallel/pool.rb +56 -58
  69. data/lib/evilution/parallel.rb +4 -0
  70. data/lib/evilution/reporter/cli.rb +99 -101
  71. data/lib/evilution/reporter/html.rb +242 -244
  72. data/lib/evilution/reporter/json.rb +57 -59
  73. data/lib/evilution/reporter/suggestion.rb +326 -328
  74. data/lib/evilution/reporter.rb +4 -0
  75. data/lib/evilution/result/mutation_result.rb +43 -46
  76. data/lib/evilution/result/summary.rb +80 -81
  77. data/lib/evilution/result.rb +4 -0
  78. data/lib/evilution/runner.rb +334 -323
  79. data/lib/evilution/session/store.rb +147 -0
  80. data/lib/evilution/session.rb +4 -0
  81. data/lib/evilution/spec_resolver.rb +49 -47
  82. data/lib/evilution/subject.rb +14 -16
  83. data/lib/evilution/version.rb +1 -1
  84. data/lib/evilution.rb +13 -0
  85. metadata +19 -2
@@ -1,123 +1,119 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class CompoundAssignment < Base
7
- REPLACEMENTS = {
8
- :+ => %i[- *],
9
- :- => [:+],
10
- :* => [:/],
11
- :/ => [:*],
12
- :% => [:*],
13
- :** => [:*],
14
- :& => %i[| ^],
15
- :| => [:&],
16
- :^ => [:&],
17
- :<< => [:>>],
18
- :>> => [:<<]
19
- }.freeze
20
-
21
- def visit_local_variable_operator_write_node(node)
22
- mutate_operator_write(node)
23
- super
24
- end
25
-
26
- def visit_instance_variable_operator_write_node(node)
27
- mutate_operator_write(node)
28
- super
29
- end
30
-
31
- def visit_class_variable_operator_write_node(node)
32
- mutate_operator_write(node)
33
- super
34
- end
35
-
36
- def visit_global_variable_operator_write_node(node)
37
- mutate_operator_write(node)
38
- super
39
- end
40
-
41
- def visit_local_variable_and_write_node(node)
42
- mutate_logical_write(node, "||=")
43
- super
44
- end
45
-
46
- def visit_local_variable_or_write_node(node)
47
- mutate_logical_write(node, "&&=")
48
- super
49
- end
50
-
51
- def visit_instance_variable_and_write_node(node)
52
- mutate_logical_write(node, "||=")
53
- super
54
- end
55
-
56
- def visit_instance_variable_or_write_node(node)
57
- mutate_logical_write(node, "&&=")
58
- super
59
- end
60
-
61
- def visit_class_variable_and_write_node(node)
62
- mutate_logical_write(node, "||=")
63
- super
64
- end
65
-
66
- def visit_class_variable_or_write_node(node)
67
- mutate_logical_write(node, "&&=")
68
- super
69
- end
70
-
71
- def visit_global_variable_and_write_node(node)
72
- mutate_logical_write(node, "||=")
73
- super
74
- end
75
-
76
- def visit_global_variable_or_write_node(node)
77
- mutate_logical_write(node, "&&=")
78
- super
79
- end
80
-
81
- private
82
-
83
- def mutate_logical_write(node, replacement)
84
- loc = node.operator_loc
85
- add_mutation(
86
- offset: loc.start_offset,
87
- length: loc.length,
88
- replacement: replacement,
89
- node: node
90
- )
91
- remove_statement(node)
92
- end
93
-
94
- def mutate_operator_write(node)
95
- replacements = REPLACEMENTS[node.binary_operator]
96
- return unless replacements
97
-
98
- loc = node.binary_operator_loc
99
-
100
- replacements.each do |replacement|
101
- replacement_str = "#{replacement}="
102
- add_mutation(
103
- offset: loc.start_offset,
104
- length: loc.length,
105
- replacement: replacement_str,
106
- node: node
107
- )
108
- end
109
- remove_statement(node)
110
- end
111
-
112
- def remove_statement(node)
113
- add_mutation(
114
- offset: node.location.start_offset,
115
- length: node.location.length,
116
- replacement: "nil",
117
- node: node
118
- )
119
- end
120
- end
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::CompoundAssignment < Evilution::Mutator::Base
6
+ REPLACEMENTS = {
7
+ :+ => %i[- *],
8
+ :- => [:+],
9
+ :* => [:/],
10
+ :/ => [:*],
11
+ :% => [:*],
12
+ :** => [:*],
13
+ :& => %i[| ^],
14
+ :| => [:&],
15
+ :^ => [:&],
16
+ :<< => [:>>],
17
+ :>> => [:<<]
18
+ }.freeze
19
+
20
+ def visit_local_variable_operator_write_node(node)
21
+ mutate_operator_write(node)
22
+ super
23
+ end
24
+
25
+ def visit_instance_variable_operator_write_node(node)
26
+ mutate_operator_write(node)
27
+ super
28
+ end
29
+
30
+ def visit_class_variable_operator_write_node(node)
31
+ mutate_operator_write(node)
32
+ super
33
+ end
34
+
35
+ def visit_global_variable_operator_write_node(node)
36
+ mutate_operator_write(node)
37
+ super
38
+ end
39
+
40
+ def visit_local_variable_and_write_node(node)
41
+ mutate_logical_write(node, "||=")
42
+ super
43
+ end
44
+
45
+ def visit_local_variable_or_write_node(node)
46
+ mutate_logical_write(node, "&&=")
47
+ super
48
+ end
49
+
50
+ def visit_instance_variable_and_write_node(node)
51
+ mutate_logical_write(node, "||=")
52
+ super
53
+ end
54
+
55
+ def visit_instance_variable_or_write_node(node)
56
+ mutate_logical_write(node, "&&=")
57
+ super
58
+ end
59
+
60
+ def visit_class_variable_and_write_node(node)
61
+ mutate_logical_write(node, "||=")
62
+ super
63
+ end
64
+
65
+ def visit_class_variable_or_write_node(node)
66
+ mutate_logical_write(node, "&&=")
67
+ super
68
+ end
69
+
70
+ def visit_global_variable_and_write_node(node)
71
+ mutate_logical_write(node, "||=")
72
+ super
73
+ end
74
+
75
+ def visit_global_variable_or_write_node(node)
76
+ mutate_logical_write(node, "&&=")
77
+ super
78
+ end
79
+
80
+ private
81
+
82
+ def mutate_logical_write(node, replacement)
83
+ loc = node.operator_loc
84
+ add_mutation(
85
+ offset: loc.start_offset,
86
+ length: loc.length,
87
+ replacement: replacement,
88
+ node: node
89
+ )
90
+ remove_statement(node)
91
+ end
92
+
93
+ def mutate_operator_write(node)
94
+ replacements = REPLACEMENTS[node.binary_operator]
95
+ return unless replacements
96
+
97
+ loc = node.binary_operator_loc
98
+
99
+ replacements.each do |replacement|
100
+ replacement_str = "#{replacement}="
101
+ add_mutation(
102
+ offset: loc.start_offset,
103
+ length: loc.length,
104
+ replacement: replacement_str,
105
+ node: node
106
+ )
121
107
  end
108
+ remove_statement(node)
109
+ end
110
+
111
+ def remove_statement(node)
112
+ add_mutation(
113
+ offset: node.location.start_offset,
114
+ length: node.location.length,
115
+ replacement: "nil",
116
+ node: node
117
+ )
122
118
  end
123
119
  end
@@ -1,36 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class ConditionalBranch < Base
7
- def visit_if_node(node)
8
- if node.statements && node.subsequent.nil?
9
- add_mutation(
10
- offset: node.statements.location.start_offset,
11
- length: node.statements.location.length,
12
- replacement: "nil",
13
- node: node
14
- )
15
- elsif node.statements && node.subsequent&.statements
16
- add_mutation(
17
- offset: node.statements.location.start_offset,
18
- length: node.statements.location.length,
19
- replacement: "nil",
20
- node: node
21
- )
3
+ require_relative "../operator"
22
4
 
23
- add_mutation(
24
- offset: node.subsequent.statements.location.start_offset,
25
- length: node.subsequent.statements.location.length,
26
- replacement: "nil",
27
- node: node
28
- )
29
- end
5
+ class Evilution::Mutator::Operator::ConditionalBranch < Evilution::Mutator::Base
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
+ )
30
21
 
31
- super
32
- end
33
- end
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
+ )
34
28
  end
29
+
30
+ super
35
31
  end
36
32
  end
@@ -1,39 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class ConditionalFlip < Base
7
- def visit_if_node(node)
8
- if node.if_keyword == "if" && !elsif?(node)
9
- add_mutation(
10
- offset: node.if_keyword_loc.start_offset,
11
- length: node.if_keyword_loc.length,
12
- replacement: "unless",
13
- node: node
14
- )
15
- end
3
+ require_relative "../operator"
16
4
 
17
- super
18
- end
5
+ class Evilution::Mutator::Operator::ConditionalFlip < Evilution::Mutator::Base
6
+ def visit_if_node(node)
7
+ if node.if_keyword == "if" && !elsif?(node)
8
+ add_mutation(
9
+ offset: node.if_keyword_loc.start_offset,
10
+ length: node.if_keyword_loc.length,
11
+ replacement: "unless",
12
+ node: node
13
+ )
14
+ end
19
15
 
20
- def visit_unless_node(node)
21
- add_mutation(
22
- offset: node.keyword_loc.start_offset,
23
- length: node.keyword_loc.length,
24
- replacement: "if",
25
- node: node
26
- )
16
+ super
17
+ end
27
18
 
28
- super
29
- end
19
+ def visit_unless_node(node)
20
+ add_mutation(
21
+ offset: node.keyword_loc.start_offset,
22
+ length: node.keyword_loc.length,
23
+ replacement: "if",
24
+ node: node
25
+ )
30
26
 
31
- private
27
+ super
28
+ end
32
29
 
33
- def elsif?(node)
34
- node.subsequent.is_a?(Prism::IfNode)
35
- end
36
- end
37
- end
30
+ private
31
+
32
+ def elsif?(node)
33
+ node.subsequent.is_a?(Prism::IfNode)
38
34
  end
39
35
  end
@@ -1,36 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class ConditionalNegation < Base
7
- def visit_if_node(node)
8
- mutate_predicate(node)
9
- super
10
- end
3
+ require_relative "../operator"
11
4
 
12
- def visit_unless_node(node)
13
- mutate_predicate(node)
14
- super
15
- end
5
+ class Evilution::Mutator::Operator::ConditionalNegation < Evilution::Mutator::Base
6
+ def visit_if_node(node)
7
+ mutate_predicate(node)
8
+ super
9
+ end
10
+
11
+ def visit_unless_node(node)
12
+ mutate_predicate(node)
13
+ super
14
+ end
16
15
 
17
- private
16
+ private
18
17
 
19
- def mutate_predicate(node)
20
- add_mutation(
21
- offset: node.predicate.location.start_offset,
22
- length: node.predicate.location.length,
23
- replacement: "true",
24
- node: node
25
- )
26
- add_mutation(
27
- offset: node.predicate.location.start_offset,
28
- length: node.predicate.location.length,
29
- replacement: "false",
30
- node: node
31
- )
32
- end
33
- end
34
- end
18
+ def mutate_predicate(node)
19
+ add_mutation(
20
+ offset: node.predicate.location.start_offset,
21
+ length: node.predicate.location.length,
22
+ replacement: "true",
23
+ node: node
24
+ )
25
+ add_mutation(
26
+ offset: node.predicate.location.start_offset,
27
+ length: node.predicate.location.length,
28
+ replacement: "false",
29
+ node: node
30
+ )
35
31
  end
36
32
  end
@@ -1,33 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class FloatLiteral < Base
7
- def visit_float_node(node)
8
- replacement = case node.value
9
- when 0.0 then "1.0"
10
- when 1.0 then "0.0"
11
- else "0.0"
12
- end
3
+ require_relative "../operator"
13
4
 
14
- add_mutation(
15
- offset: node.location.start_offset,
16
- length: node.location.length,
17
- replacement: replacement,
18
- node: node
19
- )
5
+ class Evilution::Mutator::Operator::FloatLiteral < Evilution::Mutator::Base
6
+ def visit_float_node(node)
7
+ replacement = case node.value
8
+ when 0.0 then "1.0"
9
+ when 1.0 then "0.0"
10
+ else "0.0"
11
+ end
20
12
 
21
- add_mutation(
22
- offset: node.location.start_offset,
23
- length: node.location.length,
24
- replacement: "nil",
25
- node: node
26
- )
13
+ add_mutation(
14
+ offset: node.location.start_offset,
15
+ length: node.location.length,
16
+ replacement: replacement,
17
+ node: node
18
+ )
27
19
 
28
- super
29
- end
30
- end
31
- end
20
+ add_mutation(
21
+ offset: node.location.start_offset,
22
+ length: node.location.length,
23
+ replacement: "nil",
24
+ node: node
25
+ )
26
+
27
+ super
32
28
  end
33
29
  end
@@ -1,29 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class HashLiteral < Base
7
- def visit_hash_node(node)
8
- if node.elements.any?
9
- add_mutation(
10
- offset: node.location.start_offset,
11
- length: node.location.length,
12
- replacement: "{}",
13
- node: node
14
- )
3
+ require_relative "../operator"
15
4
 
16
- add_mutation(
17
- offset: node.location.start_offset,
18
- length: node.location.length,
19
- replacement: "nil",
20
- node: node
21
- )
22
- end
5
+ class Evilution::Mutator::Operator::HashLiteral < Evilution::Mutator::Base
6
+ def visit_hash_node(node)
7
+ if node.elements.any?
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: node.location.length,
11
+ replacement: "{}",
12
+ node: node
13
+ )
23
14
 
24
- super
25
- end
26
- end
15
+ add_mutation(
16
+ offset: node.location.start_offset,
17
+ length: node.location.length,
18
+ replacement: "nil",
19
+ node: node
20
+ )
27
21
  end
22
+
23
+ super
28
24
  end
29
25
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../operator"
4
+
3
5
  class Evilution::Mutator::Operator::IntegerLiteral < Evilution::Mutator::Base
4
6
  def visit_integer_node(node)
5
7
  if node.value.zero?
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class MethodBodyReplacement < Base
7
- def visit_def_node(node)
8
- if node.body
9
- add_mutation(
10
- offset: node.body.location.start_offset,
11
- length: node.body.location.length,
12
- replacement: "nil",
13
- node: node
14
- )
15
- end
3
+ require_relative "../operator"
16
4
 
17
- super
18
- end
19
- end
5
+ class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::Base
6
+ def visit_def_node(node)
7
+ if node.body
8
+ add_mutation(
9
+ offset: node.body.location.start_offset,
10
+ length: node.body.location.length,
11
+ replacement: "nil",
12
+ node: node
13
+ )
20
14
  end
15
+
16
+ super
21
17
  end
22
18
  end
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class MethodCallRemoval < Base
7
- def visit_call_node(node)
8
- if node.receiver
9
- add_mutation(
10
- offset: node.location.start_offset,
11
- length: node.location.length,
12
- replacement: node.receiver.slice,
13
- node: node
14
- )
15
- end
3
+ require_relative "../operator"
16
4
 
17
- super
18
- end
19
- end
5
+ class Evilution::Mutator::Operator::MethodCallRemoval < Evilution::Mutator::Base
6
+ def visit_call_node(node)
7
+ if node.receiver
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: node.location.length,
11
+ replacement: node.receiver.slice,
12
+ node: node
13
+ )
20
14
  end
15
+
16
+ super
21
17
  end
22
18
  end
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Mutator
5
- module Operator
6
- class NegationInsertion < Base
7
- def visit_call_node(node)
8
- if node.name.to_s.end_with?("?")
9
- add_mutation(
10
- offset: node.location.start_offset,
11
- length: 0,
12
- replacement: "!",
13
- node: node
14
- )
15
- end
3
+ require_relative "../operator"
16
4
 
17
- super
18
- end
19
- end
5
+ class Evilution::Mutator::Operator::NegationInsertion < Evilution::Mutator::Base
6
+ def visit_call_node(node)
7
+ if node.name.to_s.end_with?("?")
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: 0,
11
+ replacement: "!",
12
+ node: node
13
+ )
20
14
  end
15
+
16
+ super
21
17
  end
22
18
  end