evilution 1.3.0 → 1.5.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 +132 -0
- data/.rubocop_todo.yml +5 -0
- data/CHANGELOG.md +127 -0
- data/README.md +136 -38
- data/docs/architecture.md +135 -11
- data/docs/ast_pattern_syntax.md +50 -5
- data/docs/isolation.md +121 -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/class_body.rb +101 -0
- data/lib/evilution/ast/included_block.rb +41 -0
- data/lib/evilution/ast/literal_callable.rb +21 -0
- data/lib/evilution/ast/never_nil.rb +36 -0
- data/lib/evilution/ast/parser.rb +187 -21
- 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/round_half_mode.rb +38 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/source_surgeon.rb +2 -3
- data/lib/evilution/ast/syntax_check.rb +56 -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/exit_guard.rb +26 -4
- 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 +11 -1
- data/lib/evilution/integration/known_failures.rb +26 -0
- data/lib/evilution/integration/loading/body_call_neutralizer.rb +177 -13
- data/lib/evilution/integration/loading/callback_redeclaration.rb +186 -0
- data/lib/evilution/integration/loading/concern_redeclaration.rb +80 -0
- data/lib/evilution/integration/loading/concern_state_cleaner.rb +22 -15
- data/lib/evilution/integration/loading/mutation_applier.rb +44 -1
- data/lib/evilution/integration/loading/own_methods.rb +87 -0
- data/lib/evilution/integration/loading/redefinition_recovery.rb +2 -3
- data/lib/evilution/integration/loading/source_evaluator.rb +9 -2
- data/lib/evilution/integration/loading/superseded_modules.rb +103 -0
- data/lib/evilution/integration/loading/syntax_validator.rb +2 -2
- 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 +84 -11
- data/lib/evilution/isolation/in_process.rb +100 -8
- data/lib/evilution/mcp/info_tool/status_glossary.rb +15 -4
- data/lib/evilution/mcp/info_tool.rb +3 -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 +21 -9
- data/lib/evilution/mutator/base.rb +69 -5
- data/lib/evilution/mutator/constant_value_subjects.rb +13 -0
- data/lib/evilution/mutator/operator/alias_removal.rb +56 -0
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +2 -2
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +10 -2
- data/lib/evilution/mutator/operator/array_literal.rb +29 -0
- data/lib/evilution/mutator/operator/attr_accessor_swap.rb +68 -0
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +12 -3
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/complex_literal.rb +39 -0
- data/lib/evilution/mutator/operator/compound_assignment.rb +47 -0
- data/lib/evilution/mutator/operator/constant_namespace_strip.rb +57 -0
- data/lib/evilution/mutator/operator/constant_read_to_nil.rb +58 -0
- data/lib/evilution/mutator/operator/constant_write_to_nil.rb +32 -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/explicit_super_mutation.rb +2 -2
- data/lib/evilution/mutator/operator/float_literal.rb +54 -19
- 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/freeze_removal.rb +86 -0
- data/lib/evilution/mutator/operator/hash_literal.rb +51 -0
- data/lib/evilution/mutator/operator/index_range_to_drop.rb +67 -0
- data/lib/evilution/mutator/operator/index_receiver_to_self.rb +20 -0
- data/lib/evilution/mutator/operator/index_to_key_predicate.rb +58 -0
- data/lib/evilution/mutator/operator/index_write_to_value.rb +37 -0
- data/lib/evilution/mutator/operator/integer_division_to_fdiv.rb +62 -0
- data/lib/evilution/mutator/operator/integer_literal.rb +39 -9
- 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/memoization_break.rb +18 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +18 -55
- data/lib/evilution/mutator/operator/nil_replacement.rb +2 -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/operator_write_receiver_promotion.rb +38 -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/range_replacement.rb +2 -0
- data/lib/evilution/mutator/operator/rational_literal.rb +40 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +1 -1
- 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_class_widening.rb +54 -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/round_half_mode_swap.rb +36 -0
- data/lib/evilution/mutator/operator/safe_navigation_insertion.rb +57 -0
- data/lib/evilution/mutator/operator/safe_navigation_removal.rb +2 -24
- data/lib/evilution/mutator/operator/send_mutation.rb +2 -0
- data/lib/evilution/mutator/operator/short_circuit_to_eager.rb +34 -0
- data/lib/evilution/mutator/operator/sleep_to_zero.rb +43 -0
- data/lib/evilution/mutator/operator/splat_operator.rb +41 -24
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/string_literal.rb +29 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +14 -40
- data/lib/evilution/mutator/operator/symbol_literal.rb +47 -2
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +1 -1
- data/lib/evilution/mutator/operator/variable_read_to_nil.rb +113 -0
- data/lib/evilution/mutator/operator/visibility_removal.rb +74 -0
- data/lib/evilution/mutator/primitives.rb +24 -2
- data/lib/evilution/mutator/receiver_positions.rb +79 -0
- data/lib/evilution/mutator/registry.rb +58 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +6 -0
- data/lib/evilution/parallel/work_queue/channel.rb +58 -5
- data/lib/evilution/parallel/work_queue/dispatcher/reply.rb +41 -0
- data/lib/evilution/parallel/work_queue/dispatcher.rb +14 -1
- data/lib/evilution/parallel/work_queue/worker/loop.rb +5 -0
- data/lib/evilution/parallel/work_queue/worker.rb +13 -4
- data/lib/evilution/parallel/work_queue.rb +15 -9
- 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/strategy/parallel.rb +10 -0
- 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/target.rb +86 -0
- data/lib/evilution/runner/subject_pipeline.rb +57 -43
- data/lib/evilution/runner.rb +5 -2
- data/lib/evilution/subject.rb +25 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +90 -0
- data/script/memory_check +62 -16
- metadata +113 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Swap the operands of a spaceship comparison: `a <=> b` becomes `b <=> a`.
|
|
6
|
+
#
|
|
7
|
+
# The result changes sign, so a sort block sorts the other way and a custom
|
|
8
|
+
# `<=>` orders the other way round. A survivor means no example asserts the
|
|
9
|
+
# direction of the ordering.
|
|
10
|
+
#
|
|
11
|
+
# Two-argument ordering methods (`between?`, `clamp`) are reversed by
|
|
12
|
+
# ArgumentOrderPermutation. Identical operands are not swapped, since that
|
|
13
|
+
# would reproduce the original comparison, and neither is a safe-navigation
|
|
14
|
+
# call, where moving the other operand into the receiver's place changes what
|
|
15
|
+
# may be nil.
|
|
16
|
+
class Evilution::Mutator::Operator::ComparisonOperandSwap < Evilution::Mutator::Base
|
|
17
|
+
def visit_call_node(node)
|
|
18
|
+
swap_operands(node) if spaceship?(node)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Ruby has no receiverless spaceship, but the explicit form can be written
|
|
25
|
+
# with no argument or several (`a.<=>`, `a.<=>(b, c)`); those have no pair
|
|
26
|
+
# of operands to swap.
|
|
27
|
+
def spaceship?(node)
|
|
28
|
+
return false unless node.name == :<=> && !node.safe_navigation?
|
|
29
|
+
return false if node.arguments.nil?
|
|
30
|
+
|
|
31
|
+
node.arguments.arguments.length == 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whatever sits between the operands — the operator and its spacing, or
|
|
35
|
+
# `.<=>(` in the explicit form — stays in place.
|
|
36
|
+
def swap_operands(node)
|
|
37
|
+
left = node.receiver
|
|
38
|
+
right = node.arguments.arguments.first
|
|
39
|
+
return if left.slice == right.slice
|
|
40
|
+
|
|
41
|
+
start_offset = left.location.start_offset
|
|
42
|
+
|
|
43
|
+
add_mutation(
|
|
44
|
+
offset: start_offset,
|
|
45
|
+
length: right.location.end_offset - start_offset,
|
|
46
|
+
replacement: "#{right.slice}#{source_between(left, right)}#{left.slice}",
|
|
47
|
+
node: node
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def source_between(left, right)
|
|
52
|
+
byteslice_source(left.location.end_offset, right.location.start_offset - left.location.end_offset)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a complex literal (`5i`) with `0i`, `1i`, its two neighbours along
|
|
6
|
+
# the imaginary axis (`6i`, `4i`) and `nil`.
|
|
7
|
+
#
|
|
8
|
+
# The literal is replaced as a whole: IntegerLiteral and FloatLiteral leave
|
|
9
|
+
# its numeric part alone.
|
|
10
|
+
class Evilution::Mutator::Operator::ComplexLiteral < Evilution::Mutator::Base
|
|
11
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
12
|
+
|
|
13
|
+
def visit_imaginary_node(node)
|
|
14
|
+
replacements_for(node.numeric).each { |part| add_mutation_with_replacement(node, "#{part}i") }
|
|
15
|
+
add_mutation_with_replacement(node, "nil")
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Equal values are emitted once, the integer first: `1i` over `1.0i`.
|
|
23
|
+
def replacements_for(numeric)
|
|
24
|
+
part = numeric.value
|
|
25
|
+
[0, 1, *neighbours_of(numeric)].reject { |candidate| candidate == part }.uniq(&:to_r)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A rational part (`3ri`) gets none: its neighbours have no literal of
|
|
29
|
+
# their own in general.
|
|
30
|
+
def neighbours_of(numeric)
|
|
31
|
+
return [] if numeric.is_a?(Prism::RationalNode)
|
|
32
|
+
|
|
33
|
+
[numeric.value + 1, numeric.value - 1]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_mutation_with_replacement(node, replacement)
|
|
37
|
+
add_mutation(offset: node.location.start_offset, length: node.location.length, replacement:, node:)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::CompoundAssignment < Evilution::Mutator::Base
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
6
8
|
REPLACEMENTS = {
|
|
7
9
|
:+ => %i[- *],
|
|
8
10
|
:- => [:+],
|
|
@@ -37,6 +39,51 @@ class Evilution::Mutator::Operator::CompoundAssignment < Evilution::Mutator::Bas
|
|
|
37
39
|
super
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
# `obj.count += 1`: the target is read through `count` and written
|
|
43
|
+
# through `count=`.
|
|
44
|
+
def visit_call_operator_write_node(node)
|
|
45
|
+
mutate_operator_write(node)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `counts[key] += 1`: read through `[]` and written through `[]=`.
|
|
50
|
+
def visit_index_operator_write_node(node)
|
|
51
|
+
mutate_operator_write(node)
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `TOTAL += 1` is written outside any method and is a constant-write
|
|
56
|
+
# subject of its own, like the assignment `TOTAL = 1` before it.
|
|
57
|
+
def visit_constant_operator_write_node(node)
|
|
58
|
+
mutate_operator_write(node)
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def visit_constant_path_operator_write_node(node)
|
|
63
|
+
mutate_operator_write(node)
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def visit_call_and_write_node(node)
|
|
68
|
+
mutate_logical_write(node, "||=")
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def visit_call_or_write_node(node)
|
|
73
|
+
mutate_logical_write(node, "&&=")
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def visit_index_and_write_node(node)
|
|
78
|
+
mutate_logical_write(node, "||=")
|
|
79
|
+
super
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visit_index_or_write_node(node)
|
|
83
|
+
mutate_logical_write(node, "&&=")
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
40
87
|
def visit_local_variable_and_write_node(node)
|
|
41
88
|
mutate_logical_write(node, "||=")
|
|
42
89
|
super
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Strip the namespace off a constant path: `Config::LIMIT` becomes `LIMIT`.
|
|
6
|
+
# A longer path is stripped at each level, so `App::Config::LIMIT` gives
|
|
7
|
+
# both `LIMIT` and `Config::LIMIT`.
|
|
8
|
+
#
|
|
9
|
+
# The bare name is looked up from where the code stands instead of inside
|
|
10
|
+
# the namespace. It either finds another constant or raises NameError; a
|
|
11
|
+
# survivor means the same constant is reachable without the qualifier, or
|
|
12
|
+
# that nothing depends on which one is found. A namespace given by an
|
|
13
|
+
# expression (`self.class::LIMIT`) is stripped too — there the survivor says
|
|
14
|
+
# no test overrides the constant in a subclass.
|
|
15
|
+
#
|
|
16
|
+
# A top-level path (`::LIMIT`) has no namespace to strip. The target of an
|
|
17
|
+
# operator write (`Config::LIMIT ||= x`) is left alone: bare, `LIMIT ||= x`
|
|
18
|
+
# is a dynamic constant assignment inside a method. Prism lets it through,
|
|
19
|
+
# where parse.y rejects it with a SyntaxError, so whether the mutant exists
|
|
20
|
+
# would depend on the parser the interpreter compiles with.
|
|
21
|
+
class Evilution::Mutator::Operator::ConstantNamespaceStrip < Evilution::Mutator::Base
|
|
22
|
+
def call(subject, **)
|
|
23
|
+
@write_targets = Set.new
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_constant_path_or_write_node(node)
|
|
28
|
+
@write_targets.add(node.target)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def visit_constant_path_and_write_node(node)
|
|
33
|
+
@write_targets.add(node.target)
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visit_constant_path_operator_write_node(node)
|
|
38
|
+
@write_targets.add(node.target)
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def visit_constant_path_node(node)
|
|
43
|
+
replace_span(node: node, target: node, replacement: name_source(node)) if strippable?(node)
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def strippable?(node)
|
|
50
|
+
node.parent && !@write_targets.include?(node)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def name_source(node)
|
|
54
|
+
location = node.name_loc
|
|
55
|
+
byteslice_source(location.start_offset, location.length)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Replace a constant reference with `nil`: `MAX` becomes `nil`, and a path
|
|
6
|
+
# such as `Config::MAX` goes as a whole.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means the constant's value never reaches an assertion — the
|
|
9
|
+
# tests would pass whatever it held.
|
|
10
|
+
#
|
|
11
|
+
# Four positions are skipped as noise. A constant used as the receiver of a
|
|
12
|
+
# call turns into `nil.new`, a NoMethodError any test that reaches it kills;
|
|
13
|
+
# so does the receiver of a write (`Counter.total += 1`, `CACHE[key] ||= v`).
|
|
14
|
+
# The namespace of a path would leave `nil::MAX`, a TypeError; the path is
|
|
15
|
+
# nil-ified instead. A constant in void statement position (every statement
|
|
16
|
+
# of a body except the last) has no value to replace; deleting it is
|
|
17
|
+
# statement_deletion's job. And the exception classes of a `rescue` clause
|
|
18
|
+
# belong to the rescue operators — `rescue nil` only fails once something is
|
|
19
|
+
# raised, and then with a TypeError of its own.
|
|
20
|
+
class Evilution::Mutator::Operator::ConstantReadToNil < Evilution::Mutator::Base
|
|
21
|
+
include Evilution::Mutator::ReceiverPositions
|
|
22
|
+
|
|
23
|
+
def call(subject, **)
|
|
24
|
+
@skipped_nodes = Set.new
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def visit_statements_node(node)
|
|
29
|
+
@skipped_nodes.merge(node.body[...-1])
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_rescue_node(node)
|
|
34
|
+
@skipped_nodes.merge(node.exceptions)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_constant_path_node(node)
|
|
39
|
+
@skipped_nodes.add(node.parent)
|
|
40
|
+
nilify(node)
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def visit_constant_read_node(node)
|
|
45
|
+
nilify(node)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def receiver_found(node)
|
|
52
|
+
@skipped_nodes.add(node)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def nilify(node)
|
|
56
|
+
mutate_to_nil(node) unless @skipped_nodes.include?(node)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
# Assign `nil` to a constant instead of its value: `LIMIT = 10` becomes
|
|
6
|
+
# `LIMIT = nil`.
|
|
7
|
+
#
|
|
8
|
+
# A survivor means nothing the tests run depends on what the constant holds.
|
|
9
|
+
# The literal operators go on to change the value piece by piece; this one
|
|
10
|
+
# takes it away whole, whatever it is — a literal, a call, a lambda.
|
|
11
|
+
#
|
|
12
|
+
# It works on constant-write subjects, each being one assignment, and does
|
|
13
|
+
# not look inside the value: a constant assigned there (in the block of a
|
|
14
|
+
# `Class.new`) is a subject of its own. A constant already assigned `nil`
|
|
15
|
+
# has nothing to lose. An operator write (`LIMIT += 1`) is left to
|
|
16
|
+
# compound_assignment, which replaces the whole of it with `nil`.
|
|
17
|
+
#
|
|
18
|
+
# Code that read the constant while the file first loaded keeps the old
|
|
19
|
+
# value (see Subject), so only what reads it when called sees the mutant.
|
|
20
|
+
class Evilution::Mutator::Operator::ConstantWriteToNil < Evilution::Mutator::Base
|
|
21
|
+
def self.subject_kinds
|
|
22
|
+
%i[constant_write]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def visit_constant_write_node(node)
|
|
26
|
+
mutate_to_nil(node, target: node.value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def visit_constant_path_write_node(node)
|
|
30
|
+
mutate_to_nil(node, target: node.value)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../operator"
|
|
6
|
+
require_relative "../../ast/value_object_definition"
|
|
7
|
+
|
|
8
|
+
# Mutate the member list of a value-object definition: `Data.define(:a, :b)`
|
|
9
|
+
# and `Struct.new(:a, :b)` lose one member at a time, and have each adjacent
|
|
10
|
+
# pair of members swapped.
|
|
11
|
+
#
|
|
12
|
+
# A surviving drop means no example reads or sets that member. A surviving swap
|
|
13
|
+
# means the type is built positionally somewhere the order is never asserted,
|
|
14
|
+
# so two members could trade values unnoticed.
|
|
15
|
+
#
|
|
16
|
+
# Definitions outside any method — assigned to a constant, or the superclass
|
|
17
|
+
# of a class — are constant subjects of their own, and this operator mutates
|
|
18
|
+
# them there. A constant subject is the definition alone: definitions nested
|
|
19
|
+
# in its block are subjects of their own too.
|
|
20
|
+
class Evilution::Mutator::Operator::DataStructMember < Evilution::Mutator::Base
|
|
21
|
+
# Arguments whose position and meaning are known: members are symbols, a
|
|
22
|
+
# string is the class name `Struct.new("Name", ...)` takes first, and the
|
|
23
|
+
# keyword hash carries `keyword_init:`. Anything else (a splat, a variable)
|
|
24
|
+
# hides the member list, so the definition is left alone.
|
|
25
|
+
KNOWN_ARGUMENT_TYPES = [Prism::SymbolNode, Prism::StringNode, Prism::KeywordHashNode].freeze
|
|
26
|
+
|
|
27
|
+
def self.subject_kinds
|
|
28
|
+
%i[method constant]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A constant subject's node is the definition itself, so the visit stops
|
|
32
|
+
# there rather than reaching definitions nested in its block.
|
|
33
|
+
def visit_call_node(node)
|
|
34
|
+
mutate_members(node) if Evilution::AST::ValueObjectDefinition.match?(node)
|
|
35
|
+
super unless @subject.kind == :constant
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def mutate_members(node)
|
|
41
|
+
arguments = argument_nodes(node)
|
|
42
|
+
members = member_indexes(arguments)
|
|
43
|
+
# A lone member has no neighbour to swap with, and dropping it would leave
|
|
44
|
+
# `Struct.new()`, which raises on Rubies that require at least one member.
|
|
45
|
+
return if members.length < 2
|
|
46
|
+
|
|
47
|
+
slices = arguments.map(&:slice)
|
|
48
|
+
members.each { |index| emit_member_drop(node, slices, index) }
|
|
49
|
+
members.each_cons(2) { |left, right| emit_member_swap(node, slices, left, right) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def member_indexes(arguments)
|
|
53
|
+
return [] unless arguments.all? { |argument| KNOWN_ARGUMENT_TYPES.include?(argument.class) }
|
|
54
|
+
|
|
55
|
+
arguments.each_index.select { |index| arguments[index].is_a?(Prism::SymbolNode) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def emit_member_drop(node, slices, index)
|
|
59
|
+
remaining = slices.dup
|
|
60
|
+
remaining.delete_at(index)
|
|
61
|
+
replace_arguments(node, remaining)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def emit_member_swap(node, slices, left, right)
|
|
65
|
+
return if slices[left] == slices[right]
|
|
66
|
+
|
|
67
|
+
swapped = slices.dup
|
|
68
|
+
swapped[left], swapped[right] = swapped[right], swapped[left]
|
|
69
|
+
replace_arguments(node, swapped)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def replace_arguments(node, slices)
|
|
73
|
+
location = node.arguments.location
|
|
74
|
+
|
|
75
|
+
add_mutation(
|
|
76
|
+
offset: location.start_offset,
|
|
77
|
+
length: location.length,
|
|
78
|
+
replacement: slices.join(", "),
|
|
79
|
+
node: node
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -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
|
|
@@ -5,8 +5,8 @@ require_relative "../operator"
|
|
|
5
5
|
class Evilution::Mutator::Operator::ExplicitSuperMutation < Evilution::Mutator::Base
|
|
6
6
|
def visit_super_node(node)
|
|
7
7
|
replace_with_zsuper(node)
|
|
8
|
-
args = node
|
|
9
|
-
mutate_arguments(node, args)
|
|
8
|
+
args = argument_nodes(node)
|
|
9
|
+
mutate_arguments(node, args) unless args.empty?
|
|
10
10
|
|
|
11
11
|
super
|
|
12
12
|
end
|
|
@@ -3,27 +3,62 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::FloatLiteral < Evilution::Mutator::Base
|
|
6
|
+
extend Evilution::Mutator::ConstantValueSubjects
|
|
7
|
+
|
|
8
|
+
# NaN fails every comparison and equality; the infinities break arithmetic
|
|
9
|
+
# on bounds. None of them can be reached from `0.0`, `1.0` or `nil`.
|
|
10
|
+
SPECIAL_VALUES = ["Float::NAN", "Float::INFINITY", "-Float::INFINITY"].freeze
|
|
11
|
+
|
|
12
|
+
# A guard wraps the pattern of its branch: `in Float if x > 1.0`.
|
|
13
|
+
GUARDS = [Prism::IfNode, Prism::UnlessNode].freeze
|
|
14
|
+
|
|
6
15
|
def visit_float_node(node)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
add_mutation(
|
|
14
|
-
offset: node.location.start_offset,
|
|
15
|
-
length: node.location.length,
|
|
16
|
-
replacement: replacement,
|
|
17
|
-
node: node
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
add_mutation(
|
|
21
|
-
offset: node.location.start_offset,
|
|
22
|
-
length: node.location.length,
|
|
23
|
-
replacement: "nil",
|
|
24
|
-
node: node
|
|
25
|
-
)
|
|
16
|
+
add_mutation_with_replacement(node, node.value == 0.0 ? "1.0" : "0.0")
|
|
17
|
+
SPECIAL_VALUES.each { |value| add_mutation_with_replacement(node, value) } unless pattern_literals.include?(node)
|
|
18
|
+
add_mutation_with_replacement(node, "nil")
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
26
22
|
|
|
23
|
+
# A pattern takes literals, not expressions: `in -Float::INFINITY` and
|
|
24
|
+
# `in Float::NAN..2.0` do not parse. The floats of a pattern keep the
|
|
25
|
+
# literal replacements only. A guard and a pinned expression (`^(x + 1.0)`)
|
|
26
|
+
# are ordinary code.
|
|
27
|
+
def visit_in_node(node)
|
|
28
|
+
pattern = node.pattern
|
|
29
|
+
pattern = pattern.statements if GUARDS.include?(pattern.class)
|
|
30
|
+
mark_pattern_literals(pattern)
|
|
27
31
|
super
|
|
28
32
|
end
|
|
33
|
+
|
|
34
|
+
def visit_match_predicate_node(node)
|
|
35
|
+
mark_pattern_literals(node.pattern)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def visit_match_required_node(node)
|
|
40
|
+
mark_pattern_literals(node.pattern)
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A complex literal (`5i`) is replaced as a whole by ComplexLiteral. Its
|
|
45
|
+
# numeric part is not a literal of its own: `nili` is not a value.
|
|
46
|
+
def visit_imaginary_node(_node); end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def pattern_literals
|
|
51
|
+
@pattern_literals ||= Set.new.compare_by_identity
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def mark_pattern_literals(node)
|
|
55
|
+
return if node.is_a?(Prism::PinnedExpressionNode)
|
|
56
|
+
|
|
57
|
+
pattern_literals.add(node) if node.is_a?(Prism::FloatNode)
|
|
58
|
+
node.compact_child_nodes.each { |child| mark_pattern_literals(child) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_mutation_with_replacement(node, replacement)
|
|
62
|
+
add_mutation(offset: node.location.start_offset, length: node.location.length, replacement:, node:)
|
|
63
|
+
end
|
|
29
64
|
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
|