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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +38 -0
  3. data/.rubocop_todo.yml +9 -0
  4. data/CHANGELOG.md +47 -0
  5. data/README.md +65 -7
  6. data/docs/architecture.md +35 -1
  7. data/lib/evilution/ast/parser.rb +1 -0
  8. data/lib/evilution/ast/pattern/filter.rb +1 -0
  9. data/lib/evilution/ast/pattern/parser.rb +1 -0
  10. data/lib/evilution/ast/pattern.rb +2 -0
  11. data/lib/evilution/ast/sorbet_sig_detector.rb +1 -0
  12. data/lib/evilution/baseline.rb +37 -15
  13. data/lib/evilution/child_output.rb +1 -1
  14. data/lib/evilution/cli/command.rb +1 -0
  15. data/lib/evilution/cli/commands/tests_list.rb +4 -3
  16. data/lib/evilution/cli/commands.rb +2 -0
  17. data/lib/evilution/cli/dispatcher.rb +2 -0
  18. data/lib/evilution/cli/exit_guard.rb +2 -0
  19. data/lib/evilution/cli/parsed_args.rb +2 -0
  20. data/lib/evilution/cli/parser/command_extractor.rb +2 -0
  21. data/lib/evilution/cli/parser/file_args.rb +2 -0
  22. data/lib/evilution/cli/parser/options_builder.rb +3 -0
  23. data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
  24. data/lib/evilution/cli/parser.rb +1 -0
  25. data/lib/evilution/cli/printers/tests_list.rb +5 -4
  26. data/lib/evilution/cli/printers.rb +2 -0
  27. data/lib/evilution/cli/result.rb +2 -0
  28. data/lib/evilution/cli.rb +5 -0
  29. data/lib/evilution/config/builders.rb +2 -0
  30. data/lib/evilution/config/env_loader.rb +2 -0
  31. data/lib/evilution/config/file_loader.rb +1 -0
  32. data/lib/evilution/config/sources.rb +1 -0
  33. data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
  34. data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
  35. data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
  36. data/lib/evilution/config/validators/fail_fast.rb +1 -0
  37. data/lib/evilution/config/validators/hooks.rb +1 -0
  38. data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
  39. data/lib/evilution/config/validators/integration.rb +1 -0
  40. data/lib/evilution/config/validators/isolation.rb +1 -0
  41. data/lib/evilution/config/validators/jobs.rb +1 -0
  42. data/lib/evilution/config/validators/preload.rb +1 -0
  43. data/lib/evilution/config/validators/profile.rb +1 -0
  44. data/lib/evilution/config/validators/spec_mappings.rb +1 -0
  45. data/lib/evilution/config/validators/spec_pattern.rb +1 -0
  46. data/lib/evilution/config/validators/warmup.rb +17 -0
  47. data/lib/evilution/config/validators.rb +2 -0
  48. data/lib/evilution/config.rb +4 -3
  49. data/lib/evilution/coverage.rb +1 -1
  50. data/lib/evilution/coverage_example_filter.rb +1 -1
  51. data/lib/evilution/diagnostic.rb +1 -1
  52. data/lib/evilution/example_filter.rb +1 -1
  53. data/lib/evilution/integration/loading/body_call_neutralizer.rb +10 -2
  54. data/lib/evilution/integration/loading/test_load_path.rb +21 -13
  55. data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
  56. data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
  57. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
  58. data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
  59. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
  60. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
  61. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
  62. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
  63. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
  64. data/lib/evilution/integration/rspec/state_guard.rb +6 -0
  65. data/lib/evilution/mcp/info_tool/actions/environment.rb +1 -0
  66. data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
  67. data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
  68. data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
  69. data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
  70. data/lib/evilution/mutation.rb +1 -1
  71. data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
  72. data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
  73. data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
  74. data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
  75. data/lib/evilution/mutator/operator/bang_method.rb +11 -3
  76. data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
  77. data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
  78. data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
  79. data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
  80. data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
  81. data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
  82. data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
  83. data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
  84. data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
  85. data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
  86. data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
  87. data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
  88. data/lib/evilution/mutator/operator/inequality_to_negated_identity.rb +43 -0
  89. data/lib/evilution/mutator/operator/keyword_argument_removal.rb +47 -0
  90. data/lib/evilution/mutator/operator/proc_to_lambda.rb +43 -0
  91. data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
  92. data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
  93. data/lib/evilution/mutator/operator/regexp_anchor_to_predicate.rb +136 -0
  94. data/lib/evilution/mutator/operator/safe_navigation_removal.rb +67 -0
  95. data/lib/evilution/mutator/operator/send_mutation.rb +42 -6
  96. data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
  97. data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
  98. data/lib/evilution/mutator/primitives.rb +25 -0
  99. data/lib/evilution/mutator/registry.rb +24 -1
  100. data/lib/evilution/parallel/pool.rb +1 -0
  101. data/lib/evilution/parallel_db_warning.rb +1 -1
  102. data/lib/evilution/rails_warmup.rb +61 -0
  103. data/lib/evilution/reporter/html/report.rb +1 -0
  104. data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
  105. data/lib/evilution/result/subject_scorer.rb +1 -0
  106. data/lib/evilution/runner/baseline_runner.rb +17 -7
  107. data/lib/evilution/runner/isolation_resolver.rb +23 -2
  108. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +17 -10
  109. data/lib/evilution/source_ast_cache.rb +1 -1
  110. data/lib/evilution/spec_ast_cache.rb +1 -1
  111. data/lib/evilution/version.rb +1 -1
  112. data/lib/evilution.rb +23 -0
  113. 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)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../parser"
