evilution 0.16.1 → 0.18.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 +47 -46
- data/CHANGELOG.md +48 -0
- data/README.md +143 -50
- data/docs/ast_pattern_syntax.md +210 -0
- data/lib/evilution/ast/pattern/filter.rb +25 -0
- data/lib/evilution/ast/pattern/matcher.rb +107 -0
- data/lib/evilution/ast/pattern/parser.rb +185 -0
- data/lib/evilution/ast/pattern.rb +4 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +52 -0
- data/lib/evilution/cli.rb +400 -24
- data/lib/evilution/config.rb +43 -2
- data/lib/evilution/disable_comment.rb +90 -0
- data/lib/evilution/hooks/loader.rb +35 -0
- data/lib/evilution/hooks/registry.rb +60 -0
- data/lib/evilution/hooks.rb +58 -0
- data/lib/evilution/integration/base.rb +4 -0
- data/lib/evilution/integration/rspec.rb +6 -2
- data/lib/evilution/isolation/fork.rb +5 -0
- data/lib/evilution/mcp/session_diff_tool.rb +5 -35
- data/lib/evilution/mutator/base.rb +4 -1
- data/lib/evilution/mutator/operator/collection_return.rb +33 -0
- data/lib/evilution/mutator/operator/defined_check.rb +16 -0
- data/lib/evilution/mutator/operator/index_assignment_removal.rb +18 -0
- data/lib/evilution/mutator/operator/index_to_dig.rb +58 -0
- data/lib/evilution/mutator/operator/index_to_fetch.rb +30 -0
- data/lib/evilution/mutator/operator/keyword_argument.rb +91 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +2 -1
- data/lib/evilution/mutator/operator/multiple_assignment.rb +47 -0
- data/lib/evilution/mutator/operator/pattern_matching_alternative.rb +46 -0
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +97 -0
- data/lib/evilution/mutator/operator/pattern_matching_guard.rb +44 -0
- data/lib/evilution/mutator/operator/regex_capture.rb +43 -0
- data/lib/evilution/mutator/operator/scalar_return.rb +37 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +46 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +2 -1
- data/lib/evilution/mutator/operator/yield_statement.rb +51 -0
- data/lib/evilution/mutator/registry.rb +17 -3
- data/lib/evilution/parallel/pool.rb +7 -51
- data/lib/evilution/parallel/work_queue.rb +224 -0
- data/lib/evilution/reporter/cli.rb +22 -1
- data/lib/evilution/reporter/html.rb +76 -3
- data/lib/evilution/reporter/json.rb +23 -2
- data/lib/evilution/reporter/suggestion.rb +115 -1
- data/lib/evilution/result/summary.rb +20 -2
- data/lib/evilution/runner.rb +133 -13
- data/lib/evilution/session/diff.rb +85 -0
- data/lib/evilution/session/store.rb +5 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +23 -0
- metadata +28 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "matcher"
|
|
4
|
+
|
|
5
|
+
class Evilution::AST::Pattern::Parser
|
|
6
|
+
def initialize(input)
|
|
7
|
+
@input = input.strip
|
|
8
|
+
@pos = 0
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse
|
|
12
|
+
raise Evilution::ConfigError, "invalid pattern: empty string" if @input.empty?
|
|
13
|
+
|
|
14
|
+
result = parse_pattern
|
|
15
|
+
skip_whitespace
|
|
16
|
+
raise Evilution::ConfigError, "unexpected characters at position #{@pos}: #{@input[@pos..]}" unless @pos >= @input.length
|
|
17
|
+
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def parse_pattern
|
|
24
|
+
skip_whitespace
|
|
25
|
+
|
|
26
|
+
if peek_string("**")
|
|
27
|
+
advance(2)
|
|
28
|
+
Evilution::AST::Pattern::DeepWildcardMatcher.new
|
|
29
|
+
elsif current_char == "_" && !identifier_continues?(1)
|
|
30
|
+
advance(1)
|
|
31
|
+
Evilution::AST::Pattern::AnyNodeMatcher.new
|
|
32
|
+
else
|
|
33
|
+
parse_node_pattern
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_node_pattern
|
|
38
|
+
node_type = consume_identifier
|
|
39
|
+
skip_whitespace
|
|
40
|
+
|
|
41
|
+
attributes = {}
|
|
42
|
+
if current_char == "{"
|
|
43
|
+
advance(1)
|
|
44
|
+
attributes = parse_attributes
|
|
45
|
+
skip_whitespace
|
|
46
|
+
expect_char("}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Evilution::AST::Pattern::NodeMatcher.new(node_type, attributes)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_attributes
|
|
53
|
+
attrs = {}
|
|
54
|
+
skip_whitespace
|
|
55
|
+
|
|
56
|
+
return attrs if current_char == "}"
|
|
57
|
+
|
|
58
|
+
loop do
|
|
59
|
+
skip_whitespace
|
|
60
|
+
name = consume_identifier
|
|
61
|
+
skip_whitespace
|
|
62
|
+
expect_char("=", "expected '=' after attribute name '#{name}'")
|
|
63
|
+
skip_whitespace
|
|
64
|
+
value = parse_value
|
|
65
|
+
attrs[name] = value
|
|
66
|
+
skip_whitespace
|
|
67
|
+
|
|
68
|
+
break unless current_char == ","
|
|
69
|
+
|
|
70
|
+
advance(1)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
attrs
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_value
|
|
77
|
+
skip_whitespace
|
|
78
|
+
|
|
79
|
+
if current_char == "!"
|
|
80
|
+
advance(1)
|
|
81
|
+
skip_whitespace
|
|
82
|
+
inner = parse_value
|
|
83
|
+
Evilution::AST::Pattern::NegationMatcher.new(inner)
|
|
84
|
+
elsif current_char == "*" && !peek_string("**")
|
|
85
|
+
advance(1)
|
|
86
|
+
Evilution::AST::Pattern::WildcardValueMatcher.new
|
|
87
|
+
elsif peek_string("**")
|
|
88
|
+
advance(2)
|
|
89
|
+
Evilution::AST::Pattern::DeepWildcardMatcher.new
|
|
90
|
+
elsif current_char == "_" && !identifier_continues?(1)
|
|
91
|
+
advance(1)
|
|
92
|
+
Evilution::AST::Pattern::AnyNodeMatcher.new
|
|
93
|
+
else
|
|
94
|
+
parse_value_or_nested
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_value_or_nested
|
|
99
|
+
id = consume_identifier
|
|
100
|
+
skip_whitespace
|
|
101
|
+
|
|
102
|
+
if current_char == "{"
|
|
103
|
+
advance(1)
|
|
104
|
+
attrs = parse_attributes
|
|
105
|
+
skip_whitespace
|
|
106
|
+
expect_char("}")
|
|
107
|
+
Evilution::AST::Pattern::NodeMatcher.new(id, attrs)
|
|
108
|
+
else
|
|
109
|
+
parse_alternatives_from(id)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def parse_alternatives_from(first)
|
|
114
|
+
values = [first]
|
|
115
|
+
|
|
116
|
+
while current_char == "|"
|
|
117
|
+
advance(1)
|
|
118
|
+
skip_whitespace
|
|
119
|
+
if current_char == "*" && !peek_string("**")
|
|
120
|
+
advance(1)
|
|
121
|
+
values << "*"
|
|
122
|
+
else
|
|
123
|
+
values << consume_identifier
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if values.length == 1
|
|
128
|
+
Evilution::AST::Pattern::ValueMatcher.new(first)
|
|
129
|
+
else
|
|
130
|
+
Evilution::AST::Pattern::AlternativesMatcher.new(values)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def consume_identifier
|
|
135
|
+
raise Evilution::ConfigError, "unexpected end of pattern at position #{@pos}" if current_char.nil?
|
|
136
|
+
|
|
137
|
+
unless current_char.match?(/[a-zA-Z_]/)
|
|
138
|
+
raise Evilution::ConfigError, "invalid identifier starting with '#{current_char}' at position #{@pos}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
start = @pos
|
|
142
|
+
advance(1)
|
|
143
|
+
advance(1) while @pos < @input.length && @input[@pos].match?(/[a-zA-Z0-9_]/)
|
|
144
|
+
|
|
145
|
+
@input[start...@pos]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def skip_whitespace
|
|
149
|
+
advance(1) while @pos < @input.length && @input[@pos] == " "
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def current_char
|
|
153
|
+
@pos < @input.length ? @input[@pos] : nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def peek_string(str)
|
|
157
|
+
@input[@pos, str.length] == str
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def identifier_continues?(offset)
|
|
161
|
+
char = @input[@pos + offset]
|
|
162
|
+
char && char.match?(/[a-zA-Z0-9_]/)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def advance(n)
|
|
166
|
+
@pos += n
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def expect_char(char, message = nil)
|
|
170
|
+
current = current_char
|
|
171
|
+
|
|
172
|
+
if current == char
|
|
173
|
+
advance(1)
|
|
174
|
+
return
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if current.nil?
|
|
178
|
+
raise Evilution::ConfigError,
|
|
179
|
+
(message || "unexpected end of pattern, expected '#{char}' at position #{@pos}")
|
|
180
|
+
else
|
|
181
|
+
raise Evilution::ConfigError,
|
|
182
|
+
(message || "unexpected character '#{current}', expected '#{char}' at position #{@pos}")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
class Evilution::AST::SorbetSigDetector
|
|
6
|
+
def call(source)
|
|
7
|
+
return [] if source.empty?
|
|
8
|
+
|
|
9
|
+
result = Prism.parse(source)
|
|
10
|
+
return [] if result.failure?
|
|
11
|
+
|
|
12
|
+
ranges = []
|
|
13
|
+
collect_sig_ranges(result.value, ranges, :byte)
|
|
14
|
+
ranges
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def line_ranges(source)
|
|
18
|
+
return [] if source.empty?
|
|
19
|
+
|
|
20
|
+
result = Prism.parse(source)
|
|
21
|
+
return [] if result.failure?
|
|
22
|
+
|
|
23
|
+
ranges = []
|
|
24
|
+
collect_sig_ranges(result.value, ranges, :line)
|
|
25
|
+
ranges
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def collect_sig_ranges(node, ranges, mode)
|
|
31
|
+
if sig_block?(node)
|
|
32
|
+
loc = node.location
|
|
33
|
+
ranges << if mode == :byte
|
|
34
|
+
(loc.start_offset...loc.end_offset)
|
|
35
|
+
else
|
|
36
|
+
(loc.start_line..loc.end_line)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
node.child_nodes.each do |child|
|
|
41
|
+
collect_sig_ranges(child, ranges, mode) if child
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sig_block?(node)
|
|
46
|
+
node.is_a?(Prism::CallNode) &&
|
|
47
|
+
node.name == :sig &&
|
|
48
|
+
node.receiver.nil? &&
|
|
49
|
+
node.arguments.nil? &&
|
|
50
|
+
!node.block.nil?
|
|
51
|
+
end
|
|
52
|
+
end
|