evilution 1.3.0 → 1.4.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 +62 -0
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +77 -0
- data/README.md +80 -24
- data/docs/architecture.md +89 -9
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +39 -2
- data/docs/migration-from-mutant.md +1 -1
- data/lib/evilution/ast/aasm_declaration.rb +133 -0
- data/lib/evilution/ast/callback_declaration.rb +91 -0
- data/lib/evilution/ast/included_block.rb +27 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/parser.rb +119 -16
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +14 -15
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/uncovered_code.rb +88 -0
- data/lib/evilution/ast/value_object_definition.rb +25 -0
- data/lib/evilution/baseline/failure_formatter.rb +27 -0
- data/lib/evilution/baseline/report.rb +66 -0
- data/lib/evilution/baseline/spec_failure.rb +38 -0
- data/lib/evilution/baseline.rb +67 -30
- data/lib/evilution/cli/parser/file_args.rb +2 -1
- data/lib/evilution/cli/parser/options_builder.rb +1 -1
- data/lib/evilution/cli.rb +3 -2
- data/lib/evilution/config/validators/spec_mappings.rb +2 -1
- data/lib/evilution/config.rb +3 -2
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/condition.rb +66 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/disturbance.rb +54 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/early_exit.rb +73 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/guard.rb +36 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/index_read.rb +82 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch/node_path.rb +43 -0
- data/lib/evilution/equivalent/heuristic/guarded_index_fetch.rb +65 -0
- data/lib/evilution/example_filter.rb +45 -0
- data/lib/evilution/hooks/registry.rb +2 -1
- data/lib/evilution/integration/base.rb +2 -0
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +132 -13
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +61 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +19 -12
- data/lib/evilution/integration/loading/mutation_applier.rb +16 -0
- data/lib/evilution/integration/loading/test_class_cache.rb +46 -0
- data/lib/evilution/integration/minitest/test_ids.rb +21 -0
- data/lib/evilution/integration/minitest.rb +45 -6
- data/lib/evilution/integration/rspec/baseline_runner.rb +47 -3
- data/lib/evilution/integration/rspec/example_ids.rb +35 -0
- data/lib/evilution/integration/rspec/result_builder.rb +6 -1
- data/lib/evilution/integration/rspec/state_guard/anonymous_example_group_examples.rb +36 -0
- data/lib/evilution/integration/rspec/state_guard.rb +4 -1
- data/lib/evilution/integration/rspec.rb +22 -4
- data/lib/evilution/integration/test_unit/result_builder.rb +6 -0
- data/lib/evilution/integration/test_unit/test_ids.rb +18 -0
- data/lib/evilution/integration/test_unit.rb +37 -9
- data/lib/evilution/isolation/fork.rb +2 -1
- data/lib/evilution/isolation/in_process.rb +21 -4
- data/lib/evilution/mcp/info_tool/status_glossary.rb +2 -2
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +2 -1
- data/lib/evilution/memory/leak_check.rb +27 -3
- data/lib/evilution/mutation.rb +7 -2
- data/lib/evilution/mutator/base.rb +49 -5
- data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/exception_swallow.rb +84 -0
- data/lib/evilution/mutator/operator/format_specifier_swap.rb +100 -0
- data/lib/evilution/mutator/operator/forwarded_argument_drop.rb +142 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/keyword_argument.rb +25 -2
- data/lib/evilution/mutator/operator/keyword_value_swap.rb +75 -0
- data/lib/evilution/mutator/operator/no_matching_pattern_else.rb +47 -0
- data/lib/evilution/mutator/operator/numbered_parameter_swap.rb +78 -0
- data/lib/evilution/mutator/operator/off_by_one_boundary.rb +68 -0
- data/lib/evilution/mutator/operator/optional_parameter_to_required.rb +14 -3
- data/lib/evilution/mutator/operator/pattern_matching_array.rb +12 -1
- data/lib/evilution/mutator/operator/pattern_wildcard_widening.rb +117 -0
- data/lib/evilution/mutator/operator/pin_operator_removal.rb +48 -0
- data/lib/evilution/mutator/operator/regex_simplification.rb +53 -132
- data/lib/evilution/mutator/operator/regexp_alternation_branch_deletion.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_anchor_promotion.rb +43 -0
- data/lib/evilution/mutator/operator/regexp_capture_to_passive.rb +61 -0
- data/lib/evilution/mutator/operator/regexp_character_type_complement.rb +54 -0
- data/lib/evilution/mutator/operator/regexp_named_group_rename.rb +106 -0
- data/lib/evilution/mutator/operator/regexp_option_removal.rb +51 -0
- data/lib/evilution/mutator/operator/regexp_quantifier_minimum_swap.rb +45 -0
- data/lib/evilution/mutator/operator/rescue_else_concatenation.rb +62 -0
- data/lib/evilution/mutator/operator/rescue_handler_concatenation.rb +69 -0
- data/lib/evilution/mutator/operator/rescue_handler_promotion.rb +65 -0
- data/lib/evilution/mutator/operator/return_keyword_removal.rb +79 -0
- data/lib/evilution/mutator/operator/rightward_assignment.rb +46 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +2 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/registry.rb +31 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/reporter/cli/line_formatters/baseline_neutralized_notice.rb +55 -0
- data/lib/evilution/reporter/cli/metrics_block.rb +2 -0
- data/lib/evilution/reporter/json/baseline.rb +15 -0
- data/lib/evilution/reporter/json.rb +7 -2
- data/lib/evilution/result/baseline_neutralization.rb +10 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +9 -4
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +22 -8
- data/lib/evilution/runner/mutation_executor/result_cache.rb +3 -0
- data/lib/evilution/runner/mutation_executor/result_packer.rb +4 -2
- data/lib/evilution/runner/mutation_executor.rb +1 -1
- data/lib/evilution/runner/mutation_planner.rb +11 -2
- data/lib/evilution/runner/report_publisher.rb +3 -2
- data/lib/evilution/runner/subject_pipeline.rb +46 -1
- data/lib/evilution/runner.rb +4 -1
- data/lib/evilution/subject.rb +8 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +28 -0
- data/script/memory_check +62 -16
- metadata +83 -4
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swallow the error of a statement that raises by convention:
|
|
6
|
+
# `record.save!` becomes `record.save! rescue nil`.
|
|
7
|
+
#
|
|
8
|
+
# The mutant behaves like the original until the statement fails; then it
|
|
9
|
+
# carries on with nil instead of raising. A survivor means no example makes
|
|
10
|
+
# that statement fail and checks the error comes out. It is the inverse of
|
|
11
|
+
# RescueRemoval: handling is added where there is none.
|
|
12
|
+
#
|
|
13
|
+
# Only calls whose failure is part of their contract are reached: bang
|
|
14
|
+
# methods, `fetch`, and the conversion functions `Integer`, `Float` and
|
|
15
|
+
# `Rational`. Wrapping every statement would mostly report that no test makes
|
|
16
|
+
# a log line raise. Statements already covered by a rescue are left alone, as
|
|
17
|
+
# is `raise`, whose swallowed form behaves as deleting the statement.
|
|
18
|
+
class Evilution::Mutator::Operator::ExceptionSwallow < Evilution::Mutator::Base
|
|
19
|
+
CONVERSION_FUNCTIONS = %i[Integer Float Rational].freeze
|
|
20
|
+
|
|
21
|
+
# A bang method name, as opposed to the `!` and `!=` operators.
|
|
22
|
+
BANG_METHOD = /\w!\z/
|
|
23
|
+
|
|
24
|
+
# Bangs that do not raise: Ruby's in-place methods mark a change to the
|
|
25
|
+
# receiver, and exit! ends the process without raising.
|
|
26
|
+
NON_RAISING_BANGS = %i[
|
|
27
|
+
uniq! sort! sort_by! select! filter! reject! map! collect! compact! flatten!
|
|
28
|
+
shuffle! reverse! rotate! slice! merge! transform_keys! transform_values!
|
|
29
|
+
gsub! sub! strip! lstrip! rstrip! chomp! chop! squeeze! tr! tr_s! delete!
|
|
30
|
+
downcase! upcase! capitalize! swapcase! unicode_normalize! scrub! encode!
|
|
31
|
+
exit!
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# Assignments whose value is the statement's call.
|
|
35
|
+
WRITE_TYPES = [
|
|
36
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
37
|
+
Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
def visit_statements_node(node)
|
|
41
|
+
node.body.each { |statement| swallow(statement) }
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Everything under a `begin` (or a `def` or block body) with a rescue clause
|
|
46
|
+
# is already handled there, so the visitor does not descend into it.
|
|
47
|
+
def visit_begin_node(node)
|
|
48
|
+
super if node.rescue_clause.nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def swallow(statement)
|
|
54
|
+
call = statement_call(statement)
|
|
55
|
+
return unless call && raising?(call)
|
|
56
|
+
|
|
57
|
+
add_mutation(
|
|
58
|
+
offset: statement.location.end_offset,
|
|
59
|
+
length: 0,
|
|
60
|
+
replacement: " rescue nil",
|
|
61
|
+
node: call
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def statement_call(statement)
|
|
66
|
+
statement = statement.value if WRITE_TYPES.include?(statement.class)
|
|
67
|
+
statement if statement.is_a?(Prism::CallNode)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def raising?(call)
|
|
71
|
+
name = call.name
|
|
72
|
+
return !NON_RAISING_BANGS.include?(name) if name.to_s.match?(BANG_METHOD)
|
|
73
|
+
return raising_fetch?(call) if name == :fetch
|
|
74
|
+
|
|
75
|
+
call.receiver.nil? && CONVERSION_FUNCTIONS.include?(name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# With a default value or a block, fetch returns that instead of raising.
|
|
79
|
+
def raising_fetch?(call)
|
|
80
|
+
return false if call.receiver.nil? || call.block
|
|
81
|
+
|
|
82
|
+
call.arguments.nil? || call.arguments.arguments.length == 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Loosen the conversion specifiers of a literal format string, one at a time:
|
|
6
|
+
#
|
|
7
|
+
# format("%05d", n) -> format("%d", n) flags and width dropped
|
|
8
|
+
# sprintf("%.2f", x) -> sprintf("%f", x) precision dropped
|
|
9
|
+
# -> sprintf("%s", x) rendered as a plain string
|
|
10
|
+
#
|
|
11
|
+
# A survivor means the output is produced but its shape — padding, decimals,
|
|
12
|
+
# number base — is never asserted.
|
|
13
|
+
#
|
|
14
|
+
# Reaches the format string of receiverless `format`, `sprintf` and `printf`
|
|
15
|
+
# and the string receiver of `"..." % args`, when it is a plain literal. Integer
|
|
16
|
+
# conversions (`%d`, `%i`, `%u`) are not turned into `%s`: for an integer
|
|
17
|
+
# argument both print the same digits.
|
|
18
|
+
class Evilution::Mutator::Operator::FormatSpecifierSwap < Evilution::Mutator::Base
|
|
19
|
+
FORMAT_METHODS = %i[format sprintf printf].freeze
|
|
20
|
+
|
|
21
|
+
# `%%` and `%{name}` are matched so they are not mistaken for the start of
|
|
22
|
+
# a specifier; neither is mutated.
|
|
23
|
+
SPECIFIER = /
|
|
24
|
+
%(?:
|
|
25
|
+
%
|
|
26
|
+
| \{\w+\}
|
|
27
|
+
| (?<name><\w+>)?(?<flags>[-+\ 0\#]*)(?<width>\d+|\*)?(?:\.(?<precision>\d+|\*))?(?<type>[a-zA-Z])
|
|
28
|
+
)
|
|
29
|
+
/x
|
|
30
|
+
|
|
31
|
+
# Conversions whose output differs from `%s` for the values they take.
|
|
32
|
+
STRINGIFIABLE_TYPES = %w[f e E g G a A x X o b B].freeze
|
|
33
|
+
|
|
34
|
+
def visit_call_node(node)
|
|
35
|
+
format_string = format_string_of(node)
|
|
36
|
+
mutate_specifiers(node, format_string) if format_string
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def format_string_of(node)
|
|
43
|
+
candidate =
|
|
44
|
+
if node.receiver.nil? && FORMAT_METHODS.include?(node.name)
|
|
45
|
+
first_argument(node)
|
|
46
|
+
elsif node.name == :%
|
|
47
|
+
node.receiver
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
candidate if plain_literal?(candidate)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def first_argument(node)
|
|
54
|
+
node.arguments.arguments.first if node.arguments
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A heredoc's content sits after the line that opens it, so edits inside
|
|
58
|
+
# it are left alone along with interpolated strings. Only `%w[]` elements
|
|
59
|
+
# lack an opening delimiter, and those never stand here on their own.
|
|
60
|
+
def plain_literal?(node)
|
|
61
|
+
node.is_a?(Prism::StringNode) && !node.opening_loc.slice.start_with?("<<")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def mutate_specifiers(node, format_string)
|
|
65
|
+
specifiers(format_string).each do |match, offset|
|
|
66
|
+
variants(match).each do |replacement|
|
|
67
|
+
add_mutation(offset: offset, length: match[0].bytesize, replacement: replacement, node: node)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Each conversion specifier with its offset in the file. The scan runs over
|
|
73
|
+
# the source text between the quotes, so offsets line up with the file even
|
|
74
|
+
# when the string holds escapes.
|
|
75
|
+
def specifiers(format_string)
|
|
76
|
+
content = format_string.content_loc
|
|
77
|
+
text = byteslice_source(content.start_offset, content.length)
|
|
78
|
+
|
|
79
|
+
text.to_enum(:scan, SPECIFIER).filter_map do
|
|
80
|
+
match = Regexp.last_match
|
|
81
|
+
[match, content.start_offset + match.byteoffset(0).first] unless match[:type].nil?
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# A `*` width or precision consumes an argument of its own, so dropping it
|
|
86
|
+
# would shift every argument after it.
|
|
87
|
+
def variants(match)
|
|
88
|
+
return [] if [match[:width], match[:precision]].include?("*")
|
|
89
|
+
|
|
90
|
+
name = match[:name]
|
|
91
|
+
replacements = []
|
|
92
|
+
replacements << "%#{name}#{match[:type]}" if modifiers?(match)
|
|
93
|
+
replacements << "%#{name}s" if STRINGIFIABLE_TYPES.include?(match[:type])
|
|
94
|
+
replacements
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def modifiers?(match)
|
|
98
|
+
!match[:flags].empty? || !match[:width].nil? || !match[:precision].nil?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Stop forwarding one part of the arguments a method passes through unnamed:
|
|
6
|
+
# `target.call(*, **, &)` becomes `target.call(**, &)`, and so on for each part.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means that part never reaches an assertion — nothing checks that
|
|
9
|
+
# the positional arguments, the keywords or the block arrive at the other end.
|
|
10
|
+
#
|
|
11
|
+
# Two spellings are handled. Anonymous `*` and `**` are dropped from the
|
|
12
|
+
# argument list they are written in. A `...` cannot be split where it stands,
|
|
13
|
+
# because `*`, `**` and `&` may not be written under a `...` signature, so the
|
|
14
|
+
# signature is spelled out as `(*, **, &)` along with every use, and one use
|
|
15
|
+
# loses one part: `def f(...) = g(...)` becomes `def f(*, **, &) = g(**, &)`.
|
|
16
|
+
#
|
|
17
|
+
# Named splats are left to SplatOperator and KeywordArgument, and an anonymous
|
|
18
|
+
# `&` written on its own to BlockPassRemoval.
|
|
19
|
+
class Evilution::Mutator::Operator::ForwardedArgumentDrop < Evilution::Mutator::Base
|
|
20
|
+
PARTS = ["*", "**", "&"].freeze
|
|
21
|
+
ALL_PARTS = PARTS.join(", ").freeze
|
|
22
|
+
|
|
23
|
+
def visit_def_node(node)
|
|
24
|
+
drop_forwarded_parts(node)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_call_node(node)
|
|
29
|
+
drop_anonymous_arguments(node)
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_super_node(node)
|
|
34
|
+
drop_anonymous_arguments(node)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_yield_node(node)
|
|
39
|
+
drop_anonymous_arguments(node)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def drop_anonymous_arguments(node)
|
|
46
|
+
return if node.arguments.nil?
|
|
47
|
+
|
|
48
|
+
items = argument_items(node.arguments)
|
|
49
|
+
# Dropping the only argument leaves an empty list, which is the mutant
|
|
50
|
+
# ArgumentListRemoval already emits.
|
|
51
|
+
return if items.length < 2
|
|
52
|
+
|
|
53
|
+
items.each_index do |index|
|
|
54
|
+
emit_argument_drop(node, items, index) if anonymous?(items[index])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Keywords sit together in one KeywordHashNode, `**` among them; they are
|
|
59
|
+
# listed individually so each can be dropped on its own.
|
|
60
|
+
def argument_items(arguments)
|
|
61
|
+
arguments.arguments.flat_map do |argument|
|
|
62
|
+
argument.is_a?(Prism::KeywordHashNode) ? argument.elements : [argument]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def anonymous?(item)
|
|
67
|
+
case item
|
|
68
|
+
when Prism::SplatNode then item.expression.nil?
|
|
69
|
+
when Prism::AssocSplatNode then item.value.nil?
|
|
70
|
+
else false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def emit_argument_drop(node, items, index)
|
|
75
|
+
remaining = items.map(&:slice)
|
|
76
|
+
remaining.delete_at(index)
|
|
77
|
+
location = node.arguments.location
|
|
78
|
+
|
|
79
|
+
add_mutation(
|
|
80
|
+
offset: location.start_offset,
|
|
81
|
+
length: location.length,
|
|
82
|
+
replacement: remaining.join(", "),
|
|
83
|
+
node: node
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def drop_forwarded_parts(node)
|
|
88
|
+
return if node.body.nil?
|
|
89
|
+
|
|
90
|
+
uses = forwarding_uses(node.body)
|
|
91
|
+
uses.each do |target|
|
|
92
|
+
PARTS.each { |part| emit_part_drop(node, uses, target, part) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Every `...` the method forwards. A method defined in the body is not
|
|
97
|
+
# searched: its `...` forwards that method's own arguments.
|
|
98
|
+
def forwarding_uses(node)
|
|
99
|
+
return [] if node.is_a?(Prism::DefNode)
|
|
100
|
+
return [node] if node.is_a?(Prism::ForwardingArgumentsNode)
|
|
101
|
+
|
|
102
|
+
node.compact_child_nodes.flat_map { |child| forwarding_uses(child) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A `...` can only be forwarded by the method that declares it, so a use
|
|
106
|
+
# found in the body means the signature ends in the matching parameter.
|
|
107
|
+
def emit_part_drop(node, uses, target, part)
|
|
108
|
+
edits = uses.map do |use|
|
|
109
|
+
[use.location, use.equal?(target) ? (PARTS - [part]).join(", ") : ALL_PARTS]
|
|
110
|
+
end
|
|
111
|
+
edits << [node.parameters.keyword_rest.location, ALL_PARTS]
|
|
112
|
+
|
|
113
|
+
emit_rewrite(node, edits.sort_by { |location, _text| location.start_offset })
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The signature and the uses are apart in the source, while a mutation
|
|
117
|
+
# replaces one contiguous range, so the range runs from the first edit to
|
|
118
|
+
# the last and is rewritten with every edit applied.
|
|
119
|
+
def emit_rewrite(node, edits)
|
|
120
|
+
start_offset = edits.first.first.start_offset
|
|
121
|
+
length = edits.last.first.end_offset - start_offset
|
|
122
|
+
|
|
123
|
+
add_mutation(
|
|
124
|
+
offset: start_offset,
|
|
125
|
+
length: length,
|
|
126
|
+
replacement: apply_edits(byteslice_source(start_offset, length), edits, start_offset),
|
|
127
|
+
node: node
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Applied last to first, so an edit never shifts the offsets of those still
|
|
132
|
+
# to come.
|
|
133
|
+
def apply_edits(text, edits, base_offset)
|
|
134
|
+
edits.reverse_each do |location, replacement|
|
|
135
|
+
text = text.byteslice(0, location.start_offset - base_offset) +
|
|
136
|
+
replacement +
|
|
137
|
+
text.byteslice((location.end_offset - base_offset)..)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
text
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Turn a division into `fdiv`: `a / b` becomes `a.fdiv(b)`.
|
|
6
|
+
#
|
|
7
|
+
# Between integers `/` drops the remainder while `fdiv` keeps it as a float,
|
|
8
|
+
# and for exact types such as Rational or BigDecimal `fdiv` falls back to
|
|
9
|
+
# Float. A survivor means no example divides values that leave a remainder,
|
|
10
|
+
# or none checks what happens to it.
|
|
11
|
+
#
|
|
12
|
+
# A float literal on either side is skipped: float division and `fdiv` give
|
|
13
|
+
# the same answer, so the mutant would change nothing.
|
|
14
|
+
class Evilution::Mutator::Operator::IntegerDivisionToFdiv < Evilution::Mutator::Base
|
|
15
|
+
# Receivers that `.fdiv` can follow directly. Anything else is an operator
|
|
16
|
+
# expression, which `.fdiv` would bind into, so it is grouped first.
|
|
17
|
+
SIMPLE_RECEIVER_TYPES = [
|
|
18
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
19
|
+
Prism::GlobalVariableReadNode, Prism::ConstantReadNode, Prism::ConstantPathNode,
|
|
20
|
+
Prism::IntegerNode, Prism::ParenthesesNode, Prism::SelfNode
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
# A method name, as opposed to an operator such as `*` or `-@`.
|
|
24
|
+
METHOD_NAME = /\A[a-zA-Z_]\w*[?!]?\z/
|
|
25
|
+
|
|
26
|
+
def visit_call_node(node)
|
|
27
|
+
replace_division(node) if division?(node)
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# The explicit form (`a./(b)`) is left alone: it is rare, and rewriting it
|
|
34
|
+
# gains nothing over the operator form. The operator form always has a
|
|
35
|
+
# receiver and exactly one divisor.
|
|
36
|
+
def division?(node)
|
|
37
|
+
return false unless node.name == :/ && node.call_operator_loc.nil?
|
|
38
|
+
|
|
39
|
+
[node.receiver, *node.arguments.arguments].none?(Prism::FloatNode)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def replace_division(node)
|
|
43
|
+
divisor = node.arguments.arguments.first
|
|
44
|
+
|
|
45
|
+
add_mutation(
|
|
46
|
+
offset: node.location.start_offset,
|
|
47
|
+
length: node.location.length,
|
|
48
|
+
replacement: "#{receiver_text(node.receiver)}.fdiv(#{divisor.slice})",
|
|
49
|
+
node: node
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def receiver_text(receiver)
|
|
54
|
+
simple_receiver?(receiver) ? receiver.slice : "(#{receiver.slice})"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def simple_receiver?(receiver)
|
|
58
|
+
return true if SIMPLE_RECEIVER_TYPES.include?(receiver.class)
|
|
59
|
+
|
|
60
|
+
receiver.is_a?(Prism::CallNode) && receiver.name.to_s.match?(METHOD_NAME)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -8,7 +8,7 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
|
8
8
|
if params
|
|
9
9
|
mutate_optional_keyword_defaults(params)
|
|
10
10
|
mutate_optional_keyword_removal(params)
|
|
11
|
-
mutate_keyword_rest_removal(
|
|
11
|
+
mutate_keyword_rest_removal(node)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
super
|
|
@@ -51,9 +51,11 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def mutate_keyword_rest_removal(
|
|
54
|
+
def mutate_keyword_rest_removal(node)
|
|
55
|
+
params = node.parameters
|
|
55
56
|
kr = params.keyword_rest
|
|
56
57
|
return unless kr.is_a?(Prism::KeywordRestParameterNode)
|
|
58
|
+
return if anonymous_rest_used?(node.body)
|
|
57
59
|
|
|
58
60
|
all_params = collect_all_params(params)
|
|
59
61
|
if all_params.length < 2
|
|
@@ -63,6 +65,27 @@ class Evilution::Mutator::Operator::KeywordArgument < Evilution::Mutator::Base
|
|
|
63
65
|
end
|
|
64
66
|
end
|
|
65
67
|
|
|
68
|
+
# An anonymous `**` in the body (`bar(**)`, `{ ** }`) only parses while the
|
|
69
|
+
# signature declares one, so removing it there would leave the body
|
|
70
|
+
# unparseable. Finding one also tells the rest is anonymous: Ruby rejects it
|
|
71
|
+
# next to a named rest. A named rest is safe to remove: without it the body
|
|
72
|
+
# reads an undefined name, which parses and fails at runtime.
|
|
73
|
+
def anonymous_rest_used?(body)
|
|
74
|
+
return false if body.nil?
|
|
75
|
+
|
|
76
|
+
uses_anonymous_rest?(body)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A nested def is not searched: an anonymous `**` inside it belongs to that
|
|
80
|
+
# method's own signature. Blocks and lambdas share the enclosing method's
|
|
81
|
+
# parameters, so they are.
|
|
82
|
+
def uses_anonymous_rest?(node)
|
|
83
|
+
return false if node.is_a?(Prism::DefNode)
|
|
84
|
+
return true if node.is_a?(Prism::AssocSplatNode) && node.value.nil?
|
|
85
|
+
|
|
86
|
+
node.compact_child_nodes.any? { |child| uses_anonymous_rest?(child) }
|
|
87
|
+
end
|
|
88
|
+
|
|
66
89
|
def emit_remove_only_kr(kr)
|
|
67
90
|
add_mutation(
|
|
68
91
|
offset: kr.location.start_offset,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap the values of neighbouring keyword arguments, keeping the keys:
|
|
6
|
+
# `compute(x: a, y: b)` becomes `compute(x: b, y: a)`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example tells the two values apart — the keyword form
|
|
9
|
+
# of the defect ArgumentOrderPermutation probes for positional arguments.
|
|
10
|
+
#
|
|
11
|
+
# Only keyword arguments written in the call are swapped: a braced hash is a
|
|
12
|
+
# single positional argument whose pairs are data. A double splat stays in
|
|
13
|
+
# place, and identical values are not swapped, since that would reproduce the
|
|
14
|
+
# original call. A shorthand key (`compute(a:, b:)`) has no value of its own
|
|
15
|
+
# to move, so the swap spells it out: `compute(a: b, b: a)`.
|
|
16
|
+
class Evilution::Mutator::Operator::KeywordValueSwap < Evilution::Mutator::Base
|
|
17
|
+
def visit_call_node(node)
|
|
18
|
+
swap_keyword_values(node)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def visit_super_node(node)
|
|
23
|
+
swap_keyword_values(node)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_yield_node(node)
|
|
28
|
+
swap_keyword_values(node)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def swap_keyword_values(node)
|
|
35
|
+
return if node.arguments.nil?
|
|
36
|
+
|
|
37
|
+
node.arguments.arguments.grep(Prism::KeywordHashNode).each do |keywords|
|
|
38
|
+
pairs = keywords.elements.grep(Prism::AssocNode)
|
|
39
|
+
pairs.each_cons(2) do |left, right|
|
|
40
|
+
emit_swap(node, left, right) unless value_text(left) == value_text(right)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whatever separates the two pairs — the comma, a double splat, line breaks,
|
|
46
|
+
# comments — stays between them, so the call keeps its layout.
|
|
47
|
+
def emit_swap(node, left, right)
|
|
48
|
+
start_offset = left.location.start_offset
|
|
49
|
+
|
|
50
|
+
add_mutation(
|
|
51
|
+
offset: start_offset,
|
|
52
|
+
length: right.location.end_offset - start_offset,
|
|
53
|
+
replacement: "#{key_text(left)}#{value_text(right)}#{source_between(left, right)}" \
|
|
54
|
+
"#{key_text(right)}#{value_text(left)}",
|
|
55
|
+
node: node
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def source_between(left, right)
|
|
60
|
+
byteslice_source(left.location.end_offset, right.location.start_offset - left.location.end_offset)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The key with everything up to its value: `x: `, `"x" => `.
|
|
64
|
+
def key_text(pair)
|
|
65
|
+
return "#{pair.key.slice} " if pair.value.is_a?(Prism::ImplicitNode)
|
|
66
|
+
|
|
67
|
+
byteslice_source(pair.location.start_offset, pair.value.location.start_offset - pair.location.start_offset)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A shorthand key stands for the local variable or method of the same name.
|
|
71
|
+
def value_text(pair)
|
|
72
|
+
value = pair.value
|
|
73
|
+
value.is_a?(Prism::ImplicitNode) ? value.value.name.to_s : value.slice
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Give a `case/in` that has no `else` an empty one.
|
|
6
|
+
#
|
|
7
|
+
# Without an else, a value no pattern matches raises NoMatchingPatternError;
|
|
8
|
+
# with an empty one the expression quietly yields nil. A survivor means no
|
|
9
|
+
# example feeds the match a value outside its patterns, so nothing asserts
|
|
10
|
+
# that such a value is rejected.
|
|
11
|
+
#
|
|
12
|
+
# The opposite edit, removing an else that is there, belongs to CaseIn.
|
|
13
|
+
class Evilution::Mutator::Operator::NoMatchingPatternElse < Evilution::Mutator::Base
|
|
14
|
+
def visit_case_match_node(node)
|
|
15
|
+
add_else(node) if node.else_clause.nil? && !exhaustive?(node)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def add_else(node)
|
|
22
|
+
offset = node.end_keyword_loc.start_offset
|
|
23
|
+
|
|
24
|
+
add_mutation(
|
|
25
|
+
offset: offset,
|
|
26
|
+
length: 0,
|
|
27
|
+
replacement: else_clause_before(offset),
|
|
28
|
+
node: node
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# An `end` on a line of its own gets the else on the line above, indented
|
|
33
|
+
# alike. Otherwise the case is written on one line and the else joins it.
|
|
34
|
+
def else_clause_before(offset)
|
|
35
|
+
line_start = line_start_byte(@file_source, offset)
|
|
36
|
+
indentation = byteslice_source(line_start, offset - line_start)
|
|
37
|
+
|
|
38
|
+
indentation.strip.empty? ? "else\n#{indentation}" : "else; "
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A bare name (`in other`, `in _`) matches every value, so an else after it
|
|
42
|
+
# could never run and adding one would change nothing. A guarded capture is
|
|
43
|
+
# not a bare name: Prism wraps it in the guard's conditional.
|
|
44
|
+
def exhaustive?(node)
|
|
45
|
+
node.conditions.any? { |clause| clause.pattern.is_a?(Prism::LocalVariableTargetNode) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap the numbered parameters of a block: `pairs.map { _1 - _2 }` becomes
|
|
6
|
+
# `pairs.map { _2 - _1 }`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means no example tells the two values apart, so the block could
|
|
9
|
+
# take them in either order unnoticed.
|
|
10
|
+
#
|
|
11
|
+
# Only parameters the body reads are swapped with each other, each with its
|
|
12
|
+
# neighbour. Swapping in a parameter the body never reads (`{ _1 }` to
|
|
13
|
+
# `{ _2 }`) would change how many values the block takes, which is a different
|
|
14
|
+
# mutation from reordering them.
|
|
15
|
+
class Evilution::Mutator::Operator::NumberedParameterSwap < Evilution::Mutator::Base
|
|
16
|
+
NUMBERED_NAME = /\A_[1-9]\z/
|
|
17
|
+
|
|
18
|
+
def visit_block_node(node)
|
|
19
|
+
swap_parameters(node)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_lambda_node(node)
|
|
24
|
+
swap_parameters(node)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def swap_parameters(node)
|
|
31
|
+
return unless node.parameters.is_a?(Prism::NumberedParametersNode)
|
|
32
|
+
|
|
33
|
+
reads = numbered_reads(node.body)
|
|
34
|
+
names = reads.map { |read| read.name.to_s }.uniq.sort
|
|
35
|
+
names.each_cons(2) { |left, right| emit_swap(node, reads, left, right) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Every read of a numbered parameter in the body. Blocks nested in it are
|
|
39
|
+
# searched too: a numbered parameter read there can only be this block's,
|
|
40
|
+
# since Ruby rejects a nested block that declares its own. A method defined
|
|
41
|
+
# in the body opens a new scope, whose blocks have parameters of their own.
|
|
42
|
+
def numbered_reads(node)
|
|
43
|
+
return [] if node.is_a?(Prism::DefNode)
|
|
44
|
+
return [node] if node.is_a?(Prism::LocalVariableReadNode) && node.name.to_s.match?(NUMBERED_NAME)
|
|
45
|
+
|
|
46
|
+
node.compact_child_nodes.flat_map { |child| numbered_reads(child) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The reads are scattered through the body, while a mutation replaces one
|
|
50
|
+
# contiguous range, so the range runs from the first affected read to the
|
|
51
|
+
# last and is rewritten with the two names exchanged.
|
|
52
|
+
def emit_swap(node, reads, left, right)
|
|
53
|
+
affected = reads.select { |read| [left, right].include?(read.name.to_s) }
|
|
54
|
+
.sort_by { |read| read.location.start_offset }
|
|
55
|
+
start_offset = affected.first.location.start_offset
|
|
56
|
+
end_offset = affected.last.location.end_offset
|
|
57
|
+
|
|
58
|
+
add_mutation(
|
|
59
|
+
offset: start_offset,
|
|
60
|
+
length: end_offset - start_offset,
|
|
61
|
+
replacement: exchange(affected, start_offset, end_offset, left => right, right => left),
|
|
62
|
+
node: node
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def exchange(affected, start_offset, end_offset, renames)
|
|
67
|
+
text = byteslice_source(start_offset, end_offset - start_offset)
|
|
68
|
+
|
|
69
|
+
affected.reverse_each do |read|
|
|
70
|
+
location = read.location
|
|
71
|
+
text = text.byteslice(0, location.start_offset - start_offset) +
|
|
72
|
+
renames.fetch(read.name.to_s) +
|
|
73
|
+
text.byteslice((location.end_offset - start_offset)..)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
text
|
|
77
|
+
end
|
|
78
|
+
end
|