evilution 1.2.0 → 1.3.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/interactions.jsonl +38 -0
- data/.rubocop_todo.yml +9 -0
- data/CHANGELOG.md +47 -0
- data/README.md +65 -7
- data/docs/architecture.md +35 -1
- data/lib/evilution/ast/parser.rb +1 -0
- data/lib/evilution/ast/pattern/filter.rb +1 -0
- data/lib/evilution/ast/pattern/parser.rb +1 -0
- data/lib/evilution/ast/pattern.rb +2 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
- data/lib/evilution/baseline.rb +37 -15
- data/lib/evilution/child_output.rb +1 -1
- data/lib/evilution/cli/command.rb +1 -0
- data/lib/evilution/cli/commands/tests_list.rb +4 -3
- data/lib/evilution/cli/commands.rb +2 -0
- data/lib/evilution/cli/dispatcher.rb +2 -0
- data/lib/evilution/cli/exit_guard.rb +2 -0
- data/lib/evilution/cli/parsed_args.rb +2 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -0
- data/lib/evilution/cli/parser/file_args.rb +2 -0
- data/lib/evilution/cli/parser/options_builder.rb +3 -0
- data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
- data/lib/evilution/cli/parser.rb +1 -0
- data/lib/evilution/cli/printers/tests_list.rb +5 -4
- data/lib/evilution/cli/printers.rb +2 -0
- data/lib/evilution/cli/result.rb +2 -0
- data/lib/evilution/cli.rb +5 -0
- data/lib/evilution/config/builders.rb +2 -0
- data/lib/evilution/config/env_loader.rb +2 -0
- data/lib/evilution/config/file_loader.rb +1 -0
- data/lib/evilution/config/sources.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
- data/lib/evilution/config/validators/fail_fast.rb +1 -0
- data/lib/evilution/config/validators/hooks.rb +1 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
- data/lib/evilution/config/validators/integration.rb +1 -0
- data/lib/evilution/config/validators/isolation.rb +1 -0
- data/lib/evilution/config/validators/jobs.rb +1 -0
- data/lib/evilution/config/validators/preload.rb +1 -0
- data/lib/evilution/config/validators/profile.rb +1 -0
- data/lib/evilution/config/validators/spec_mappings.rb +1 -0
- data/lib/evilution/config/validators/spec_pattern.rb +1 -0
- data/lib/evilution/config/validators/warmup.rb +17 -0
- data/lib/evilution/config/validators.rb +2 -0
- data/lib/evilution/config.rb +4 -3
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +1 -1
- data/lib/evilution/example_filter.rb +1 -1
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +10 -2
- data/lib/evilution/integration/loading/test_load_path.rb +21 -13
- data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard.rb +6 -0
- data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
- data/lib/evilution/mutation.rb +1 -1
- data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
- data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
- data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
- data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
- data/lib/evilution/mutator/operator/bang_method.rb +11 -3
- data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
- data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
- data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
- data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
- data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
- data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
- data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
- data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
- data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
- data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
- data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
- data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
- data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
- data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +42 -6
- data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +24 -1
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/rails_warmup.rb +61 -0
- data/lib/evilution/reporter/html/report.rb +1 -0
- data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/subject_scorer.rb +1 -0
- data/lib/evilution/runner/baseline_runner.rb +17 -7
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +17 -10
- data/lib/evilution/source_ast_cache.rb +1 -1
- data/lib/evilution/spec_ast_cache.rb +1 -1
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +23 -0
- metadata +27 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
require_relative "../parser"
|
|
4
5
|
require_relative "../../version"
|
|
5
6
|
require_relative "file_args"
|
|
6
7
|
|
|
@@ -119,6 +120,8 @@ class Evilution::CLI::Parser::OptionsBuilder
|
|
|
119
120
|
"(default: auto-detect spec/rails_helper.rb -> spec/spec_helper.rb -> " \
|
|
120
121
|
"test/test_helper.rb for Rails projects)") { |f| @options[:preload] = f }
|
|
121
122
|
opts.on("--no-preload", "Disable parent-process preload even for Rails projects") { @options[:preload] = false }
|
|
123
|
+
opts.on("--warmup NAME", "After preload, warm lazily-initialised framework state once " \
|
|
124
|
+
"in the parent: none, rails (default: none)") { |w| @options[:warmup] = w }
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
def add_output_options(opts)
|
data/lib/evilution/cli/parser.rb
CHANGED
|
@@ -24,16 +24,17 @@ class Evilution::CLI::Printers::TestsList
|
|
|
24
24
|
io.puts(@specs.length == 1 ? "1 spec file" : "#{@specs.length} spec files")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Each entry is `{ source:, specs: [...] }`; a source can resolve to several
|
|
28
|
+
# spec files (spec_mappings), and each gets its own line.
|
|
27
29
|
def render_resolved(io)
|
|
28
30
|
unique_specs = []
|
|
29
31
|
@entries.each do |entry|
|
|
30
32
|
source = entry[:source]
|
|
31
|
-
|
|
32
|
-
if
|
|
33
|
+
specs = entry[:specs]
|
|
34
|
+
io.puts(" #{source} (no spec found)") if specs.empty?
|
|
35
|
+
specs.each do |spec|
|
|
33
36
|
unique_specs << spec
|
|
34
37
|
io.puts(" #{spec} (#{source})")
|
|
35
|
-
else
|
|
36
|
-
io.puts(" #{source} (no spec found)")
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
|
data/lib/evilution/cli/result.rb
CHANGED
data/lib/evilution/cli.rb
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "version"
|
|
4
|
+
|
|
5
|
+
# Declared before the children are required: each child requires this file
|
|
6
|
+
# and nests under Evilution::CLI, so the namespace must exist first.
|
|
7
|
+
class Evilution::CLI; end # rubocop:disable Lint/EmptyClass
|
|
8
|
+
|
|
4
9
|
require_relative "cli/parser"
|
|
5
10
|
require_relative "cli/parsed_args"
|
|
6
11
|
require_relative "cli/printers/subjects"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validators"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
# `warmup: rails` warms Rails' lazily-initialised state in the parent after
|
|
7
|
+
# the preload; `none` (the default) skips it. `false` is accepted as `none`
|
|
8
|
+
# so `warmup: false` reads naturally in .evilution.yml.
|
|
9
|
+
class Evilution::Config::Validators::Warmup < Evilution::Config::Validators::Base
|
|
10
|
+
ALLOWED = %i[none rails].freeze
|
|
11
|
+
|
|
12
|
+
def self.call(value)
|
|
13
|
+
return :none if value == false
|
|
14
|
+
|
|
15
|
+
coerce_symbol!(value, allowed: ALLOWED, name: "warmup")
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/evilution/config.rb
CHANGED
|
@@ -21,7 +21,7 @@ class Evilution::Config
|
|
|
21
21
|
example_targeting_fallback: :full_file, example_targeting_strategy: :lexical,
|
|
22
22
|
example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
|
|
23
23
|
quiet_children: false, quiet_children_dir: "tmp/evilution_children",
|
|
24
|
-
profile: :default, canary: true
|
|
24
|
+
profile: :default, canary: true, warmup: :none
|
|
25
25
|
}.freeze
|
|
26
26
|
|
|
27
27
|
attr_reader :target_files, :schema_version, :timeout, :format,
|
|
@@ -33,7 +33,7 @@ class Evilution::Config
|
|
|
33
33
|
:fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
|
|
34
34
|
:example_targeting, :example_targeting_fallback, :example_targeting_strategy,
|
|
35
35
|
:example_targeting_cache,
|
|
36
|
-
:spec_selector, :quiet_children, :quiet_children_dir, :profile, :canary
|
|
36
|
+
:spec_selector, :quiet_children, :quiet_children_dir, :profile, :canary, :warmup
|
|
37
37
|
|
|
38
38
|
def initialize(**options)
|
|
39
39
|
skip_file = options.delete(:skip_config_file) ? true : false
|
|
@@ -274,7 +274,7 @@ class Evilution::Config
|
|
|
274
274
|
|
|
275
275
|
VALIDATED_ATTRS = %i[
|
|
276
276
|
integration jobs fail_fast isolation ignore_patterns
|
|
277
|
-
hooks preload spec_mappings spec_pattern profile
|
|
277
|
+
hooks preload spec_mappings spec_pattern profile warmup
|
|
278
278
|
].freeze
|
|
279
279
|
private_constant :VALIDATED_ATTRS
|
|
280
280
|
|
|
@@ -311,6 +311,7 @@ require_relative "config/validators/example_targeting_fallback"
|
|
|
311
311
|
require_relative "config/validators/example_targeting_strategy"
|
|
312
312
|
require_relative "config/validators/example_targeting_cache"
|
|
313
313
|
require_relative "config/validators/profile"
|
|
314
|
+
require_relative "config/validators/warmup"
|
|
314
315
|
require_relative "config/builders"
|
|
315
316
|
require_relative "config/builders/spec_resolver"
|
|
316
317
|
require_relative "config/builders/spec_selector"
|
data/lib/evilution/coverage.rb
CHANGED
data/lib/evilution/diagnostic.rb
CHANGED
|
@@ -13,7 +13,9 @@ require_relative "../loading"
|
|
|
13
13
|
# Strategy: walk Prism tree, find CallNodes that sit directly under a class
|
|
14
14
|
# or module body (not inside a def). Calls on a small allowlist of patterns
|
|
15
15
|
# known to be idempotent (`include`, `attr_*`, visibility modifiers, etc.)
|
|
16
|
-
# are preserved; everything else
|
|
16
|
+
# are preserved; everything else has its source range replaced with `()`.
|
|
17
|
+
# The replacement is shorter than the call it replaces, so edits are applied
|
|
18
|
+
# from the end of the source backwards to keep earlier offsets valid.
|
|
17
19
|
class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
18
20
|
IDEMPOTENT_CALLS = %i[
|
|
19
21
|
include extend prepend using
|
|
@@ -25,6 +27,12 @@ class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
|
25
27
|
require require_relative autoload
|
|
26
28
|
].to_set.freeze
|
|
27
29
|
|
|
30
|
+
# `()` rather than `nil`: under -w Ruby flags a bare `nil` statement as
|
|
31
|
+
# "possibly useless use of nil in void context", which a project raising on
|
|
32
|
+
# warnings turns into an error for every mutation of the file. `()` evaluates
|
|
33
|
+
# to nil without the warning on every supported Ruby (`(nil)` still warns on 3.3).
|
|
34
|
+
REPLACEMENT = "()"
|
|
35
|
+
|
|
28
36
|
class << self
|
|
29
37
|
attr_writer :preloaded_features
|
|
30
38
|
|
|
@@ -80,7 +88,7 @@ class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
|
80
88
|
def apply_edits(source, edits)
|
|
81
89
|
bytes = source.b
|
|
82
90
|
edits.sort_by!(&:first).reverse_each do |start_offset, end_offset|
|
|
83
|
-
bytes[start_offset, end_offset - start_offset] =
|
|
91
|
+
bytes[start_offset, end_offset - start_offset] = REPLACEMENT
|
|
84
92
|
end
|
|
85
93
|
bytes.force_encoding(source.encoding)
|
|
86
94
|
end
|
|
@@ -22,29 +22,37 @@ module Evilution::Integration::Loading::TestLoadPath
|
|
|
22
22
|
# Prepend every relevant test directory to $LOAD_PATH (idempotently).
|
|
23
23
|
# Iterate in reverse so the first entry from #dirs_for ends up frontmost,
|
|
24
24
|
# preserving its order (mirrors how `ruby -Ia -Ib` lands a before b).
|
|
25
|
-
def add!(files, base: Evilution.project_base_dir)
|
|
26
|
-
dirs_for(files, base).reverse_each do |dir|
|
|
25
|
+
def add!(files, base: Evilution.project_base_dir, outside_root_dirs: true)
|
|
26
|
+
dirs_for(files, base, outside_root_dirs: outside_root_dirs).reverse_each do |dir|
|
|
27
27
|
$LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
# The directories to put on $LOAD_PATH for the given
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
|
|
31
|
+
# The directories to put on $LOAD_PATH for the given files: the
|
|
32
|
+
# conventional test/ and spec/ roots under base, each file's own directory,
|
|
33
|
+
# and the topmost test/spec ancestor of each file (covers nested layouts
|
|
34
|
+
# like test/unit, spec/lib, spec/unit). Existing directories only, and only
|
|
35
|
+
# those inside the project base -- never a broad outside-project dir (e.g. a
|
|
36
|
+
# /tmp test file), which would over-widen $LOAD_PATH for the whole process
|
|
37
|
+
# (the baseline runs in the long-lived parent).
|
|
38
|
+
#
|
|
39
|
+
# A resolved test file outside every test root (a tests/ layout, a test
|
|
40
|
+
# reached through --spec or spec_mappings) still contributes its own
|
|
41
|
+
# directory, mirroring `ruby -Itests`. A preload does not: with
|
|
42
|
+
# `outside_root_dirs: false`, a `--preload config/evilution_preload.rb`
|
|
43
|
+
# would otherwise put config/ ahead of the gems, and `require "puma"` during
|
|
44
|
+
# Bundler.require would load the app's config/puma.rb (GH #1597).
|
|
45
|
+
def dirs_for(files, base, outside_root_dirs: true)
|
|
39
46
|
base = File.expand_path(base)
|
|
40
47
|
dirs = conventional_roots(base)
|
|
41
48
|
Array(files).each do |file|
|
|
42
49
|
file_dir = File.dirname(File.expand_path(file, base))
|
|
43
|
-
dirs << file_dir
|
|
44
50
|
root = root_ancestor(file_dir, base)
|
|
45
|
-
|
|
51
|
+
next unless root || outside_root_dirs
|
|
52
|
+
|
|
53
|
+
dirs << file_dir << root
|
|
46
54
|
end
|
|
47
|
-
dirs.uniq.select { |dir| File.directory?(dir) && within?(dir, base) }
|
|
55
|
+
dirs.compact.uniq.select { |dir| File.directory?(dir) && within?(dir, base) }
|
|
48
56
|
end
|
|
49
57
|
|
|
50
58
|
def within?(dir, base)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../state_guard"
|
|
3
4
|
require_relative "../../rspec"
|
|
4
5
|
|
|
5
|
-
class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
|
|
6
|
-
|
|
7
6
|
class Evilution::Integration::RSpec::StateGuard::ExampleGroupsConstants
|
|
8
7
|
def snapshot
|
|
9
8
|
return nil unless defined?(::RSpec::ExampleGroups)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../state_guard"
|
|
3
4
|
require_relative "../../rspec"
|
|
4
5
|
|
|
5
|
-
class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
|
|
6
|
-
|
|
7
6
|
module Evilution::Integration::RSpec::StateGuard::Internals
|
|
8
7
|
module_function
|
|
9
8
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../state_guard"
|
|
3
4
|
require_relative "../../rspec"
|
|
4
5
|
|
|
5
|
-
class Evilution::Integration::RSpec::StateGuard; end unless defined?(Evilution::Integration::RSpec::StateGuard) # rubocop:disable Lint/EmptyClass
|
|
6
|
-
|
|
7
6
|
class Evilution::Integration::RSpec::StateGuard::ObjectSpaceExampleGroups
|
|
8
7
|
def snapshot
|
|
9
8
|
groups = Set.new
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../rspec"
|
|
4
|
+
|
|
5
|
+
# Declared before the children are required: each child requires this file
|
|
6
|
+
# and nests under StateGuard, while DEFAULT_STRATEGIES below needs the
|
|
7
|
+
# children loaded, so the namespace must exist first.
|
|
8
|
+
class Evilution::Integration::RSpec::StateGuard; end # rubocop:disable Lint/EmptyClass
|
|
9
|
+
|
|
4
10
|
require_relative "state_guard/object_space_example_groups"
|
|
5
11
|
require_relative "state_guard/world_example_groups"
|
|
6
12
|
require_relative "state_guard/world_sources_by_path"
|
data/lib/evilution/mutation.rb
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Drop a call's whole argument list: `combine(a, b)` becomes `combine`,
|
|
6
|
+
# `normalize(value)` becomes `normalize`. Every argument kind goes —
|
|
7
|
+
# positional, splat, keywords and `...` — while a block-pass stays attached
|
|
8
|
+
# (`each_slice(2, &block)` becomes `each_slice(&block)`) and so does a
|
|
9
|
+
# literal block.
|
|
10
|
+
#
|
|
11
|
+
# A kill by ArgumentError proves arity is asserted; a survivor means the
|
|
12
|
+
# arguments never mattered. argument_removal drops one argument at a time and
|
|
13
|
+
# only from calls with two or more.
|
|
14
|
+
#
|
|
15
|
+
# Operator methods (`a + b`, `a[i]`) are left to binary operand promotion and
|
|
16
|
+
# attribute writes to attribute_write_to_read.
|
|
17
|
+
class Evilution::Mutator::Operator::ArgumentListRemoval < Evilution::Mutator::Base
|
|
18
|
+
IDENTIFIER = /\A[[:alpha:]_]/
|
|
19
|
+
private_constant :IDENTIFIER
|
|
20
|
+
|
|
21
|
+
def visit_call_node(node)
|
|
22
|
+
remove_argument_list(node) if removable?(node)
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def removable?(node)
|
|
29
|
+
node.arguments && node.message_loc && node.name.match?(IDENTIFIER) && !node.attribute_write?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def remove_argument_list(node)
|
|
33
|
+
block_pass = node.block if node.block.is_a?(Prism::BlockArgumentNode)
|
|
34
|
+
start = node.message_loc.end_offset
|
|
35
|
+
|
|
36
|
+
add_mutation(
|
|
37
|
+
offset: start,
|
|
38
|
+
length: argument_list_end(node, block_pass) - start,
|
|
39
|
+
replacement: block_pass ? "(#{block_pass.slice})" : "",
|
|
40
|
+
node: node
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Without parentheses the list ends at its last element: the block-pass
|
|
45
|
+
# when there is one (it always comes last), else the last argument. The
|
|
46
|
+
# call node itself would reach past a literal `do ... end` block.
|
|
47
|
+
def argument_list_end(node, block_pass)
|
|
48
|
+
return node.closing_loc.end_offset if node.closing_loc
|
|
49
|
+
|
|
50
|
+
(block_pass || node.arguments).end_offset
|
|
51
|
+
end
|
|
52
|
+
end
|