evilution 1.1.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 +58 -0
- data/.rubocop_todo.yml +9 -0
- data/CHANGELOG.md +86 -0
- data/README.md +202 -14
- data/docs/architecture.md +54 -5
- data/docs/isolation.md +3 -4
- data/exe/evil +8 -1
- data/exe/evilution +8 -1
- data/lib/evilution/ast/local_reads.rb +43 -0
- 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 +61 -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 +4 -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 +6 -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 +7 -5
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +22 -0
- data/lib/evilution/example_filter.rb +1 -1
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +10 -2
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +20 -4
- data/lib/evilution/integration/loading/reeval_warning_filter.rb +72 -0
- data/lib/evilution/integration/loading/source_evaluator.rb +4 -1
- data/lib/evilution/integration/loading/test_load_path.rb +21 -13
- data/lib/evilution/integration/minitest.rb +2 -1
- data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +9 -1
- data/lib/evilution/integration/rspec/state_guard/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +5 -1
- 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/integration/rspec/unresolved_spec_warner.rb +2 -1
- data/lib/evilution/integration/rspec.rb +48 -2
- data/lib/evilution/integration/test_unit/test_file_resolver.rb +2 -1
- data/lib/evilution/isolation/fork.rb +29 -8
- 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/block_destructuring_expansion.rb +85 -0
- data/lib/evilution/mutator/operator/block_parameter_drop.rb +96 -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/forwarding_super_to_explicit.rb +71 -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/method_body_replacement.rb +10 -1
- data/lib/evilution/mutator/operator/method_body_to_raise.rb +59 -0
- data/lib/evilution/mutator/operator/method_body_to_super.rb +150 -0
- data/lib/evilution/mutator/operator/optional_default_injection.rb +71 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +41 -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/operator/typed_default_return.rb +84 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +32 -1
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/process_supervisor.rb +20 -8
- data/lib/evilution/rails_warmup.rb +61 -0
- data/lib/evilution/reporter/cli/item_formatters/neutral_group.rb +23 -0
- data/lib/evilution/reporter/cli/item_formatters/subject_score.rb +42 -0
- data/lib/evilution/reporter/cli/item_formatters/subject_score_group.rb +16 -0
- data/lib/evilution/reporter/cli/line_formatters/infra_retry_notice.rb +19 -0
- data/lib/evilution/reporter/cli/line_formatters/result_line.rb +27 -3
- data/lib/evilution/reporter/cli/line_formatters/score.rb +19 -1
- data/lib/evilution/reporter/cli/line_formatters/unresolved_targets.rb +35 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +4 -0
- data/lib/evilution/reporter/cli/trailer.rb +11 -7
- data/lib/evilution/reporter/cli.rb +20 -4
- data/lib/evilution/reporter/html/report.rb +1 -0
- data/lib/evilution/reporter/json/subjects.rb +29 -0
- data/lib/evilution/reporter/json.rb +23 -1
- data/lib/evilution/result/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +3 -2
- data/lib/evilution/result/neutral_reason.rb +35 -0
- data/lib/evilution/result/subject_score.rb +28 -0
- data/lib/evilution/result/subject_scorer.rb +38 -0
- data/lib/evilution/result/summary.rb +44 -2
- data/lib/evilution/runner/baseline_runner.rb +17 -7
- data/lib/evilution/runner/canary.rb +52 -5
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/infra_retry.rb +54 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +23 -10
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +18 -1
- data/lib/evilution/runner/mutation_executor/result_cache.rb +11 -0
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +19 -1
- data/lib/evilution/runner/mutation_executor.rb +32 -4
- data/lib/evilution/runner/report_publisher.rb +31 -9
- data/lib/evilution/runner/target_spec_audit.rb +43 -0
- data/lib/evilution/runner.rb +10 -1
- 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 +35 -0
- metadata +50 -2
|
@@ -12,22 +12,47 @@ class Evilution::Mutator::Operator::ComparisonReplacement < Evilution::Mutator::
|
|
|
12
12
|
:!= => [:==]
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
|
+
# Operators rewritten into a method call, since the method cannot be
|
|
16
|
+
# swapped in as an operator (`a eql? b` does not parse). Ordering
|
|
17
|
+
# comparisons collapse to the stricter equalities — `a < b` becomes
|
|
18
|
+
# `a.eql?(b)` and `a.equal?(b)` — and `a =~ b` becomes `a.match?(b)`,
|
|
19
|
+
# which returns a boolean and leaves `$~` / `$1` unset.
|
|
20
|
+
EQUALITY_METHODS = %i[eql? equal?].freeze
|
|
21
|
+
private_constant :EQUALITY_METHODS
|
|
22
|
+
|
|
23
|
+
METHOD_REPLACEMENTS = {
|
|
24
|
+
:< => EQUALITY_METHODS,
|
|
25
|
+
:<= => EQUALITY_METHODS,
|
|
26
|
+
:> => EQUALITY_METHODS,
|
|
27
|
+
:>= => EQUALITY_METHODS,
|
|
28
|
+
:=~ => [:match?]
|
|
29
|
+
}.freeze
|
|
30
|
+
private_constant :METHOD_REPLACEMENTS
|
|
31
|
+
|
|
15
32
|
def visit_call_node(node)
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
replace_operator(node)
|
|
34
|
+
replace_with_methods(node)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
18
39
|
|
|
40
|
+
def replace_operator(node)
|
|
19
41
|
loc = node.message_loc
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
add_mutation(
|
|
24
|
-
offset: loc.start_offset,
|
|
25
|
-
length: loc.length,
|
|
26
|
-
replacement: replacement.to_s,
|
|
27
|
-
node: node
|
|
28
|
-
)
|
|
42
|
+
|
|
43
|
+
REPLACEMENTS.fetch(node.name, []).each do |replacement|
|
|
44
|
+
add_mutation(offset: loc.start_offset, length: loc.length, replacement: replacement.to_s, node: node)
|
|
29
45
|
end
|
|
46
|
+
end
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
def replace_with_methods(node)
|
|
49
|
+
methods = METHOD_REPLACEMENTS[node.name]
|
|
50
|
+
return unless methods && (node.arguments in Prism::ArgumentsNode[arguments: [argument]])
|
|
51
|
+
|
|
52
|
+
left = receiver_source(node.receiver)
|
|
53
|
+
methods.each do |method|
|
|
54
|
+
add_mutation(offset: node.start_offset, length: node.location.length,
|
|
55
|
+
replacement: "#{left}.#{method}(#{source_of(argument)})", node: node)
|
|
56
|
+
end
|
|
32
57
|
end
|
|
33
58
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Resolve a dynamic constant lookup with a literal name into a constant path:
|
|
6
|
+
# `Registry.const_get(:Handler)` becomes `Registry::Handler`. A literal
|
|
7
|
+
# inherit flag is dropped; a receiverless call resolves against `self`.
|
|
8
|
+
#
|
|
9
|
+
# Both forms find the same own, inherited and mixed-in constants and raise the
|
|
10
|
+
# same NameError for a missing one. They differ on top-level constants (found
|
|
11
|
+
# by `const_get` through Object, rejected by `::`), on private constants
|
|
12
|
+
# (returned by `const_get` only) and with `inherit = false` (ancestors skipped
|
|
13
|
+
# by `const_get`, searched by `::`). A survivor means none of those paths is
|
|
14
|
+
# exercised, so the dynamic lookup may not be needed.
|
|
15
|
+
#
|
|
16
|
+
# Safe navigation is skipped: a constant path has no nil-safe form.
|
|
17
|
+
class Evilution::Mutator::Operator::ConstGetToConstantPath < Evilution::Mutator::Base
|
|
18
|
+
CONSTANT_NAME = /\A[A-Z]\w*\z/
|
|
19
|
+
private_constant :CONSTANT_NAME
|
|
20
|
+
|
|
21
|
+
# `nil` behaves like `false`.
|
|
22
|
+
INHERIT_FLAGS = [Prism::TrueNode, Prism::FalseNode, Prism::NilNode].freeze
|
|
23
|
+
private_constant :INHERIT_FLAGS
|
|
24
|
+
|
|
25
|
+
def visit_call_node(node)
|
|
26
|
+
rewrite(node) if node.name == :const_get && node.block.nil? && !node.safe_navigation?
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def rewrite(node)
|
|
33
|
+
name = constant_name(node)
|
|
34
|
+
return if name.nil?
|
|
35
|
+
|
|
36
|
+
receiver = node.receiver ? source_of(node.receiver) : "self"
|
|
37
|
+
add_mutation(offset: node.start_offset, length: node.location.length, replacement: "#{receiver}::#{name}", node: node)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def constant_name(node)
|
|
41
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [Prism::SymbolNode => symbol, *flags]]
|
|
42
|
+
return unless flags.empty? || literal_inherit_flag?(flags)
|
|
43
|
+
|
|
44
|
+
symbol.unescaped if symbol.unescaped.match?(CONSTANT_NAME)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def literal_inherit_flag?(flags)
|
|
48
|
+
flags.length == 1 && INHERIT_FLAGS.any? { |type| flags.first.is_a?(type) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Make the first step of a `dig` strict: `config.dig(:db, :host)` becomes
|
|
6
|
+
# `config.fetch(:db).dig(:host)`. The reverse of index_to_dig.
|
|
7
|
+
#
|
|
8
|
+
# `dig` returns nil when the first key is missing or holds nil; the fetch
|
|
9
|
+
# chain raises there instead (KeyError / IndexError, or NoMethodError on
|
|
10
|
+
# `nil.dig`). A survivor means no test reaches the lookup with that key
|
|
11
|
+
# absent.
|
|
12
|
+
#
|
|
13
|
+
# Only calls with two or more positional keys qualify; the remaining keys
|
|
14
|
+
# keep their original formatting.
|
|
15
|
+
class Evilution::Mutator::Operator::DigToFetchChain < Evilution::Mutator::Base
|
|
16
|
+
NON_POSITIONAL = [Prism::SplatNode, Prism::KeywordHashNode, Prism::ForwardingArgumentsNode].freeze
|
|
17
|
+
private_constant :NON_POSITIONAL
|
|
18
|
+
|
|
19
|
+
def visit_call_node(node)
|
|
20
|
+
rewrite(node) if node.name == :dig && node.receiver && node.block.nil?
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Replace `dig(first, ` with `fetch(first).dig(` — up to the second key —
|
|
27
|
+
# keeping whatever opened the argument list (`(`, a newline, a space).
|
|
28
|
+
def rewrite(node)
|
|
29
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [first, second, *]]
|
|
30
|
+
return unless positional_keys?(node.arguments.arguments)
|
|
31
|
+
|
|
32
|
+
start = node.message_loc.start_offset
|
|
33
|
+
add_mutation(offset: start, length: second.start_offset - start, replacement: fetch_prefix(node, first), node: node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def positional_keys?(keys)
|
|
37
|
+
keys.none? { |key| NON_POSITIONAL.any? { |type| key.is_a?(type) } }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def fetch_prefix(node, first)
|
|
41
|
+
message_end = node.message_loc.end_offset
|
|
42
|
+
opener = byteslice_source(message_end, first.start_offset - message_end)
|
|
43
|
+
"fetch(#{source_of(first)}).dig#{opener}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a double negation with its operand: `!!value` becomes `value`, and
|
|
6
|
+
# the same for the `not not value` keyword form.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no test distinguishes a truthy object from `true` (or nil
|
|
9
|
+
# from `false`) — the boolean coercion is never observed.
|
|
10
|
+
class Evilution::Mutator::Operator::DoubleNegationRemoval < Evilution::Mutator::Base
|
|
11
|
+
def visit_call_node(node)
|
|
12
|
+
inner = node.receiver
|
|
13
|
+
promote_child(node, inner.receiver) if negation?(node) && negation?(inner)
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def negation?(node)
|
|
20
|
+
node.is_a?(Prism::CallNode) && node.name == :!
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Resolve a dynamic dispatch with a literal symbol into a direct call:
|
|
6
|
+
# `target.send(:reset)` becomes `target.reset`, and the same for `__send__`
|
|
7
|
+
# and `public_send`. Remaining arguments, a block-pass and a literal block
|
|
8
|
+
# stay attached.
|
|
9
|
+
#
|
|
10
|
+
# The two forms differ only in visibility checks, so the mutant is emitted
|
|
11
|
+
# only where those checks can disagree:
|
|
12
|
+
#
|
|
13
|
+
# - send / __send__ on an explicit receiver other than self: the direct call
|
|
14
|
+
# raises NoMethodError for a private method. With an implicit or self
|
|
15
|
+
# receiver a private method resolves either way (Ruby 2.7+).
|
|
16
|
+
# - public_send with an implicit or self receiver: the direct call reaches a
|
|
17
|
+
# private method that public_send refuses. On any other receiver both
|
|
18
|
+
# refuse it, and only a protected method called from inside its class
|
|
19
|
+
# would differ.
|
|
20
|
+
#
|
|
21
|
+
# method_missing is reached the same way by both forms.
|
|
22
|
+
class Evilution::Mutator::Operator::DynamicDispatchResolution < Evilution::Mutator::Base
|
|
23
|
+
SEND_SELECTORS = %i[send __send__].freeze
|
|
24
|
+
private_constant :SEND_SELECTORS
|
|
25
|
+
|
|
26
|
+
OPERATOR_NAMES = %w[
|
|
27
|
+
+ - * / % ** == != < > <= >= <=> === =~ !~ ! [] []= << >> & | ^ ~ +@ -@
|
|
28
|
+
].to_set.freeze
|
|
29
|
+
private_constant :OPERATOR_NAMES
|
|
30
|
+
|
|
31
|
+
METHOD_NAME = /\A[[:alpha:]_][[:alnum:]_]*[?!=]?\z/
|
|
32
|
+
private_constant :METHOD_NAME
|
|
33
|
+
|
|
34
|
+
def visit_call_node(node)
|
|
35
|
+
selector = literal_selector(node)
|
|
36
|
+
resolve(node, selector) if selector && visibility_can_differ?(node)
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def literal_selector(node)
|
|
43
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [Prism::SymbolNode => symbol, *]]
|
|
44
|
+
|
|
45
|
+
name = symbol.unescaped
|
|
46
|
+
name if name.match?(METHOD_NAME) || OPERATOR_NAMES.include?(name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def visibility_can_differ?(node)
|
|
50
|
+
self_receiver = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
51
|
+
|
|
52
|
+
if SEND_SELECTORS.include?(node.name)
|
|
53
|
+
!self_receiver
|
|
54
|
+
else
|
|
55
|
+
node.name == :public_send && self_receiver
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def resolve(node, selector)
|
|
60
|
+
start = node.message_loc.start_offset
|
|
61
|
+
following = following_item(node)
|
|
62
|
+
|
|
63
|
+
if following
|
|
64
|
+
replacement = "#{selector}#{node.opening_loc ? "(" : " "}"
|
|
65
|
+
finish = following.start_offset
|
|
66
|
+
else
|
|
67
|
+
replacement = selector
|
|
68
|
+
finish = node.closing_loc ? node.closing_loc.end_offset : node.arguments.end_offset
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
add_mutation(offset: start, length: finish - start, replacement: replacement, node: node)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The argument after the selector, or the block-pass when the selector is
|
|
75
|
+
# the only argument.
|
|
76
|
+
def following_item(node)
|
|
77
|
+
node.arguments.arguments[1] || (node.block if node.block.is_a?(Prism::BlockArgumentNode))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
|
|
7
|
+
# Give a forwarding `super` an empty argument list: `def f(a); super; end`
|
|
8
|
+
# becomes `def f(a); super(); end`.
|
|
9
|
+
#
|
|
10
|
+
# Bare `super` hands the parent whatever the method was called with; `super()`
|
|
11
|
+
# hands it nothing. A survivor means the forwarded arguments never reach an
|
|
12
|
+
# assertion — the parent ignores them, or nothing exercises what it does with
|
|
13
|
+
# them. ZsuperRemoval only turns the same node into `nil`, which asks whether
|
|
14
|
+
# the call happens rather than what it carries, and ExplicitSuperMutation works
|
|
15
|
+
# the other direction, on a super that already lists its arguments.
|
|
16
|
+
#
|
|
17
|
+
# The enclosing method must declare something to forward. With no parameters
|
|
18
|
+
# the two forms are identical, and so they are with only a block parameter:
|
|
19
|
+
# `super()` still passes the block along, so nothing would change.
|
|
20
|
+
class Evilution::Mutator::Operator::ForwardingSuperToExplicit < Evilution::Mutator::Base
|
|
21
|
+
KEYWORD_LENGTH = "super".length
|
|
22
|
+
private_constant :KEYWORD_LENGTH
|
|
23
|
+
|
|
24
|
+
def visit_def_node(node)
|
|
25
|
+
enclosing_methods.push(node)
|
|
26
|
+
super
|
|
27
|
+
ensure
|
|
28
|
+
enclosing_methods.pop
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def visit_forwarding_super_node(node)
|
|
32
|
+
add_parentheses(node) if forwards_arguments?
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# A stack, so a def nested in another method's body is answered with its own
|
|
39
|
+
# parameters rather than the outer method's.
|
|
40
|
+
def enclosing_methods
|
|
41
|
+
@enclosing_methods ||= []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Traversal always starts at the subject's own def, so a forwarding super is
|
|
45
|
+
# never reached without one on the stack.
|
|
46
|
+
def forwards_arguments?
|
|
47
|
+
parameters = enclosing_methods.last.parameters
|
|
48
|
+
return false if parameters.nil?
|
|
49
|
+
|
|
50
|
+
!only_block_parameter?(parameters)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# `&blk` is forwarded either way, so it is not something `super()` drops.
|
|
54
|
+
# Post-required parameters are not checked: a signature only has them after a
|
|
55
|
+
# rest parameter, which the rest check already rejects.
|
|
56
|
+
def only_block_parameter?(parameters)
|
|
57
|
+
parameters.requireds.empty? && parameters.optionals.empty? && parameters.keywords.empty? &&
|
|
58
|
+
parameters.rest.nil? && parameters.keyword_rest.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The parentheses are inserted right after the keyword rather than replacing
|
|
62
|
+
# the node, whose span also covers a block written on the super.
|
|
63
|
+
def add_parentheses(node)
|
|
64
|
+
add_mutation(
|
|
65
|
+
offset: node.location.start_offset + KEYWORD_LENGTH,
|
|
66
|
+
length: 0,
|
|
67
|
+
replacement: "()",
|
|
68
|
+
node: node
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Rewrite an inequality with stricter comparisons: `a != b` becomes
|
|
6
|
+
# `!a.eql?(b)` and `!a.equal?(b)`.
|
|
7
|
+
#
|
|
8
|
+
# `eql?` adds a type check (`1 != 1.0` is false, `!1.eql?(1.0)` is true) and
|
|
9
|
+
# `equal?` compares identity (two equal strings or arrays are still
|
|
10
|
+
# different objects). A survivor means no test separates value equality from
|
|
11
|
+
# the stricter forms, so the comparison in use is never pinned down.
|
|
12
|
+
#
|
|
13
|
+
# Comparisons against nil, true, false or a symbol literal are skipped: those
|
|
14
|
+
# are singletons, so all three forms always agree.
|
|
15
|
+
class Evilution::Mutator::Operator::InequalityToNegatedIdentity < Evilution::Mutator::Base
|
|
16
|
+
SELECTORS = %i[eql? equal?].freeze
|
|
17
|
+
private_constant :SELECTORS
|
|
18
|
+
|
|
19
|
+
SINGLETON_LITERALS = [Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::SymbolNode].freeze
|
|
20
|
+
private_constant :SINGLETON_LITERALS
|
|
21
|
+
|
|
22
|
+
def visit_call_node(node)
|
|
23
|
+
rewrite(node) if node.name == :!=
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def rewrite(node)
|
|
30
|
+
return unless node.arguments in Prism::ArgumentsNode[arguments: [argument]]
|
|
31
|
+
return if [node.receiver, argument].any? { |operand| singleton_literal?(operand) }
|
|
32
|
+
|
|
33
|
+
left = receiver_source(node.receiver)
|
|
34
|
+
SELECTORS.each do |selector|
|
|
35
|
+
add_mutation(offset: node.start_offset, length: node.location.length,
|
|
36
|
+
replacement: "!#{left}.#{selector}(#{source_of(argument)})", node: node)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def singleton_literal?(operand)
|
|
41
|
+
SINGLETON_LITERALS.any? { |type| operand.is_a?(type) }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Drop one keyword argument at a call site: `build(name: "x", size: 2)`
|
|
6
|
+
# becomes `build(size: 2)` and `build(name: "x")`. Applies to any argument
|
|
7
|
+
# list — calls, `super` and `yield`.
|
|
8
|
+
#
|
|
9
|
+
# A survivor means the callee never consumes that keyword, or its default
|
|
10
|
+
# happens to match. argument_removal and argument_nil_substitution skip
|
|
11
|
+
# keyword arguments entirely.
|
|
12
|
+
#
|
|
13
|
+
# Only `key: value` pairs are dropped; a `**opts` double splat stays, though
|
|
14
|
+
# it still counts as a neighbour when removing the separator. A lone keyword
|
|
15
|
+
# that is the whole argument list is left to argument_list_removal.
|
|
16
|
+
class Evilution::Mutator::Operator::KeywordArgumentRemoval < Evilution::Mutator::Base
|
|
17
|
+
def visit_arguments_node(node)
|
|
18
|
+
node.arguments.each_with_index do |argument, index|
|
|
19
|
+
next unless argument.is_a?(Prism::KeywordHashNode)
|
|
20
|
+
|
|
21
|
+
drop_keywords(argument, index.zero? ? nil : node.arguments[index - 1])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def drop_keywords(hash, previous_argument)
|
|
30
|
+
elements = hash.elements
|
|
31
|
+
return if elements.length == 1 && previous_argument.nil?
|
|
32
|
+
|
|
33
|
+
elements.each_with_index do |element, index|
|
|
34
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
35
|
+
|
|
36
|
+
drop_element(element, elements[index + 1], index.zero? ? previous_argument : elements[index - 1])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Cut from the element to the next one, or — for the last element — from
|
|
41
|
+
# the end of whatever precedes it, so the separating comma goes too.
|
|
42
|
+
def drop_element(element, following, preceding)
|
|
43
|
+
start, finish = following ? [element.start_offset, following.start_offset] : [preceding.end_offset, element.end_offset]
|
|
44
|
+
|
|
45
|
+
add_mutation(offset: start, length: finish - start, replacement: "", node: element, skip_unparseable: true)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -46,9 +46,18 @@ class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::
|
|
|
46
46
|
# class has no parent implementation of the method. We emit it only when the
|
|
47
47
|
# original body already calls super, using that as a heuristic that a super
|
|
48
48
|
# target is intended in this context.
|
|
49
|
+
#
|
|
50
|
+
# The search stops at a nested def, whose super belongs to that method rather
|
|
51
|
+
# than to this one. Counting it would emit a super replacement for a method
|
|
52
|
+
# that may have no parent to call, and the mutant would raise NoMethodError on
|
|
53
|
+
# contact — a kill that proves nothing (EV-vk1f / GH #1625). A block is not a
|
|
54
|
+
# boundary: `values.each { super }` does call this method's parent.
|
|
55
|
+
# MethodBodyToSuper#calls_super? draws the same line.
|
|
49
56
|
def body_calls_super?(node)
|
|
50
57
|
return true if node.is_a?(Prism::SuperNode) || node.is_a?(Prism::ForwardingSuperNode)
|
|
51
58
|
|
|
52
|
-
node.child_nodes.any?
|
|
59
|
+
node.child_nodes.any? do |child|
|
|
60
|
+
child && !child.is_a?(Prism::DefNode) && body_calls_super?(child)
|
|
61
|
+
end
|
|
53
62
|
end
|
|
54
63
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a whole method body with a bare `raise`: `def foo; body; end` becomes
|
|
6
|
+
# `def foo; raise; end`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example ever calls the method on a path it asserts, so
|
|
9
|
+
# the method is either dead or reached only where the suite looks away. Where
|
|
10
|
+
# body-to-nil (MethodBodyReplacement) asks whether the return value matters,
|
|
11
|
+
# this asks the prior question of whether the call happens at all — a method
|
|
12
|
+
# whose result is discarded survives the nil mutation but not this one.
|
|
13
|
+
class Evilution::Mutator::Operator::MethodBodyToRaise < Evilution::Mutator::Base
|
|
14
|
+
def visit_def_node(node)
|
|
15
|
+
replace_body_with_raise(node)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def replace_body_with_raise(node)
|
|
22
|
+
statements = body_statements(node.body)
|
|
23
|
+
return if statements.nil?
|
|
24
|
+
return if single_raise?(statements)
|
|
25
|
+
|
|
26
|
+
location = statements.location
|
|
27
|
+
|
|
28
|
+
add_mutation(
|
|
29
|
+
offset: location.start_offset,
|
|
30
|
+
length: location.length,
|
|
31
|
+
replacement: "raise",
|
|
32
|
+
node: node
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A method-level rescue/ensure (`def foo; stmts; rescue; ...; end`) makes the
|
|
37
|
+
# body a BeginNode whose location spans the entire `def...end`, keyword and
|
|
38
|
+
# matching `end` included. Replacing that range would delete the method
|
|
39
|
+
# framing and leave a bare `raise` at the enclosing scope, so only the leading
|
|
40
|
+
# statements are replaceable. Returns nil for a rescue/ensure-only body, where
|
|
41
|
+
# there are none, and for an empty method, whose body is nil outright.
|
|
42
|
+
def body_statements(body)
|
|
43
|
+
return body unless body.is_a?(Prism::BeginNode)
|
|
44
|
+
|
|
45
|
+
body.statements
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# A body that already consists of one unconditional raise would mutate to a
|
|
49
|
+
# near-equivalent: `raise NotImplementedError` -> `raise` only changes the
|
|
50
|
+
# error class, which is what RaiseArgumentStrip (#1537) probes. Arguments are
|
|
51
|
+
# not inspected, so a bare raise is skipped on the same rule.
|
|
52
|
+
def single_raise?(statements)
|
|
53
|
+
body = statements.body
|
|
54
|
+
return false unless body.length == 1
|
|
55
|
+
|
|
56
|
+
only = body.first
|
|
57
|
+
only.is_a?(Prism::CallNode) && only.name == :raise && only.receiver.nil?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
|
|
7
|
+
# Replace a whole method body with a bare `super`: `def foo; body; end` becomes
|
|
8
|
+
# `def foo; super; end`.
|
|
9
|
+
#
|
|
10
|
+
# A survivor means the body adds nothing the suite asserts over whatever
|
|
11
|
+
# ancestor implementation it might be overriding. MethodBodyReplacement emits
|
|
12
|
+
# the same replacement, but only for a body that already calls super, so a plain
|
|
13
|
+
# override — the case worth probing — is never reached by it.
|
|
14
|
+
#
|
|
15
|
+
# `super` needs somewhere to go: without one the mutant raises NoMethodError and
|
|
16
|
+
# dies on contact, which scores a kill that proves nothing. Emission is
|
|
17
|
+
# therefore limited to methods whose enclosing scope has a plausible super
|
|
18
|
+
# chain: an explicit superclass, or a mixin in the right position for that kind
|
|
19
|
+
# of method (include/prepend for instance methods, extend for singleton ones).
|
|
20
|
+
# Whether an ancestor actually defines this method name cannot be known from the
|
|
21
|
+
# source, so a class that inherits or mixes in anything qualifies.
|
|
22
|
+
class Evilution::Mutator::Operator::MethodBodyToSuper < Evilution::Mutator::Base
|
|
23
|
+
INSTANCE_MIXINS = %i[include prepend].freeze
|
|
24
|
+
SINGLETON_MIXINS = %i[extend].freeze
|
|
25
|
+
|
|
26
|
+
def visit_def_node(node)
|
|
27
|
+
replace_body_with_super(node)
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def replace_body_with_super(node)
|
|
34
|
+
statements = body_statements(node.body)
|
|
35
|
+
return if statements.nil?
|
|
36
|
+
return if calls_super?(statements)
|
|
37
|
+
return unless super_target?(node)
|
|
38
|
+
|
|
39
|
+
location = statements.location
|
|
40
|
+
|
|
41
|
+
add_mutation(
|
|
42
|
+
offset: location.start_offset,
|
|
43
|
+
length: location.length,
|
|
44
|
+
replacement: "super",
|
|
45
|
+
node: node
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A method-level rescue/ensure (`def foo; stmts; rescue; ...; end`) makes the
|
|
50
|
+
# body a BeginNode whose location spans the entire `def...end`, keyword and
|
|
51
|
+
# matching `end` included. Replacing that range would delete the method
|
|
52
|
+
# framing, so only the leading statements are replaceable. Returns nil for a
|
|
53
|
+
# rescue/ensure-only body and for an empty method, whose body is nil.
|
|
54
|
+
def body_statements(body)
|
|
55
|
+
return body unless body.is_a?(Prism::BeginNode)
|
|
56
|
+
|
|
57
|
+
body.statements
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# MethodBodyReplacement owns bodies that already reach for the parent
|
|
61
|
+
# implementation; emitting here would attribute one mutation to two operators.
|
|
62
|
+
#
|
|
63
|
+
# The search stops at a nested def, whose `super` belongs to that method
|
|
64
|
+
# rather than to the body being mutated. A nested class or module would be a
|
|
65
|
+
# boundary too, but Ruby rejects both inside a method body, so a def is the
|
|
66
|
+
# only one reachable here. A block is not a boundary: `values.map { super }`
|
|
67
|
+
# still calls the enclosing method's parent.
|
|
68
|
+
def calls_super?(node)
|
|
69
|
+
return true if node.is_a?(Prism::SuperNode) || node.is_a?(Prism::ForwardingSuperNode)
|
|
70
|
+
|
|
71
|
+
node.child_nodes.any? do |child|
|
|
72
|
+
child && !child.is_a?(Prism::DefNode) && calls_super?(child)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# `def self.foo` and a def inside `class << self` both define singleton
|
|
77
|
+
# methods, which inherit through the singleton class: an explicit superclass
|
|
78
|
+
# carries them, `extend` mixes into them, `include`/`prepend` do not.
|
|
79
|
+
def super_target?(def_node)
|
|
80
|
+
scope = enclosing_scope(def_node)
|
|
81
|
+
return false unless scope
|
|
82
|
+
|
|
83
|
+
singleton = def_node.receiver || scope[:singleton]
|
|
84
|
+
return true if scope[:node].is_a?(Prism::ClassNode) && scope[:node].superclass
|
|
85
|
+
|
|
86
|
+
mixins = singleton ? SINGLETON_MIXINS : INSTANCE_MIXINS
|
|
87
|
+
mixin?(scope[:node], mixins)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The innermost class or module containing the def. A `class << self` body is
|
|
91
|
+
# transparent to the search — the mixins that matter live in the class around
|
|
92
|
+
# it — but it marks everything inside as a singleton method.
|
|
93
|
+
def enclosing_scope(def_node)
|
|
94
|
+
tree = self.class.parsed_tree_for(@subject.file_path, @file_source)
|
|
95
|
+
finder = ScopeFinder.new(def_node.location.start_line)
|
|
96
|
+
finder.visit(tree)
|
|
97
|
+
finder.result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A scope is only consulted once a def was found inside it, so its body is
|
|
101
|
+
# never nil here. A bare `include` with nothing to mix in parses with a nil
|
|
102
|
+
# arguments node, which is what separates it from a real mixin call.
|
|
103
|
+
def mixin?(scope_node, names)
|
|
104
|
+
scope_node.body.body.any? do |child|
|
|
105
|
+
child.is_a?(Prism::CallNode) && child.receiver.nil? &&
|
|
106
|
+
names.include?(child.name) && child.arguments
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Visitor that reports the innermost class/module enclosing a line, and
|
|
111
|
+
# whether that line sits inside a `class << self` body.
|
|
112
|
+
class ScopeFinder < Prism::Visitor
|
|
113
|
+
def initialize(target_line)
|
|
114
|
+
@target_line = target_line
|
|
115
|
+
@scope = nil
|
|
116
|
+
@singleton = false
|
|
117
|
+
super()
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The enclosing scope and the `class << self` flag are collected
|
|
121
|
+
# independently: the class node is visited before the singleton-class body
|
|
122
|
+
# inside it, so a flag read at that point would always be false.
|
|
123
|
+
def result
|
|
124
|
+
return nil unless @scope
|
|
125
|
+
|
|
126
|
+
{ node: @scope, singleton: @singleton }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def visit_class_node(node)
|
|
130
|
+
@scope = node if covers?(node)
|
|
131
|
+
super
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def visit_module_node(node)
|
|
135
|
+
@scope = node if covers?(node)
|
|
136
|
+
super
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def visit_singleton_class_node(node)
|
|
140
|
+
@singleton = true if covers?(node)
|
|
141
|
+
super
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def covers?(node)
|
|
147
|
+
@target_line.between?(node.location.start_line, node.location.end_line)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|