evilution 0.23.0 → 0.25.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +210 -0
  3. data/CHANGELOG.md +51 -0
  4. data/README.md +81 -4
  5. data/exe/evil +6 -0
  6. data/lib/evilution/ast/source_surgeon.rb +15 -1
  7. data/lib/evilution/cli/commands/compare.rb +68 -0
  8. data/lib/evilution/cli/parser/command_extractor.rb +78 -0
  9. data/lib/evilution/cli/parser/file_args.rb +41 -0
  10. data/lib/evilution/cli/parser/options_builder.rb +123 -0
  11. data/lib/evilution/cli/parser/stdin_reader.rb +28 -0
  12. data/lib/evilution/cli/parser.rb +27 -196
  13. data/lib/evilution/cli/printers/compare.rb +159 -0
  14. data/lib/evilution/cli.rb +1 -0
  15. data/lib/evilution/compare/categorizer.rb +109 -0
  16. data/lib/evilution/compare/detector.rb +21 -0
  17. data/lib/evilution/compare/fingerprint.rb +83 -0
  18. data/lib/evilution/compare/normalizer.rb +106 -0
  19. data/lib/evilution/compare/record.rb +16 -0
  20. data/lib/evilution/compare.rb +15 -0
  21. data/lib/evilution/config.rb +178 -3
  22. data/lib/evilution/example_filter.rb +143 -0
  23. data/lib/evilution/integration/base.rb +11 -57
  24. data/lib/evilution/integration/crash_detector.rb +5 -2
  25. data/lib/evilution/integration/minitest.rb +25 -7
  26. data/lib/evilution/integration/minitest_crash_detector.rb +5 -2
  27. data/lib/evilution/integration/rspec.rb +99 -12
  28. data/lib/evilution/isolation/fork.rb +26 -0
  29. data/lib/evilution/isolation/in_process.rb +1 -0
  30. data/lib/evilution/mcp/info_tool.rb +77 -5
  31. data/lib/evilution/mcp/mutate_tool/config_builder.rb +20 -0
  32. data/lib/evilution/mcp/mutate_tool/error_payload.rb +17 -0
  33. data/lib/evilution/mcp/mutate_tool/option_parser.rb +54 -0
  34. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +37 -0
  35. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +31 -0
  36. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +52 -0
  37. data/lib/evilution/mcp/mutate_tool.rb +34 -186
  38. data/lib/evilution/mutation.rb +43 -3
  39. data/lib/evilution/mutator/base.rb +39 -1
  40. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +5 -1
  41. data/lib/evilution/mutator/operator/argument_removal.rb +5 -1
  42. data/lib/evilution/parallel/work_queue.rb +149 -31
  43. data/lib/evilution/parallel_db_warning.rb +68 -0
  44. data/lib/evilution/reporter/cli.rb +38 -11
  45. data/lib/evilution/reporter/html/assets/style.css +85 -0
  46. data/lib/evilution/reporter/html/baseline_keys.rb +28 -0
  47. data/lib/evilution/reporter/html/diff_formatter.rb +27 -0
  48. data/lib/evilution/reporter/html/escape.rb +12 -0
  49. data/lib/evilution/reporter/html/namespace.rb +11 -0
  50. data/lib/evilution/reporter/html/report.rb +68 -0
  51. data/lib/evilution/reporter/html/section.rb +21 -0
  52. data/lib/evilution/reporter/html/sections/baseline_comparison.rb +46 -0
  53. data/lib/evilution/reporter/html/sections/error_details.rb +30 -0
  54. data/lib/evilution/reporter/html/sections/error_entry.rb +22 -0
  55. data/lib/evilution/reporter/html/sections/file_section.rb +62 -0
  56. data/lib/evilution/reporter/html/sections/header.rb +29 -0
  57. data/lib/evilution/reporter/html/sections/mutation_map.rb +32 -0
  58. data/lib/evilution/reporter/html/sections/neutral_details.rb +25 -0
  59. data/lib/evilution/reporter/html/sections/summary_cards.rb +11 -0
  60. data/lib/evilution/reporter/html/sections/survived_details.rb +35 -0
  61. data/lib/evilution/reporter/html/sections/survived_entry.rb +36 -0
  62. data/lib/evilution/reporter/html/sections/truncation_notice.rb +17 -0
  63. data/lib/evilution/reporter/html/sections/unparseable_details.rb +25 -0
  64. data/lib/evilution/reporter/html/sections/unresolved_details.rb +25 -0
  65. data/lib/evilution/reporter/html/sections.rb +4 -0
  66. data/lib/evilution/reporter/html/stylesheet.rb +14 -0
  67. data/lib/evilution/reporter/html/templates/baseline_comparison.html.erb +8 -0
  68. data/lib/evilution/reporter/html/templates/error_details.html.erb +6 -0
  69. data/lib/evilution/reporter/html/templates/error_entry.html.erb +10 -0
  70. data/lib/evilution/reporter/html/templates/file_section.html.erb +12 -0
  71. data/lib/evilution/reporter/html/templates/header.html.erb +4 -0
  72. data/lib/evilution/reporter/html/templates/mutation_map.html.erb +6 -0
  73. data/lib/evilution/reporter/html/templates/neutral_details.html.erb +14 -0
  74. data/lib/evilution/reporter/html/templates/report.html.erb +17 -0
  75. data/lib/evilution/reporter/html/templates/summary_cards.html.erb +26 -0
  76. data/lib/evilution/reporter/html/templates/survived_details.html.erb +21 -0
  77. data/lib/evilution/reporter/html/templates/survived_entry.html.erb +8 -0
  78. data/lib/evilution/reporter/html/templates/truncation_notice.html.erb +1 -0
  79. data/lib/evilution/reporter/html/templates/unparseable_details.html.erb +11 -0
  80. data/lib/evilution/reporter/html/templates/unresolved_details.html.erb +11 -0
  81. data/lib/evilution/reporter/html.rb +11 -390
  82. data/lib/evilution/reporter/json.rb +19 -9
  83. data/lib/evilution/reporter/suggestion/diff_helpers.rb +28 -0
  84. data/lib/evilution/reporter/suggestion/registry.rb +64 -0
  85. data/lib/evilution/reporter/suggestion/templates/generic.rb +55 -0
  86. data/lib/evilution/reporter/suggestion/templates/minitest.rb +659 -0
  87. data/lib/evilution/reporter/suggestion/templates/rspec.rb +613 -0
  88. data/lib/evilution/reporter/suggestion.rb +8 -1327
  89. data/lib/evilution/result/mutation_result.rb +9 -1
  90. data/lib/evilution/result/summary.rb +21 -1
  91. data/lib/evilution/runner/baseline_runner.rb +92 -0
  92. data/lib/evilution/runner/diagnostics.rb +105 -0
  93. data/lib/evilution/runner/isolation_resolver.rb +134 -0
  94. data/lib/evilution/runner/mutation_executor.rb +325 -0
  95. data/lib/evilution/runner/mutation_planner.rb +126 -0
  96. data/lib/evilution/runner/report_publisher.rb +60 -0
  97. data/lib/evilution/runner/subject_pipeline.rb +121 -0
  98. data/lib/evilution/runner.rb +61 -692
  99. data/lib/evilution/source_ast_cache.rb +39 -0
  100. data/lib/evilution/spec_ast_cache.rb +166 -0
  101. data/lib/evilution/spec_resolver.rb +6 -1
  102. data/lib/evilution/spec_selector.rb +39 -0
  103. data/lib/evilution/temp_dir_tracker.rb +23 -3
  104. data/lib/evilution/version.rb +1 -1
  105. data/script/memory_check +7 -5
  106. metadata +75 -2
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../mutate_tool"
4
+ require_relative "../../runner"
5
+ require_relative "../../spec_resolver"
6
+
7
+ module Evilution::MCP::MutateTool::SurvivedEnricher
8
+ def self.call(data, survived_results, config)
9
+ entries = data["survived"]
10
+ return unless entries.is_a?(Array)
11
+
12
+ explicit_spec = explicit_spec_override(config)
13
+ resolver = explicit_spec ? nil : resolver_for_integration(config.integration)
14
+ cache = {}
15
+
16
+ entries.each_with_index do |entry, index|
17
+ result = survived_results[index]
18
+ next unless result
19
+
20
+ mutation = result.mutation
21
+ entry["subject"] = mutation.subject.name
22
+ spec_file = explicit_spec || cache.fetch(mutation.file_path) do
23
+ cache[mutation.file_path] = resolver.call(mutation.file_path)
24
+ end
25
+ entry["spec_file"] = spec_file if spec_file
26
+ entry["next_step"] = build_next_step(mutation, spec_file)
27
+ end
28
+ end
29
+
30
+ def self.explicit_spec_override(config)
31
+ return nil unless config.respond_to?(:spec_files)
32
+
33
+ files = Array(config.spec_files).compact.map(&:to_s).reject(&:empty?)
34
+ files.first
35
+ end
36
+ private_class_method :explicit_spec_override
37
+
38
+ def self.resolver_for_integration(integration)
39
+ integration_class = Evilution::Runner::INTEGRATIONS[integration.to_sym]
40
+ return Evilution::SpecResolver.new unless integration_class
41
+
42
+ integration_class.baseline_options[:spec_resolver] || Evilution::SpecResolver.new
43
+ end
44
+ private_class_method :resolver_for_integration
45
+
46
+ def self.build_next_step(mutation, spec_file)
47
+ target = spec_file || "the covering test file"
48
+ "Add a test in #{target} that fails against this mutation at #{mutation.file_path}:#{mutation.line} " \
49
+ "(#{mutation.subject.name}, #{mutation.operator_name})."
50
+ end
51
+ private_class_method :build_next_step
52
+ end
@@ -5,7 +5,6 @@ require "mcp"
5
5
  require_relative "../config"
