evilution 0.13.0 → 0.15.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 +17 -17
- data/CHANGELOG.md +39 -0
- data/lib/evilution/ast/inheritance_scanner.rb +70 -0
- data/lib/evilution/ast/parser.rb +73 -68
- data/lib/evilution/ast/source_surgeon.rb +7 -9
- data/lib/evilution/ast.rb +4 -0
- data/lib/evilution/baseline.rb +73 -75
- data/lib/evilution/cache.rb +75 -77
- data/lib/evilution/cli.rb +412 -173
- data/lib/evilution/config.rb +141 -136
- data/lib/evilution/equivalent/detector.rb +29 -27
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +32 -33
- data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
- data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
- data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
- data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
- data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
- data/lib/evilution/equivalent/heuristic.rb +6 -0
- data/lib/evilution/equivalent.rb +4 -0
- data/lib/evilution/git/changed_files.rb +35 -37
- data/lib/evilution/git.rb +4 -0
- data/lib/evilution/integration/base.rb +5 -7
- data/lib/evilution/integration/rspec.rb +114 -116
- data/lib/evilution/integration.rb +4 -0
- data/lib/evilution/isolation/fork.rb +98 -100
- data/lib/evilution/isolation/in_process.rb +59 -61
- data/lib/evilution/isolation.rb +4 -0
- data/lib/evilution/mcp/mutate_tool.rb +172 -143
- data/lib/evilution/mcp/server.rb +12 -11
- data/lib/evilution/mcp/session_diff_tool.rb +89 -0
- data/lib/evilution/mcp/session_list_tool.rb +46 -0
- data/lib/evilution/mcp/session_show_tool.rb +53 -0
- data/lib/evilution/mcp.rb +4 -0
- data/lib/evilution/memory/leak_check.rb +80 -84
- data/lib/evilution/memory.rb +34 -36
- data/lib/evilution/mutation.rb +40 -42
- data/lib/evilution/mutator/base.rb +62 -48
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
- data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
- data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
- data/lib/evilution/mutator/operator/array_literal.rb +18 -22
- data/lib/evilution/mutator/operator/block_removal.rb +16 -20
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
- data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
- data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
- data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
- data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
- data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
- data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
- data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
- data/lib/evilution/mutator/operator/float_literal.rb +22 -26
- data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
- data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
- data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
- data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
- data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
- data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
- data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
- data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
- data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
- data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
- data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
- data/lib/evilution/mutator/operator/string_literal.rb +18 -22
- data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
- data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
- data/lib/evilution/mutator/operator.rb +6 -0
- data/lib/evilution/mutator/registry.rb +56 -56
- data/lib/evilution/mutator.rb +4 -0
- data/lib/evilution/parallel/pool.rb +56 -58
- data/lib/evilution/parallel.rb +4 -0
- data/lib/evilution/reporter/cli.rb +99 -101
- data/lib/evilution/reporter/html.rb +242 -244
- data/lib/evilution/reporter/json.rb +57 -59
- data/lib/evilution/reporter/suggestion.rb +354 -328
- data/lib/evilution/reporter.rb +4 -0
- data/lib/evilution/result/mutation_result.rb +43 -46
- data/lib/evilution/result/summary.rb +80 -81
- data/lib/evilution/result.rb +4 -0
- data/lib/evilution/runner.rb +401 -316
- data/lib/evilution/session/store.rb +147 -0
- data/lib/evilution/session.rb +4 -0
- data/lib/evilution/spec_resolver.rb +49 -47
- data/lib/evilution/subject.rb +14 -16
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +16 -0
- metadata +24 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "mcp"
|
|
5
|
+
require_relative "../session/store"
|
|
6
|
+
|
|
7
|
+
require_relative "../mcp"
|
|
8
|
+
|
|
9
|
+
class Evilution::MCP::SessionShowTool < MCP::Tool
|
|
10
|
+
tool_name "evilution-session-show"
|
|
11
|
+
description "Show full details of a past mutation testing session, " \
|
|
12
|
+
"including survived mutations with diffs."
|
|
13
|
+
input_schema(
|
|
14
|
+
properties: {
|
|
15
|
+
path: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Path to the session JSON file (as returned by evilution-session-list)"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
24
|
+
def call(server_context:, path: nil)
|
|
25
|
+
unless path
|
|
26
|
+
return ::MCP::Tool::Response.new(
|
|
27
|
+
[{ type: "text", text: ::JSON.generate({ error: { type: "config_error", message: "path is required" } }) }],
|
|
28
|
+
error: true
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
store = Evilution::Session::Store.new
|
|
33
|
+
data = store.load(path)
|
|
34
|
+
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(data) }])
|
|
35
|
+
rescue Evilution::Error => e
|
|
36
|
+
::MCP::Tool::Response.new(
|
|
37
|
+
[{ type: "text", text: ::JSON.generate({ error: { type: "not_found", message: e.message } }) }],
|
|
38
|
+
error: true
|
|
39
|
+
)
|
|
40
|
+
rescue ::JSON::ParserError => e
|
|
41
|
+
::MCP::Tool::Response.new(
|
|
42
|
+
[{ type: "text", text: ::JSON.generate({ error: { type: "parse_error", message: e.message } }) }],
|
|
43
|
+
error: true
|
|
44
|
+
)
|
|
45
|
+
rescue SystemCallError => e
|
|
46
|
+
::MCP::Tool::Response.new(
|
|
47
|
+
[{ type: "text", text: ::JSON.generate({ error: { type: "runtime_error", message: e.message } }) }],
|
|
48
|
+
error: true
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -2,90 +2,86 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../memory"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
GC.start
|
|
61
|
-
@samples << Evilution::Memory.rss_kb
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
take_final_sample
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def take_final_sample
|
|
68
|
-
return if (@iterations % sample_interval).zero?
|
|
69
|
-
|
|
70
|
-
GC.start
|
|
71
|
-
@samples << Evilution::Memory.rss_kb
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def sample_interval
|
|
75
|
-
@sample_interval ||= [@iterations / 10, 1].max
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def result
|
|
79
|
-
{
|
|
80
|
-
passed: passed?,
|
|
81
|
-
growth_kb: growth_kb,
|
|
82
|
-
growth_mb: growth_kb ? growth_kb / 1024.0 : nil,
|
|
83
|
-
samples: samples,
|
|
84
|
-
iterations: @iterations,
|
|
85
|
-
max_growth_kb: @max_growth_kb,
|
|
86
|
-
rss_available: rss_available?
|
|
87
|
-
}
|
|
88
|
-
end
|
|
5
|
+
class Evilution::Memory::LeakCheck
|
|
6
|
+
WARMUP_ITERATIONS = 5
|
|
7
|
+
DEFAULT_ITERATIONS = 50
|
|
8
|
+
DEFAULT_MAX_GROWTH_KB = 10_240 # 10 MB
|
|
9
|
+
|
|
10
|
+
attr_reader :samples
|
|
11
|
+
|
|
12
|
+
def initialize(iterations: DEFAULT_ITERATIONS, max_growth_kb: DEFAULT_MAX_GROWTH_KB)
|
|
13
|
+
@iterations = iterations
|
|
14
|
+
@max_growth_kb = max_growth_kb
|
|
15
|
+
@samples = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(&)
|
|
19
|
+
warmup(&)
|
|
20
|
+
measure(&)
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def rss_available?
|
|
25
|
+
!Evilution::Memory.rss_kb.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def growth_kb
|
|
29
|
+
return nil if samples.any?(&:nil?)
|
|
30
|
+
return 0 if samples.size < 2
|
|
31
|
+
|
|
32
|
+
samples.last - samples.first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def passed?
|
|
36
|
+
kb = growth_kb
|
|
37
|
+
return false if kb.nil?
|
|
38
|
+
|
|
39
|
+
kb <= @max_growth_kb
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def warmup(&block)
|
|
45
|
+
WARMUP_ITERATIONS.times { block.call }
|
|
46
|
+
GC.start
|
|
47
|
+
GC.compact if GC.respond_to?(:compact)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def measure(&)
|
|
51
|
+
@samples << Evilution::Memory.rss_kb
|
|
52
|
+
|
|
53
|
+
@iterations.times do |i|
|
|
54
|
+
yield
|
|
55
|
+
|
|
56
|
+
next unless ((i + 1) % sample_interval).zero?
|
|
57
|
+
|
|
58
|
+
GC.start
|
|
59
|
+
@samples << Evilution::Memory.rss_kb
|
|
89
60
|
end
|
|
61
|
+
|
|
62
|
+
take_final_sample
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def take_final_sample
|
|
66
|
+
return if (@iterations % sample_interval).zero?
|
|
67
|
+
|
|
68
|
+
GC.start
|
|
69
|
+
@samples << Evilution::Memory.rss_kb
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sample_interval
|
|
73
|
+
@sample_interval ||= [@iterations / 10, 1].max
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def result
|
|
77
|
+
{
|
|
78
|
+
passed: passed?,
|
|
79
|
+
growth_kb: growth_kb,
|
|
80
|
+
growth_mb: growth_kb ? growth_kb / 1024.0 : nil,
|
|
81
|
+
samples: samples,
|
|
82
|
+
iterations: @iterations,
|
|
83
|
+
max_growth_kb: @max_growth_kb,
|
|
84
|
+
rss_available: rss_available?
|
|
85
|
+
}
|
|
90
86
|
end
|
|
91
87
|
end
|
data/lib/evilution/memory.rb
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Evilution
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
[result, delta_kb]
|
|
38
|
-
end
|
|
3
|
+
module Evilution::Memory
|
|
4
|
+
PROC_STATUS_PATH = "/proc/%d/status"
|
|
5
|
+
RSS_PATTERN = /VmRSS:\s+(\d+)\s+kB/
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def rss_kb
|
|
10
|
+
rss_kb_for(Process.pid)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def rss_mb
|
|
14
|
+
kb = rss_kb
|
|
15
|
+
return nil unless kb
|
|
16
|
+
|
|
17
|
+
kb / 1024.0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def rss_kb_for(pid)
|
|
21
|
+
path = format(PROC_STATUS_PATH, pid)
|
|
22
|
+
content = File.read(path)
|
|
23
|
+
match = content.match(RSS_PATTERN)
|
|
24
|
+
return nil unless match
|
|
25
|
+
|
|
26
|
+
match[1].to_i
|
|
27
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ESRCH
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delta
|
|
32
|
+
before = rss_kb
|
|
33
|
+
result = yield
|
|
34
|
+
after = rss_kb
|
|
35
|
+
delta_kb = before && after ? after - before : nil
|
|
36
|
+
[result, delta_kb]
|
|
39
37
|
end
|
|
40
38
|
end
|
data/lib/evilution/mutation.rb
CHANGED
|
@@ -3,57 +3,55 @@
|
|
|
3
3
|
require "diff/lcs"
|
|
4
4
|
require "diff/lcs/hunk"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
6
|
+
class Evilution::Mutation
|
|
7
|
+
attr_reader :subject, :operator_name, :original_source,
|
|
8
|
+
:mutated_source, :file_path, :line, :column
|
|
9
|
+
|
|
10
|
+
def initialize(subject:, operator_name:, original_source:, mutated_source:, file_path:, line:, column: 0)
|
|
11
|
+
@subject = subject
|
|
12
|
+
@operator_name = operator_name
|
|
13
|
+
@original_source = original_source
|
|
14
|
+
@mutated_source = mutated_source
|
|
15
|
+
@file_path = file_path
|
|
16
|
+
@line = line
|
|
17
|
+
@column = column
|
|
18
|
+
@diff = nil
|
|
19
|
+
end
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
def diff
|
|
22
|
+
@diff ||= compute_diff
|
|
23
|
+
end
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
def strip_sources!
|
|
26
|
+
diff # ensure diff is cached before clearing sources
|
|
27
|
+
@original_source = nil
|
|
28
|
+
@mutated_source = nil
|
|
29
|
+
end
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
private
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
def compute_diff
|
|
34
|
+
original_lines = original_source.lines
|
|
35
|
+
mutated_lines = mutated_source.lines
|
|
36
|
+
diffs = ::Diff::LCS.diff(original_lines, mutated_lines)
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
return "" if diffs.empty?
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end
|
|
40
|
+
result = []
|
|
41
|
+
diffs.flatten(1).each do |change|
|
|
42
|
+
case change.action
|
|
43
|
+
when "-"
|
|
44
|
+
result << "- #{change.element.chomp}"
|
|
45
|
+
when "+"
|
|
46
|
+
result << "+ #{change.element.chomp}"
|
|
49
47
|
end
|
|
50
|
-
result.join("\n")
|
|
51
48
|
end
|
|
49
|
+
result.join("\n")
|
|
50
|
+
end
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
public
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
end
|
|
54
|
+
def to_s
|
|
55
|
+
"#{operator_name}: #{file_path}:#{line}"
|
|
58
56
|
end
|
|
59
57
|
end
|
|
@@ -2,53 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
5
|
+
require_relative "../mutator"
|
|
6
|
+
|
|
7
|
+
class Evilution::Mutator::Base < Prism::Visitor
|
|
8
|
+
attr_reader :mutations
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@mutations = []
|
|
12
|
+
@subject = nil
|
|
13
|
+
@file_source = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(subject)
|
|
17
|
+
@subject = subject
|
|
18
|
+
@file_source = File.read(subject.file_path)
|
|
19
|
+
@mutations = []
|
|
20
|
+
visit(subject.node)
|
|
21
|
+
@mutations
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def add_mutation(offset:, length:, replacement:, node:)
|
|
27
|
+
mutated_source = Evilution::AST::SourceSurgeon.apply(
|
|
28
|
+
@file_source,
|
|
29
|
+
offset: offset,
|
|
30
|
+
length: length,
|
|
31
|
+
replacement: replacement
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@mutations << Evilution::Mutation.new(
|
|
35
|
+
subject: @subject,
|
|
36
|
+
operator_name: self.class.operator_name,
|
|
37
|
+
original_source: @file_source,
|
|
38
|
+
mutated_source: mutated_source,
|
|
39
|
+
file_path: @subject.file_path,
|
|
40
|
+
line: node.location.start_line,
|
|
41
|
+
column: node.location.start_column
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.operator_name
|
|
46
|
+
class_name = name || "anonymous"
|
|
47
|
+
class_name.split("::").last
|
|
48
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
49
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
50
|
+
.downcase
|
|
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, {})
|
|
53
67
|
end
|
|
54
68
|
end
|
|
@@ -1,42 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
super
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def positional_only?(args)
|
|
37
|
-
args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
|
|
38
|
-
end
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator::Base
|
|
6
|
+
SKIP_TYPES = [
|
|
7
|
+
Prism::SplatNode,
|
|
8
|
+
Prism::KeywordHashNode,
|
|
9
|
+
Prism::BlockArgumentNode,
|
|
10
|
+
Prism::ForwardingArgumentsNode
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
args = node.arguments&.arguments
|
|
15
|
+
|
|
16
|
+
if args && args.length >= 1 && positional_only?(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
|
+
)
|
|
39
27
|
end
|
|
40
28
|
end
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def positional_only?(args)
|
|
36
|
+
args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
|
|
41
37
|
end
|
|
42
38
|
end
|
|
@@ -1,42 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
super
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def positional_only?(args)
|
|
37
|
-
args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
|
|
38
|
-
end
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
6
|
+
SKIP_TYPES = [
|
|
7
|
+
Prism::SplatNode,
|
|
8
|
+
Prism::KeywordHashNode,
|
|
9
|
+
Prism::BlockArgumentNode,
|
|
10
|
+
Prism::ForwardingArgumentsNode
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
args = node.arguments&.arguments
|
|
15
|
+
|
|
16
|
+
if args && args.length >= 2 && positional_only?(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
|
+
)
|
|
39
27
|
end
|
|
40
28
|
end
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def positional_only?(args)
|
|
36
|
+
args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
|
|
41
37
|
end
|
|
42
38
|
end
|