evilution 0.15.0 → 0.16.1

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +32 -32
  4. data/CHANGELOG.md +27 -0
  5. data/lib/evilution/ast/source_surgeon.rb +3 -3
  6. data/lib/evilution/cli.rb +1 -0
  7. data/lib/evilution/config.rb +7 -1
  8. data/lib/evilution/mutator/operator/bang_method.rb +48 -0
  9. data/lib/evilution/mutator/operator/bitwise_complement.rb +31 -0
  10. data/lib/evilution/mutator/operator/bitwise_replacement.rb +30 -0
  11. data/lib/evilution/mutator/operator/break_statement.rb +50 -0
  12. data/lib/evilution/mutator/operator/class_variable_write.rb +25 -0
  13. data/lib/evilution/mutator/operator/collection_replacement.rb +25 -1
  14. data/lib/evilution/mutator/operator/ensure_removal.rb +27 -0
  15. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +47 -0
  16. data/lib/evilution/mutator/operator/global_variable_write.rb +25 -0
  17. data/lib/evilution/mutator/operator/inline_rescue.rb +39 -0
  18. data/lib/evilution/mutator/operator/instance_variable_write.rb +25 -0
  19. data/lib/evilution/mutator/operator/local_variable_assignment.rb +16 -0
  20. data/lib/evilution/mutator/operator/next_statement.rb +50 -0
  21. data/lib/evilution/mutator/operator/redo_statement.rb +18 -0
  22. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +94 -0
  23. data/lib/evilution/mutator/operator/rescue_removal.rb +37 -0
  24. data/lib/evilution/mutator/operator/send_mutation.rb +11 -2
  25. data/lib/evilution/mutator/operator/zsuper_removal.rb +16 -0
  26. data/lib/evilution/mutator/registry.rb +17 -1
  27. data/lib/evilution/reporter/progress_bar.rb +84 -0
  28. data/lib/evilution/reporter/suggestion.rb +225 -1
  29. data/lib/evilution/runner.rb +23 -11
  30. data/lib/evilution/version.rb +1 -1
  31. data/lib/evilution.rb +17 -0
  32. metadata +19 -2
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::ExplicitSuperMutation < Evilution::Mutator::Base
6
+ def visit_super_node(node)
7
+ replace_with_zsuper(node)
8
+ args = node.arguments&.arguments
9
+ mutate_arguments(node, args) if args && !args.empty?
10
+
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ def replace_with_zsuper(node)
17
+ add_mutation(
18
+ offset: node.location.start_offset,
19
+ length: node.location.length,
20
+ replacement: "super",
21
+ node: node
22
+ )
23
+ end
24
+
25
+ def mutate_arguments(node, args)
26
+ # Remove all arguments: super(a, b) -> super()
27
+ add_mutation(
28
+ offset: node.arguments.location.start_offset,
29
+ length: node.arguments.location.length,
30
+ replacement: "",
31
+ node: node
32
+ )
33
+
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
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::GlobalVariableWrite < Evilution::Mutator::Base
6
+ def visit_global_variable_write_node(node)
7
+ # Mutation 1: remove assignment, keep only the value expression
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: node.location.length,
11
+ replacement: node.value.slice,
12
+ node: node
13
+ )
14
+
15
+ # Mutation 2: replace value with nil
16
+ add_mutation(
17
+ offset: node.value.location.start_offset,
18
+ length: node.value.location.length,
19
+ replacement: "nil",
20
+ node: node
21
+ )
22
+
23
+ super
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::InlineRescue < Evilution::Mutator::Base
6
+ def visit_rescue_modifier_node(node)
7
+ generate_rescue_removal(node)
8
+ generate_nil_fallback(node)
9
+
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ def generate_rescue_removal(node)
16
+ expr_end = node.expression.location.start_offset + node.expression.location.length
17
+ rescue_end = node.rescue_expression.location.start_offset + node.rescue_expression.location.length
18
+
19
+ add_mutation(
20
+ offset: expr_end,
21
+ length: rescue_end - expr_end,
22
+ replacement: "",
23
+ node: node
24
+ )
25
+ end
26
+
27
+ def generate_nil_fallback(node)
28
+ return if node.rescue_expression.is_a?(Prism::NilNode)
29
+
30
+ fallback_loc = node.rescue_expression.location
31
+
32
+ add_mutation(
33
+ offset: fallback_loc.start_offset,
34
+ length: fallback_loc.length,
35
+ replacement: "nil",
36
+ node: node
37
+ )
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::InstanceVariableWrite < Evilution::Mutator::Base
6
+ def visit_instance_variable_write_node(node)
7
+ # Mutation 1: remove assignment, keep only the value expression
8
+ add_mutation(
9
+ offset: node.location.start_offset,
10
+ length: node.location.length,
11
+ replacement: node.value.slice,
12
+ node: node
13
+ )
14
+
15
+ # Mutation 2: replace value with nil
16
+ add_mutation(
17
+ offset: node.value.location.start_offset,
18
+ length: node.value.location.length,
19
+ replacement: "nil",
20
+ node: node
21
+ )
22
+
23
+ super
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::LocalVariableAssignment < Evilution::Mutator::Base
6
+ def visit_local_variable_write_node(node)
7
+ add_mutation(
8
+ offset: node.location.start_offset,
9
+ length: node.location.length,
10
+ replacement: node.value.slice,
11
+ node: node
12
+ )
13
+
14
+ super
15
+ end
16
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::NextStatement < Evilution::Mutator::Base
6
+ def visit_next_node(node)
7
+ generate_removal(node)
8
+ generate_nil_value(node)
9
+ generate_break_swap(node)
10
+
11
+ super
12
+ end
13
+
14
+ private
15
+
16
+ def generate_removal(node)
17
+ loc = node.location
18
+
19
+ add_mutation(
20
+ offset: loc.start_offset,
21
+ length: loc.length,
22
+ replacement: "nil",
23
+ node: node
24
+ )
25
+ end
26
+
27
+ def generate_nil_value(node)
28
+ return if node.arguments.nil?
29
+
30
+ args_loc = node.arguments.location
31
+
32
+ add_mutation(
33
+ offset: args_loc.start_offset,
34
+ length: args_loc.length,
35
+ replacement: "nil",
36
+ node: node
37
+ )
38
+ end
39
+
40
+ def generate_break_swap(node)
41
+ keyword_loc = node.keyword_loc
42
+
43
+ add_mutation(
44
+ offset: keyword_loc.start_offset,
45
+ length: keyword_loc.length,
46
+ replacement: "break",
47
+ node: node
48
+ )
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::RedoStatement < Evilution::Mutator::Base
6
+ def visit_redo_node(node)
7
+ loc = node.location
8
+
9
+ add_mutation(
10
+ offset: loc.start_offset,
11
+ length: loc.length,
12
+ replacement: "nil",
13
+ node: node
14
+ )
15
+
16
+ super
17
+ end
18
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::RescueBodyReplacement < Evilution::Mutator::Base
6
+ def visit_rescue_node(node)
7
+ generate_nil_replacement(node)
8
+ generate_raise_replacement(node)
9
+
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ def generate_nil_replacement(node)
16
+ return if node.statements.nil?
17
+
18
+ body_loc = node.statements.location
19
+ indent = " " * indentation_of(body_loc.start_offset)
20
+
21
+ add_mutation(
22
+ offset: body_loc.start_offset,
23
+ length: body_loc.length,
24
+ replacement: "#{indent}nil".lstrip,
25
+ node: node
26
+ )
27
+ end
28
+
29
+ def generate_raise_replacement(node)
30
+ return if bare_raise?(node)
31
+
32
+ if node.statements.nil?
33
+ insert_raise_into_empty(node)
34
+ else
35
+ replace_body_with_raise(node)
36
+ end
37
+ end
38
+
39
+ def replace_body_with_raise(node)
40
+ body_loc = node.statements.location
41
+ indent = " " * indentation_of(body_loc.start_offset)
42
+
43
+ add_mutation(
44
+ offset: body_loc.start_offset,
45
+ length: body_loc.length,
46
+ replacement: "#{indent}raise".lstrip,
47
+ node: node
48
+ )
49
+ end
50
+
51
+ def insert_raise_into_empty(node)
52
+ insert_offset = rescue_line_end(node)
53
+ indent = " " * (indentation_of(node.keyword_loc.start_offset) + 2)
54
+
55
+ add_mutation(
56
+ offset: insert_offset,
57
+ length: 0,
58
+ replacement: "\n#{indent}raise",
59
+ node: node
60
+ )
61
+ end
62
+
63
+ def bare_raise?(node)
64
+ return false if node.statements.nil?
65
+
66
+ body = node.statements.body
67
+ body.length == 1 &&
68
+ body.first.is_a?(Prism::CallNode) &&
69
+ body.first.name == :raise &&
70
+ body.first.arguments.nil? &&
71
+ body.first.receiver.nil?
72
+ end
73
+
74
+ def rescue_line_end(node)
75
+ if node.reference
76
+ node.reference.location.start_offset + node.reference.location.length
77
+ elsif node.exceptions.any?
78
+ last_exc = node.exceptions.last
79
+ last_exc.location.start_offset + last_exc.location.length
80
+ else
81
+ node.keyword_loc.start_offset + node.keyword_loc.length
82
+ end
83
+ end
84
+
85
+ def indentation_of(offset)
86
+ pos = offset - 1
87
+ col = 0
88
+ while pos >= 0 && @file_source[pos] != "\n"
89
+ col += 1
90
+ pos -= 1
91
+ end
92
+ col
93
+ end
94
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::RescueRemoval < Evilution::Mutator::Base
6
+ def visit_rescue_node(node)
7
+ remove_start = line_start_before(node.keyword_loc.start_offset)
8
+ remove_end = rescue_end_offset(node)
9
+
10
+ add_mutation(
11
+ offset: remove_start,
12
+ length: remove_end - remove_start,
13
+ replacement: "",
14
+ node: node
15
+ )
16
+
17
+ super
18
+ end
19
+
20
+ private
21
+
22
+ def rescue_end_offset(node)
23
+ if node.subsequent
24
+ line_start_before(node.subsequent.keyword_loc.start_offset)
25
+ elsif node.statements
26
+ node.statements.location.start_offset + node.statements.location.length
27
+ else
28
+ node.keyword_loc.start_offset + node.keyword_loc.length
29
+ end
30
+ end
31
+
32
+ def line_start_before(offset)
33
+ pos = offset - 1
34
+ pos -= 1 while pos.positive? && @file_source[pos] != "\n"
35
+ pos
36
+ end
37
+ end
@@ -14,13 +14,22 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
14
14
  detect: [:find],