6
6
  require_relative "../runner"
7
7
  require_relative "../reporter/json"
8
- require_relative "../reporter/suggestion"
9
8
  require_relative "../spec_resolver"
10
9
 
11
10
  require_relative "../mcp"
@@ -104,194 +103,43 @@ class Evilution::MCP::MutateTool < MCP::Tool
104
103
 
105
104
  class << self
106
105
  def call(server_context:, files: [], verbosity: nil, **opts)
107
- validate_opts!(opts)
108
- parsed_files, line_ranges = parse_files(Array(files))
109
- config_opts = build_config_opts(parsed_files, line_ranges, opts)
110
- config = Evilution::Config.new(**config_opts)
111
- suggest_tests = opts[:suggest_tests]
112
- on_result = build_streaming_callback(server_context, suggest_tests, config.integration)
113
- runner = Evilution::Runner.new(config: config, on_result: on_result)
114
- summary = runner.call
115
- report = Evilution::Reporter::JSON.new(suggest_tests: suggest_tests == true, integration: config.integration).call(summary)
116
- compact = trim_report(report, normalize_verbosity(verbosity), summary.survived_results, config)
106
+ Evilution::MCP::MutateTool::OptionParser.validate!(opts)
107
+ parsed_files, line_ranges = Evilution::MCP::MutateTool::OptionParser.parse_files(Array(files))
108
+ config = Evilution::MCP::MutateTool::ConfigBuilder.build(
109
+ files: parsed_files,
110
+ line_ranges: line_ranges,
111
+ params: opts
112
+ )
113
+ on_result = Evilution::MCP::MutateTool::ProgressStreamer.build(
114
+ server_context: server_context,
115
+ suggest_tests: opts[:suggest_tests],
116
+ integration: config.integration
117
+ )
118
+ summary = Evilution::Runner.new(config: config, on_result: on_result).call
119
+ report = Evilution::Reporter::JSON.new(
120
+ suggest_tests: opts[:suggest_tests] == true,
121
+ integration: config.integration
122
+ ).call(summary)
123
+ normalized_verbosity = Evilution::MCP::MutateTool::OptionParser.normalize_verbosity(verbosity)
124
+ compact = Evilution::MCP::MutateTool::ReportTrimmer.call(
125
+ report,
126
+ verbosity: normalized_verbosity,
127
+ survived_results: summary.survived_results,
128
+ config: config,
129
+ enricher: Evilution::MCP::MutateTool::SurvivedEnricher
130
+ )
117
131
 
