evilution 0.14.0 → 0.16.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/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +41 -41
- data/CHANGELOG.md +43 -0
- data/lib/evilution/ast/inheritance_scanner.rb +70 -0
- data/lib/evilution/ast/parser.rb +10 -6
- data/lib/evilution/cli.rb +6 -1
- data/lib/evilution/config.rb +7 -1
- data/lib/evilution/equivalent/detector.rb +5 -1
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +5 -2
- data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
- data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
- data/lib/evilution/mutator/base.rb +16 -0
- data/lib/evilution/mutator/operator/bang_method.rb +48 -0
- data/lib/evilution/mutator/operator/bitwise_complement.rb +31 -0
- data/lib/evilution/mutator/operator/bitwise_replacement.rb +30 -0
- data/lib/evilution/mutator/operator/break_statement.rb +50 -0
- data/lib/evilution/mutator/operator/class_variable_write.rb +25 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +25 -1
- data/lib/evilution/mutator/operator/ensure_removal.rb +27 -0
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +47 -0
- data/lib/evilution/mutator/operator/global_variable_write.rb +25 -0
- data/lib/evilution/mutator/operator/inline_rescue.rb +39 -0
- data/lib/evilution/mutator/operator/instance_variable_write.rb +25 -0
- data/lib/evilution/mutator/operator/local_variable_assignment.rb +16 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
- data/lib/evilution/mutator/operator/next_statement.rb +50 -0
- data/lib/evilution/mutator/operator/redo_statement.rb +18 -0
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +94 -0
- data/lib/evilution/mutator/operator/rescue_removal.rb +37 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +11 -2
- data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
- data/lib/evilution/mutator/operator/zsuper_removal.rb +16 -0
- data/lib/evilution/mutator/registry.rb +19 -1
- data/lib/evilution/reporter/progress_bar.rb +84 -0
- data/lib/evilution/reporter/suggestion.rb +253 -1
- data/lib/evilution/runner.rb +105 -19
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +20 -0
- metadata +24 -2
|
@@ -4,6 +4,8 @@ require_relative "heuristic/noop_source"
|
|
|
4
4
|
require_relative "heuristic/method_body_nil"
|
|
5
5
|
require_relative "heuristic/alias_swap"
|
|
6
6
|
require_relative "heuristic/dead_code"
|
|
7
|
+
require_relative "heuristic/arithmetic_identity"
|
|
8
|
+
require_relative "heuristic/comment_marking"
|
|
7
9
|
|
|
8
10
|
require_relative "../equivalent"
|
|
9
11
|
|
|
@@ -34,7 +36,9 @@ class Evilution::Equivalent::Detector
|
|
|
34
36
|
Evilution::Equivalent::Heuristic::NoopSource.new,
|
|
35
37
|
Evilution::Equivalent::Heuristic::MethodBodyNil.new,
|
|
36
38
|
Evilution::Equivalent::Heuristic::AliasSwap.new,
|
|
37
|
-
Evilution::Equivalent::Heuristic::DeadCode.new
|
|
39
|
+
Evilution::Equivalent::Heuristic::DeadCode.new,
|
|
40
|
+
Evilution::Equivalent::Heuristic::ArithmeticIdentity.new,
|
|
41
|
+
Evilution::Equivalent::Heuristic::CommentMarking.new
|
|
38
42
|
]
|
|
39
43
|
end
|
|
40
44
|
end
|
|
@@ -6,11 +6,14 @@ class Evilution::Equivalent::Heuristic::AliasSwap
|
|
|
6
6
|
ALIAS_PAIRS = Set[
|
|
7
7
|
Set[:detect, :find],
|
|
8
8
|
Set[:length, :size],
|
|
9
|
-
Set[:collect, :map]
|
|
9
|
+
Set[:collect, :map],
|
|
10
|
+
Set[:count, :length]
|
|
10
11
|
].freeze
|
|
11
12
|
|
|
13
|
+
MATCHING_OPERATORS = Set["send_mutation", "collection_replacement"].freeze
|
|
14
|
+
|
|
12
15
|
def match?(mutation)
|
|
13
|
-
return false unless mutation.operator_name
|
|
16
|
+
return false unless MATCHING_OPERATORS.include?(mutation.operator_name)
|
|
14
17
|
|
|
15
18
|
diff = mutation.diff
|
|
16
19
|
removed = extract_method(diff, "- ")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../heuristic"
|
|
4
|
+
|
|
5
|
+
class Evilution::Equivalent::Heuristic::ArithmeticIdentity
|
|
6
|
+
# Patterns where the original expression is an arithmetic identity operation.
|
|
7
|
+
# "x + 0" is identity (equals x), so mutating the 0 to something else means
|
|
8
|
+
# the original was a no-op — if the test doesn't catch it, it's likely equivalent.
|
|
9
|
+
ADDITIVE_IDENTITY = /[\w)\].]+\s*[+-]\s*0\b|\b0\s*\+\s*[\w(\[]/
|
|
10
|
+
MULTIPLICATIVE_IDENTITY = %r{[\w)\].]+\s*[*/]\s*1\b|\b1\s*\*\s*[\w(\[]}
|
|
11
|
+
EXPONENT_IDENTITY = /[\w)\].]+\s*\*\*\s*1\b/
|
|
12
|
+
|
|
13
|
+
def match?(mutation)
|
|
14
|
+
return false unless mutation.operator_name == "integer_literal"
|
|
15
|
+
|
|
16
|
+
removed = diff_line(mutation.diff, "- ")
|
|
17
|
+
return false unless removed
|
|
18
|
+
|
|
19
|
+
content = removed.sub(/^- /, "")
|
|
20
|
+
content.match?(ADDITIVE_IDENTITY) ||
|
|
21
|
+
content.match?(MULTIPLICATIVE_IDENTITY) ||
|
|
22
|
+
content.match?(EXPONENT_IDENTITY)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def diff_line(diff, prefix)
|
|
28
|
+
diff.split("\n").find { |l| l.start_with?(prefix) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../heuristic"
|
|
4
|
+
|
|
5
|
+
class Evilution::Equivalent::Heuristic::CommentMarking
|
|
6
|
+
MARKER = /#\s*evilution:equivalent\b/
|
|
7
|
+
|
|
8
|
+
def match?(mutation)
|
|
9
|
+
source = mutation.original_source
|
|
10
|
+
return false unless source
|
|
11
|
+
|
|
12
|
+
lines = source.lines
|
|
13
|
+
line_index = mutation.line - 1
|
|
14
|
+
return false if line_index.negative? || line_index >= lines.length
|
|
15
|
+
|
|
16
|
+
return true if lines[line_index].match?(MARKER)
|
|
17
|
+
return true if line_index.positive? && lines[line_index - 1].match?(MARKER)
|
|
18
|
+
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -49,4 +49,20 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
49
49
|
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
50
50
|
.downcase
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
@parse_cache = {}
|
|
54
|
+
|
|
55
|
+
def self.parsed_tree_for(file_path, file_source)
|
|
56
|
+
cache = Evilution::Mutator::Base.instance_variable_get(:@parse_cache)
|
|
57
|
+
entry = cache[file_path]
|
|
58
|
+
return entry[:tree] if entry && entry[:source_hash] == file_source.hash
|
|
59
|
+
|
|
60
|
+
tree = Prism.parse(file_source).value
|
|
61
|
+
cache[file_path] = { source_hash: file_source.hash, tree: tree }
|
|
62
|
+
tree
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.clear_parse_cache!
|
|
66
|
+
Evilution::Mutator::Base.instance_variable_set(:@parse_cache, {})
|
|
67
|
+
end
|
|
52
68
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::BangMethod < Evilution::Mutator::Base
|
|
6
|
+
KNOWN_BANG_PAIRS = %i[
|
|
7
|
+
sort map collect select reject uniq compact flatten
|
|
8
|
+
shuffle reverse slice gsub sub strip chomp chop squeeze
|
|
9
|
+
delete encode merge update save
|
|
10
|
+
].to_set.freeze
|
|
11
|
+
|
|
12
|
+
def visit_call_node(node)
|
|
13
|
+
return super unless node.receiver
|
|
14
|
+
|
|
15
|
+
loc = node.message_loc
|
|
16
|
+
return super unless loc
|
|
17
|
+
|
|
18
|
+
name = node.name.to_s
|
|
19
|
+
|
|
20
|
+
if name.end_with?("!")
|
|
21
|
+
generate_non_bang(node, loc, name)
|
|
22
|
+
elsif KNOWN_BANG_PAIRS.include?(node.name)
|
|
23
|
+
generate_bang(node, loc, name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def generate_non_bang(node, loc, name)
|
|
32
|
+
add_mutation(
|
|
33
|
+
offset: loc.start_offset,
|
|
34
|
+
length: loc.length,
|
|
35
|
+
replacement: name.chomp("!"),
|
|
36
|
+
node: node
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate_bang(node, loc, name)
|
|
41
|
+
add_mutation(
|
|
42
|
+
offset: loc.start_offset,
|
|
43
|
+
length: loc.length,
|
|
44
|
+
replacement: "#{name}!",
|
|
45
|
+
node: node
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::BitwiseComplement < Evilution::Mutator::Base
|
|
6
|
+
def visit_call_node(node)
|
|
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 = @file_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
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::BitwiseReplacement < Evilution::Mutator::Base
|
|
6
|
+
REPLACEMENTS = {
|
|
7
|
+
:& => %i[| ^],
|
|
8
|
+
:| => %i[& ^],
|
|
9
|
+
:^ => %i[& |]
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def visit_call_node(node)
|
|
13
|
+
replacements = REPLACEMENTS[node.name]
|
|
14
|
+
return super unless replacements
|
|
15
|
+
|
|
16
|
+
loc = node.message_loc
|
|
17
|
+
return super unless loc
|
|
18
|
+
|
|
19
|
+
replacements.each do |replacement|
|
|
20
|
+
add_mutation(
|
|
21
|
+
offset: loc.start_offset,
|
|
22
|
+
length: loc.length,
|
|
23
|
+
replacement: replacement.to_s,
|
|
24
|
+
node: node
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::BreakStatement < Evilution::Mutator::Base
|
|
6
|
+
def visit_break_node(node)
|
|
7
|
+
generate_removal(node)
|
|
8
|
+
generate_nil_value(node)
|
|
9
|
+
generate_next_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_next_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: "next",
|
|
47
|
+
node: node
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::ClassVariableWrite < Evilution::Mutator::Base
|
|
6
|
+
def visit_class_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
|
|
@@ -17,7 +17,31 @@ class Evilution::Mutator::Operator::CollectionReplacement < Evilution::Mutator::
|
|
|
17
17
|
any?: [:all?],
|
|
18
18
|
all?: [:any?],
|
|
19
19
|
count: [:length],
|
|
20
|
-
length: [:count]
|
|
20
|
+
length: [:count],
|
|
21
|
+
pop: [:shift],
|
|
22
|
+
shift: [:pop],
|
|
23
|
+
push: [:unshift],
|
|
24
|
+
unshift: [:push],
|
|
25
|
+
each_key: [:each_value],
|
|
26
|
+
each_value: [:each_key],
|
|
27
|
+
assoc: [:rassoc],
|
|
28
|
+
rassoc: [:assoc],
|
|
29
|
+
grep: [:grep_v],
|
|
30
|
+
grep_v: [:grep],
|
|
31
|
+
take: [:drop],
|
|
32
|
+
drop: [:take],
|
|
33
|
+
min: [:max],
|
|
34
|
+
max: [:min],
|
|
35
|
+
min_by: [:max_by],
|
|
36
|
+
max_by: [:min_by],
|
|
37
|
+
compact: [:flatten],
|
|
38
|
+
flatten: [:compact],
|
|
39
|
+
zip: [:product],
|
|
40
|
+
product: [:zip],
|
|
41
|
+
first: [:last],
|
|
42
|
+
last: [:first],
|
|
43
|
+
keys: [:values],
|
|
44
|
+
values: [:keys]
|
|
21
45
|
}.freeze
|
|
22
46
|
|
|
23
47
|
def visit_call_node(node)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::EnsureRemoval < Evilution::Mutator::Base
|
|
6
|
+
def visit_ensure_node(node)
|
|
7
|
+
remove_start = line_start_after_newline(node.ensure_keyword_loc.start_offset)
|
|
8
|
+
remove_end = line_start_after_newline(node.end_keyword_loc.start_offset)
|
|
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 line_start_after_newline(offset)
|
|
23
|
+
pos = offset
|
|
24
|
+
pos -= 1 while pos.positive? && @file_source[pos - 1] != "\n"
|
|
25
|
+
pos
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -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,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
|
|
7
|
+
class Evilution::Mutator::Operator::MixinRemoval < Evilution::Mutator::Base
|
|
8
|
+
MIXIN_METHODS = %i[include extend prepend].freeze
|
|
9
|
+
|
|
10
|
+
def call(subject)
|
|
11
|
+
@subject = subject
|
|
12
|
+
@file_source = File.read(subject.file_path)
|
|
13
|
+
@mutations = []
|
|
14
|
+
|
|
15
|
+
tree = self.class.parsed_tree_for(subject.file_path, @file_source)
|
|
16
|
+
enclosing = find_enclosing_scope(tree, subject.line_number)
|
|
17
|
+
return @mutations unless enclosing
|
|
18
|
+
|
|
19
|
+
first_method_line = find_first_method_line(enclosing)
|
|
20
|
+
return @mutations unless first_method_line == subject.line_number
|
|
21
|
+
|
|
22
|
+
find_mixin_calls(enclosing).each do |call_node|
|
|
23
|
+
add_mutation(
|
|
24
|
+
offset: call_node.location.start_offset,
|
|
25
|
+
length: call_node.location.length,
|
|
26
|
+
replacement: "",
|
|
27
|
+
node: call_node
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@mutations
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def find_enclosing_scope(tree, target_line)
|
|
37
|
+
finder = ScopeFinder.new(target_line)
|
|
38
|
+
finder.visit(tree)
|
|
39
|
+
finder.result
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_first_method_line(scope_node)
|
|
43
|
+
return nil unless scope_node.body
|
|
44
|
+
|
|
45
|
+
scope_node.body.body.each do |node|
|
|
46
|
+
return node.location.start_line if node.is_a?(Prism::DefNode)
|
|
47
|
+
end
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def find_mixin_calls(scope_node)
|
|
52
|
+
return [] unless scope_node.body
|
|
53
|
+
|
|
54
|
+
scope_node.body.body.select do |node|
|
|
55
|
+
node.is_a?(Prism::CallNode) &&
|
|
56
|
+
MIXIN_METHODS.include?(node.name) &&
|
|
57
|
+
node.receiver.nil?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Visitor to find the ClassNode or ModuleNode enclosing a given line number.
|
|
62
|
+
class ScopeFinder < Prism::Visitor
|
|
63
|
+
attr_reader :result
|
|
64
|
+
|
|
65
|
+
def initialize(target_line)
|
|
66
|
+
@target_line = target_line
|
|
67
|
+
@result = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def visit_class_node(node)
|
|
71
|
+
@result = node if @target_line.between?(node.location.start_line, node.location.end_line)
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def visit_module_node(node)
|
|
76
|
+
@result = node if @target_line.between?(node.location.start_line, node.location.end_line)
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
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
|