15
15
  find: [:detect],
16
16
  each_with_object: [:inject],
17
- inject: [:each_with_object],
17
+ inject: %i[each_with_object sum],
18
18
  reverse_each: [:each],
19
19
  each: [:reverse_each],
20
20
  length: [:size],
21
21
  size: [:length],
22
22
  values_at: [:fetch_values],
23
- fetch_values: [:values_at]
23
+ fetch_values: [:values_at],
24
+ sum: [:inject],
25
+ count: [:size],
26
+ select: [:filter],
27
+ filter: [:select],
28
+ to_s: [:to_i],
29
+ to_i: [:to_s],
30
+ to_f: [:to_i],
31
+ to_a: [:to_h],
32
+ to_h: [:to_a]
24
33
  }.freeze
25
34
 
26
35
  def visit_call_node(node)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../operator"
4
+
5
+ class Evilution::Mutator::Operator::ZsuperRemoval < Evilution::Mutator::Base
6
+ def visit_forwarding_super_node(node)
7
+ add_mutation(
8
+ offset: node.location.start_offset,
9
+ length: node.location.length,
10
+ replacement: "nil",
11
+ node: node
12
+ )
13
+
14
+ super
15
+ end
16
+ end
@@ -35,7 +35,23 @@ class Evilution::Mutator::Registry
35
35
  Evilution::Mutator::Operator::ArgumentNilSubstitution,