118
132
  ::MCP::Tool::Response.new([{ type: "text", text: compact }])
119
133
  rescue Evilution::Error => e
120
- error_payload = build_error_payload(e)
121
- ::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(error_payload) }], error: true)
122
- end
123
-
124
- VALID_VERBOSITIES = %w[full summary minimal].freeze
125
- PASSTHROUGH_KEYS = %i[target timeout jobs fail_fast suggest_tests incremental integration
126
- isolation baseline save_session].freeze
127
- ALLOWED_OPT_KEYS = (PASSTHROUGH_KEYS + %i[spec skip_config]).freeze
128
-
129
- private
130
-
131
- def parse_files(raw_files)
132
- files = []
133
- ranges = {}
134
-
135
- raw_files.each do |arg|
136
- file, range_str = arg.split(":", 2)
137
- files << file
138
- next unless range_str
139
-
140
- ranges[file] = parse_line_range(range_str)
141
- end
142
-
143
- [files, ranges]
144
- end
145
-
146
- def parse_line_range(str)
147
- if str.include?("-")
148
- start_str, end_str = str.split("-", 2)
149
- start_line = Integer(start_str)
150
- end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
151
- start_line..end_line
152
- else
153
- line = Integer(str)
154
- line..line
155
- end
156
- rescue ArgumentError, TypeError
157
- raise Evilution::ParseError, "invalid line range: #{str.inspect}"
158
- end
159
-
160
- def validate_opts!(opts)
161
- unknown = opts.keys - ALLOWED_OPT_KEYS
162
- return if unknown.empty?
163
-
164
- raise Evilution::ParseError, "unknown parameters: #{unknown.join(", ")}"
165
- end
166
-
167
- def build_config_opts(files, line_ranges, params)
168
- # Preload is disabled for MCP invocations: `require`-ing Rails into the
169
- # long-lived MCP server would poison subsequent runs against other
170
- # projects. MCP users who want the speedup should use the CLI.
171
- opts = { target_files: files, line_ranges: line_ranges, format: :json, quiet: true, preload: false }
172
- opts[:skip_config_file] = true if params[:skip_config]
173
- opts[:spec_files] = params[:spec] if params[:spec]
174
- PASSTHROUGH_KEYS.each { |key| opts[key] = params[key] unless params[key].nil? }
175
- opts
176
- end
177
-
178
- def normalize_verbosity(value)
179
- normalized = value.to_s.strip.downcase
180
- normalized = "summary" if normalized.empty?
181
- return normalized if VALID_VERBOSITIES.include?(normalized)
182
-
183
- raise Evilution::ParseError, "invalid verbosity: #{value.inspect} (must be full, summary, or minimal)"
184
- end
185
-
186
- def trim_report(json_string, verbosity, survived_results, config)
187
- data = ::JSON.parse(json_string)
188
- case verbosity
189
- when "full"
190
- strip_diffs(data, "killed")
191
- strip_diffs(data, "neutral")
192
- strip_diffs(data, "equivalent")
193
- when "summary"
194
- data.delete("killed")
195
- data.delete("neutral")
196
- data.delete("equivalent")
197
- when "minimal"
198
- data.delete("killed")
199
- data.delete("neutral")
200
- data.delete("equivalent")
201
- data.delete("timed_out")
202
- data.delete("errors")
203
- end
204
- enrich_survived(data, survived_results, config)
205
- ::JSON.generate(data)
206
- end
207
-
208
- def enrich_survived(data, survived_results, config)
209
- entries = data["survived"]
210
- return unless entries.is_a?(Array)
211
-
212
- explicit_spec = explicit_spec_override(config)
213
- resolver = explicit_spec ? nil : resolver_for_integration(config.integration)
214
- cache = {}
215
-
216
- entries.each_with_index do |entry, index|
217
- result = survived_results[index]
218
- next unless result
219
-
220
- mutation = result.mutation
221
- entry["subject"] = mutation.subject.name
222
- spec_file = explicit_spec || cache.fetch(mutation.file_path) do
223
- cache[mutation.file_path] = resolver.call(mutation.file_path)
224
- end
225
- entry["spec_file"] = spec_file if spec_file
226
- entry["next_step"] = build_next_step(mutation, spec_file)
227
- end
228
- end
229
-
230
- def explicit_spec_override(config)
231
- return nil unless config.respond_to?(:spec_files)
232
-
233
- files = Array(config.spec_files).compact.map(&:to_s).reject(&:empty?)
234
- files.first
235
- end
236
-
237
- def resolver_for_integration(integration)
238
- integration_class = Evilution::Runner::INTEGRATIONS[integration.to_sym]
239
- return Evilution::SpecResolver.new unless integration_class
240
-
241
- integration_class.baseline_options[:spec_resolver] || Evilution::SpecResolver.new
242
- end
243
-
244
- def build_next_step(mutation, spec_file)
245
- target = spec_file || "the covering test file"
246
- "Add a test in #{target} that fails against this mutation at #{mutation.file_path}:#{mutation.line} " \
247
- "(#{mutation.subject.name}, #{mutation.operator_name})."
248
- end
249
-
250
- def strip_diffs(data, key)
251
- return unless data[key]
252
-
253
- data[key].each { |entry| entry.delete("diff") }
254
- end
255
-
256
- def build_streaming_callback(server_context, suggest_tests, integration)
257
- return nil unless suggest_tests && server_context.respond_to?(:report_progress)
258
-
259
- suggestion = Evilution::Reporter::Suggestion.new(suggest_tests: true, integration: integration)
260
- survivor_index = 0
261
-
262
- proc do |result|
263
- next unless result.survived?
264
-
265
- begin
266
- survivor_index += 1
267
- detail = build_suggestion_detail(result.mutation, suggestion)
268
- server_context.report_progress(survivor_index, message: ::JSON.generate(detail))
269
- rescue StandardError # rubocop:disable Lint/SuppressedException
270
- end
271
- end
272
- end
273
-
274
- def build_suggestion_detail(mutation, suggestion)
275
- {
276
- operator: mutation.operator_name,
277
- file: mutation.file_path,
278
- line: mutation.line,
279
- subject: mutation.subject.name,
280
- diff: mutation.diff,
281
- suggestion: suggestion.suggestion_for(mutation)
282
- }
283
- end
284
-
285
- def build_error_payload(error)
286
- error_type = case error
287
- when Evilution::ConfigError then "config_error"
288
- when Evilution::ParseError then "parse_error"
289
- else "runtime_error"
290
- end
291
-
292
- payload = { type: error_type, message: error.message }
293
- payload[:file] = error.file if error.file
294
- { error: payload }
134
+ payload = Evilution::MCP::MutateTool::ErrorPayload.build(e)
135
+ ::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(payload) }], error: true)
295
136
  end