3
4
  require_relative "file_args"
4
5
 
5
6
  class Evilution::CLI::Parser::StdinReader
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
3
4
  require_relative "parsed_args"
4
5
 
5
6
  class Evilution::CLI::Parser
@@ -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
- spec = entry[:spec]
32
- if spec
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
 
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  module Evilution::CLI::Printers
4
6
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../cli"
4
+
3
5
  class Evilution::CLI
4
6
  Result = Struct.new(:exit_code, :error, :error_rendered, keyword_init: true) do
5
7
  def initialize(exit_code:, error: nil, error_rendered: false)
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"
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
4
+
3
5
  module Evilution::Config::Builders
4
6
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
4
+
3
5
  module Evilution::Config::EnvLoader
4
6
  module_function
5
7
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require_relative "../config"
4
5
 
5
6
  module Evilution::Config::FileLoader
6
7
  module_function
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
3
4
  require_relative "file_loader"
4
5
  require_relative "env_loader"
5
6
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::ExampleTargetingCache < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::ExampleTargetingFallback < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::ExampleTargetingStrategy < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::FailFast < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Hooks < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::IgnorePatterns < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Integration < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Isolation < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Jobs < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Preload < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::Profile < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::SpecMappings < Evilution::Config::Validators::Base
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../validators"
3
4
  require_relative "base"
4
5
 
5
6
  class Evilution::Config::Validators::SpecPattern < Evilution::Config::Validators::Base
@@ -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
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../config"
4
+
3
5
  module Evilution::Config::Validators
4
6
  end
@@ -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"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../evilution"
3
+ require_relative "version"
4
4
 
5
5
  # Per-example line-coverage support: build a `source file:line -> [examples]`
6
6
  # map so mutation targeting can run exactly the examples that execute a line.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../evilution"
3
+ require_relative "version"
4
4
  require_relative "coverage/map"
5
5
 
6
6
  # Per-mutation example targeting backed by a real line-coverage Map (EV-ndjd).
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../evilution"
3
+ require_relative "version"
4
4
 
5
5
  # Evilution's own advisory messages -- "no matching spec found", and the like.
6
6
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
- require_relative "../evilution"
4
+ require_relative "version"
5
5
  require_relative "spec_ast_cache"
6
6
 
7
7
  class Evilution::ExampleFilter
@@ -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 is replaced byte-for-byte with `nil`.
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] = "nil"
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 resolved test files:
32
- # the conventional test/ and spec/ roots under base, each file's own
33
- # directory, and the topmost test/spec ancestor of each file (covers nested
34
- # layouts like test/unit, spec/lib, spec/unit). Existing directories only,
35
- # and only those inside the project base -- never a broad outside-project dir
36
- # (e.g. a /tmp test file), which would over-widen $LOAD_PATH for the whole
37
- # process (the baseline runs in the long-lived parent).
38
- def dirs_for(files, base)
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
- dirs << root if root
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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../state_guard"
3
4
  require_relative "../../rspec"
4
5
 
5
6
  # Restores the RSpec.configuration state that ::RSpec::Core::Runner.run mutates
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../state_guard"
3
4
  require_relative "../../rspec"
4
5
  require_relative "internals"
5
6
 
@@ -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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../state_guard"
3
4
  require_relative "../../rspec"
4
5
  require_relative "internals"
5
6
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../state_guard"
3
4
  require_relative "../../rspec"
4
5
  require_relative "internals"
5
6
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../state_guard"
3
4
  require_relative "../../rspec"
4
5
  require_relative "internals"
5
6
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../state_guard"
3
4
  require_relative "../../rspec"
4
5
  require_relative "internals"
5
6
 
@@ -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"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../actions"
3
4
  require_relative "base"
4
5
  require_relative "../../../config"
5
6
  require_relative "../../../version"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../actions"
3
4
  require_relative "base"
4
5
  require_relative "../../../version"
5
6
  require_relative "../../../feedback"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../actions"
3
4
  require_relative "base"
4
5
  require_relative "../status_glossary"
5
6
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../actions"
3
4
  require_relative "base"
4
5
  require_relative "../config_factory"
5
6
  require_relative "../../../runner"
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../actions"
3
4
  require_relative "base"
4
5
  require_relative "../config_factory"
5
6
  require_relative "../../../runner"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "diff/lcs"
4
- require_relative "../evilution"
4
+ require_relative "version"
5
5
 
6
6
  class Evilution::Mutation
7
7
  Sources = Data.define(:original, :mutated)
@@ -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