36
36
  Evilution::Mutator::Operator::CompoundAssignment,
37
37
  Evilution::Mutator::Operator::MixinRemoval,
38
- Evilution::Mutator::Operator::SuperclassRemoval
38
+ Evilution::Mutator::Operator::SuperclassRemoval,
39
+ Evilution::Mutator::Operator::LocalVariableAssignment,
40
+ Evilution::Mutator::Operator::InstanceVariableWrite,
41
+ Evilution::Mutator::Operator::ClassVariableWrite,
42
+ Evilution::Mutator::Operator::GlobalVariableWrite,
43
+ Evilution::Mutator::Operator::RescueRemoval,
44
+ Evilution::Mutator::Operator::RescueBodyReplacement,
45
+ Evilution::Mutator::Operator::InlineRescue,
46
+ Evilution::Mutator::Operator::EnsureRemoval,
47
+ Evilution::Mutator::Operator::BreakStatement,
48
+ Evilution::Mutator::Operator::NextStatement,
49
+ Evilution::Mutator::Operator::RedoStatement,
50
+ Evilution::Mutator::Operator::BangMethod,
51
+ Evilution::Mutator::Operator::BitwiseReplacement,
52
+ Evilution::Mutator::Operator::BitwiseComplement,
53
+ Evilution::Mutator::Operator::ZsuperRemoval,
54
+ Evilution::Mutator::Operator::ExplicitSuperMutation
39
55
  ].each { |op| registry.register(op) }