296
137
  end
297
138
  end
139
+
140
+ require_relative "mutate_tool/error_payload"
141
+ require_relative "mutate_tool/option_parser"
142
+ require_relative "mutate_tool/config_builder"
143
+ require_relative "mutate_tool/report_trimmer"
144
+ require_relative "mutate_tool/survived_enricher"
145
+ require_relative "mutate_tool/progress_streamer"
@@ -1,27 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "diff/lcs"
4
- require "diff/lcs/hunk"
5
4
 
6
5
  class Evilution::Mutation
7
6
  attr_reader :subject, :operator_name, :original_source,
8
- :mutated_source, :file_path, :line, :column
7
+ :mutated_source, :original_slice, :mutated_slice,
8
+ :file_path, :line, :column, :parse_status
9
9
 
10
- def initialize(subject:, operator_name:, original_source:, mutated_source:, file_path:, line:, column: 0)
10
+ # rubocop:disable Metrics/ParameterLists
11
+ def initialize(subject:, operator_name:, original_source:, mutated_source:,
12
+ file_path:, line:, column: 0, original_slice: nil, mutated_slice: nil,
13
+ parse_status: :ok)
14
+ # rubocop:enable Metrics/ParameterLists
11
15
  @subject = subject
12
16
  @operator_name = operator_name