40
56
  registry
41
57
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../reporter"
4
+
5
+ class Evilution::Reporter::ProgressBar
6
+ attr_reader :total, :width, :completed, :killed, :survived
7
+
8
+ def initialize(total:, output: $stdout, width: 30)
9
+ @total = total
10
+ @output = output
11
+ @width = width
12
+ @completed = 0
13
+ @killed = 0
14
+ @survived = 0
15
+ @tty = self.class.tty?(output)
16
+ @start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
17
+ end
18
+
19
+ def tick(status:)
20
+ @completed += 1
21
+ @killed += 1 if status == :killed
22
+ @survived += 1 if status == :survived
23
+ render
24
+ end
25
+
26
+ def render
27
+ line = "#{bar_string} #{stats_string} | #{time_string}"
28
+ if @tty
29
+ @output.print("\r#{line}")
30
+ else
31
+ @output.puts(line)
32
+ end
33
+ end
34
+
35
+ def finish
36
+ render
37
+ @output.print("\n") if @tty
38
+ end
39
+
40
+ def self.tty?(io)
41
+ io.respond_to?(:tty?) && io.tty?
42
+ end
43
+
44
+ private
45
+
46
+ def bar_string
47
+ fraction = @total.positive? ? @completed.to_f / @total : 0
48
+ raw_filled = (fraction * @width).to_i
49
+ filled = raw_filled.clamp(0, @width)
50
+
51
+ interior = if filled <= 0
52
+ " " * @width
53
+ elsif filled >= @width
54
+ "#{"=" * (@width - 1)}>"
55
+ else
56
+ "#{"=" * (filled - 1)}>#{" " * (@width - filled)}"
57
+ end
58
+
59
+ "[#{interior}]"
60
+ end
61
+
62
+ def stats_string
63
+ "#{@completed}/#{@total} mutations | #{@killed} killed | #{@survived} survived"
64
+ end
65
+
66
+ def time_string
67
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
68
+ remaining = estimate_remaining(elapsed)
69
+ "#{format_time(elapsed)} elapsed | ~#{format_time(remaining)} remaining"
70
+ end
71
+
72
+ def estimate_remaining(elapsed)
73
+ return 0 unless @completed.positive?
74
+
75
+ remaining = (@total - @completed) * (elapsed / @completed)
76
+ [remaining, 0].max
77
+ end
78
+
79
+ def format_time(seconds)
80
+ mins = (seconds / 60).to_i
81
+ secs = (seconds % 60).to_i
82
+ format("%<mins>02d:%<secs>02d", mins: mins, secs: secs)
83
+ end
84
+ end