13
17
  @original_source = original_source
14
18
  @mutated_source = mutated_source
19
+ @original_slice = original_slice
20
+ @mutated_slice = mutated_slice
15
21
  @file_path = file_path
16
22
  @line = line
17
23
  @column = column
24
+ @parse_status = parse_status
18
25
  @diff = nil
19
26
  end
20
27
 
28
+ def unparseable?
29
+ @parse_status == :unparseable
30
+ end
31
+
21
32
  def diff
22
33
  @diff ||= compute_diff
23
34
  end
24
35
 
36
+ def unified_diff
37
+ return @unified_diff if defined?(@unified_diff)
38
+
39
+ @unified_diff = compute_unified_diff
40
+ end
41
+
25
42
  def strip_sources!
26
43
  diff # ensure diff is cached before clearing sources
27
44
  @original_source = nil
@@ -49,6 +66,29 @@ class Evilution::Mutation
49
66
  result.join("\n")
50
67
  end
51
68
 
69
+ def compute_unified_diff
70
+ return nil if @original_slice.nil? || @mutated_slice.nil?
71
+
72
+ original_lines = @original_slice.lines
73
+ mutated_lines = @mutated_slice.lines
74
+ body = ::Diff::LCS.sdiff(original_lines, mutated_lines).map { |c| format_sdiff_change(c) }.join("\n")
75
+ [
76
+ "--- a/#{file_path}",
77
+ "+++ b/#{file_path}",
78
+ "@@ -#{line},#{original_lines.length} +#{line},#{mutated_lines.length} @@",
79
+ body
80
+ ].reject(&:empty?).join("\n")
81
+ end
82
+
83
+ def format_sdiff_change(change)
84
+ case change.action
85
+ when "=" then " #{change.old_element.chomp}"
86
+ when "-" then "-#{change.old_element.chomp}"
87
+ when "+" then "+#{change.new_element.chomp}"
88
+ when "!" then "-#{change.old_element.chomp}\n+#{change.new_element.chomp}"
89
+ end
90
+ end
91
+
52
92
  public
53
93
 
54
94
  def to_s
@@ -27,24 +27,62 @@ class Evilution::Mutator::Base < Prism::Visitor
27
27
  def add_mutation(offset:, length:, replacement:, node:)
28
28
  return if @filter && @filter.skip?(node)
29
29
 
30
- mutated_source = Evilution::AST::SourceSurgeon.apply(
30
+ surgery = Evilution::AST::SourceSurgeon.apply(
31
31
  @file_source,
32
32
  offset: offset,
33
33
  length: length,
34
34
  replacement: replacement
35
35
  )
36
+ mutated_source = surgery.source
37
+
38
+ original_slice, mutated_slice = slice_affected_lines(
39
+ mutated_source: mutated_source,
40
+ offset: offset,
41
+ length: length,
42
+ replacement_bytesize: replacement.bytesize
43
+ )
36
44
 
37
45
  @mutations << Evilution::Mutation.new(
38
46
  subject: @subject,
39
47
  operator_name: self.class.operator_name,
40
48
  original_source: @file_source,
41
49
  mutated_source: mutated_source,
50
+ original_slice: original_slice,
51
+ mutated_slice: mutated_slice,
52
+ parse_status: surgery.status,
42
53
  file_path: @subject.file_path,
43
54
  line: node.location.start_line,
44
55
  column: node.location.start_column
45
56
  )
46
57
  end
47
58
 
59
+ NEWLINE_BYTE = 10
60
+ private_constant :NEWLINE_BYTE
61
+
62
+ def slice_affected_lines(mutated_source:, offset:, length:, replacement_bytesize:)
63
+ line_start = line_start_byte(@file_source, offset)
64
+ orig_line_end = line_end_byte(@file_source, [offset + length - 1, line_start].max)
65
+ mut_line_end = line_end_byte(mutated_source, [offset + replacement_bytesize - 1, line_start].max)
66
+
67
+ [
68
+ @file_source.byteslice(line_start, orig_line_end - line_start),
69
+ mutated_source.byteslice(line_start, mut_line_end - line_start)
70
+ ]
71
+ end
72
+
73
+ def line_start_byte(source, offset)
74
+ i = offset - 1
75
+ i -= 1 while i >= 0 && source.getbyte(i) != NEWLINE_BYTE
76
+ i + 1
77
+ end
78
+
79
+ def line_end_byte(source, from)
80
+ limit = source.bytesize
81
+ i = from
82
+ i += 1 while i < limit && source.getbyte(i) != NEWLINE_BYTE
83
+ i < limit ? i + 1 : limit
84
+ end
85
+
48
86
  def byteslice_source(offset, length)
49
87
  @file_source.byteslice(offset, length).force_encoding(@file_source.encoding)
50
88
  end
@@ -13,7 +13,7 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
13
13
  def visit_call_node(node)
14
14
  args = node.arguments&.arguments
15
15
 
16
- if args && args.length >= 1 && positional_only?(args)
16
+ if mutable?(node, args)
17
17
  args.each_index do |i|
18
18
  parts = args.each_with_index.map { |a, j| j == i ? "nil" : a.slice }
19
19
  replacement = parts.join(", ")
@@ -32,6 +32,10 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
32
32
 
33
33
  private
34
34
 
35
+ def mutable?(node, args)
36
+ args && args.length >= 1 && positional_only?(args) && node.name != :[]=
37
+ end
38
+
35
39
  def positional_only?(args)
36
40
  args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
37
41
  end
@@ -13,7 +13,7 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
13
13
  def visit_call_node(node)
14
14
  args = node.arguments&.arguments
15
15
 
16
- if args && args.length >= 2 && positional_only?(args)
16
+ if mutable?(node, args)
17
17
  args.each_index do |i|
18
18
  remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
19
19
  replacement = remaining.join(", ")
@@ -32,6 +32,10 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
32
32
 
33
33
  private
34
34
 
35
+ def mutable?(node, args)
36
+ args && args.length >= 2 && positional_only?(args) && node.name != :[]=
37
+ end
38
+
35
39
  def positional_only?(args)
36
40
  args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
37
41
  end