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
|
@@ -24,11 +24,21 @@ class Evilution::Integration::Base
|
|
|
24
24
|
return load_error if load_error
|
|
25
25
|
|
|
26
26
|
fire_hook(:mutation_insert_post, mutation: mutation, file_path: mutation.file_path)
|
|
27
|
-
run_tests(mutation)
|
|
27
|
+
Evilution.while_running_tests { run_tests(mutation) }
|
|
28
|
+
ensure
|
|
29
|
+
@mutation_applier.restore(mutation) unless restored_by_applier?(load_error)
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
private
|
|
31
33
|
|
|
34
|
+
# The applier evaluates the original to tell that a mutant broke the load of
|
|
35
|
+
# its file; after that the file is as it was, and need not be restored again.
|
|
36
|
+
def restored_by_applier?(load_error)
|
|
37
|
+
return false if load_error.nil?
|
|
38
|
+
|
|
39
|
+
load_error[:load_crashed] == true
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
def ensure_framework_loaded
|
|
33
43
|
raise NotImplementedError, "#{self.class}#ensure_framework_loaded must be implemented"
|
|
34
44
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration"
|
|
4
|
+
|
|
5
|
+
# The tests the baseline saw failing before any mutation ran, by the ids the
|
|
6
|
+
# integration gives them.
|
|
7
|
+
#
|
|
8
|
+
# Such a test fails whatever the mutation is, so a mutation run in which
|
|
9
|
+
# nothing else failed has no verdict. The question is asked in the process
|
|
10
|
+
# that ran the tests: the names of everything that failed need not travel
|
|
11
|
+
# back, only the answer.
|
|
12
|
+
class Evilution::Integration::KnownFailures
|
|
13
|
+
def initialize(ids)
|
|
14
|
+
@ids = ids.to_set
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def empty?
|
|
18
|
+
@ids.empty?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Whether failed_ids names at least one test, and none the baseline did not
|
|
22
|
+
# already see failing.
|
|
23
|
+
def only?(failed_ids)
|
|
24
|
+
!failed_ids.empty? && failed_ids.all? { |id| @ids.include?(id) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
require_relative "../loading"
|
|
5
|
+
require_relative "../../ast/aasm_declaration"
|
|
6
|
+
require_relative "../../ast/included_block"
|
|
7
|
+
require_relative "../../ast/callback_declaration"
|
|
8
|
+
require_relative "callback_redeclaration"
|
|
9
|
+
require_relative "concern_redeclaration"
|
|
5
10
|
|
|
6
11
|
# Strip non-idempotent class/module-body side-effect calls from a mutated
|
|
7
12
|
# source before re-eval. Such calls (e.g. dry-monads `register_mixin`, plugin
|
|
@@ -14,8 +19,9 @@ require_relative "../loading"
|
|
|
14
19
|
# or module body (not inside a def). Calls on a small allowlist of patterns
|
|
15
20
|
# known to be idempotent (`include`, `attr_*`, visibility modifiers, etc.)
|
|
16
21
|
# are preserved; everything else has its source range replaced with `()`.
|
|
17
|
-
#
|
|
18
|
-
# from the end of the source backwards
|
|
22
|
+
# A call spanning several lines leaves its line breaks behind, so the source
|
|
23
|
+
# keeps its line count. Edits are applied from the end of the source backwards
|
|
24
|
+
# to keep earlier offsets valid.
|
|
19
25
|
class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
20
26
|
IDEMPOTENT_CALLS = %i[
|
|
21
27
|
include extend prepend using
|
|
@@ -33,6 +39,12 @@ class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
|
33
39
|
# to nil without the warning on every supported Ruby (`(nil)` still warns on 3.3).
|
|
34
40
|
REPLACEMENT = "()"
|
|
35
41
|
|
|
42
|
+
REDECLARATION = "::Evilution::Integration::Loading::ConcernRedeclaration"
|
|
43
|
+
GUARD = "#{REDECLARATION}.skipping? or ".freeze
|
|
44
|
+
REDECLARE = "; #{REDECLARATION}.call(self)".freeze
|
|
45
|
+
REDECLARE_PREPENDED = "; #{REDECLARATION}.call(self, :prepended)".freeze
|
|
46
|
+
CALLBACKS = "::Evilution::Integration::Loading::CallbackRedeclaration"
|
|
47
|
+
|
|
36
48
|
class << self
|
|
37
49
|
attr_writer :preloaded_features
|
|
38
50
|
|
|
@@ -60,13 +72,35 @@ class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
|
60
72
|
# strips method definitions that subsequent sibling statements (alias, etc.)
|
|
61
73
|
# depend on, producing cascading NameError. Callers that don't pass a path
|
|
62
74
|
# get the legacy always-neutralize behavior.
|
|
63
|
-
|
|
75
|
+
#
|
|
76
|
+
# `keep_offset` (optional) spares the one class-body call whose source
|
|
77
|
+
# contains that byte offset: a mutation inside a scope body only takes
|
|
78
|
+
# effect if the mutated file re-runs that `scope` call to replace it.
|
|
79
|
+
# When that call is an `aasm` machine, only the `event` or `state` holding
|
|
80
|
+
# the offset is spared inside its block: re-running one declaration replaces
|
|
81
|
+
# it, while re-running the whole machine would register its
|
|
82
|
+
# `after_all_transitions`-style callbacks a second time.
|
|
83
|
+
# A concern's `included` block is never blanked, whether or not it holds
|
|
84
|
+
# the offset: classes that include the concern later run all of it. Neither
|
|
85
|
+
# is its `prepended` block, for the classes that prepend it later.
|
|
86
|
+
# When it does hold the offset, every other call in
|
|
87
|
+
# it is guarded and the block is followed by a ConcernRedeclaration call,
|
|
88
|
+
# which re-declares the spared statement alone on the classes that include
|
|
89
|
+
# the concern already. Both are added on existing lines, so no line moves.
|
|
90
|
+
# An `aasm` machine inside such a block is treated the same way one level
|
|
91
|
+
# down: its other declarations are guarded, not blanked.
|
|
92
|
+
# When it is a callback declaration (`validate ... if: -> { }`), it is
|
|
93
|
+
# wrapped in a CallbackRedeclaration call, which puts what the re-run
|
|
94
|
+
# registers in the place of what the declaration registered before.
|
|
95
|
+
# `keep_lines` is where that was: the declaration's lines in the file as it
|
|
96
|
+
# was loaded.
|
|
97
|
+
def call(source, file_path: nil, keep_offset: nil, keep_lines: nil)
|
|
64
98
|
return source if file_path && !preloaded?(file_path)
|
|
65
99
|
|
|
66
100
|
result = Prism.parse(source)
|
|
67
101
|
return source if result.failure?
|
|
68
102
|
|
|
69
|
-
edits = collect_edits(result.value)
|
|
103
|
+
edits = collect_edits(source, result.value, keep_offset, keep_lines)
|
|
70
104
|
return source if edits.empty?
|
|
71
105
|
|
|
72
106
|
apply_edits(source, edits)
|
|
@@ -78,26 +112,36 @@ class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
|
78
112
|
self.class.preloaded_features.include?(File.expand_path(file_path))
|
|
79
113
|
end
|
|
80
114
|
|
|
81
|
-
def collect_edits(tree)
|
|
115
|
+
def collect_edits(source, tree, keep_offset, keep_lines)
|
|
82
116
|
edits = []
|
|
83
|
-
walker = Walker.new(IDEMPOTENT_CALLS, edits)
|
|
117
|
+
walker = Walker.new(source, IDEMPOTENT_CALLS, edits, keep_offset, keep_lines)
|
|
84
118
|
walker.visit(tree)
|
|
85
119
|
edits
|
|
86
120
|
end
|
|
87
121
|
|
|
88
122
|
def apply_edits(source, edits)
|
|
89
123
|
bytes = source.b
|
|
90
|
-
edits.sort_by!(&:first).reverse_each do |start_offset, end_offset|
|
|
91
|
-
bytes[start_offset, end_offset - start_offset] =
|
|
124
|
+
edits.sort_by!(&:first).reverse_each do |start_offset, end_offset, replacement|
|
|
125
|
+
bytes[start_offset, end_offset - start_offset] = replacement.b
|
|
92
126
|
end
|
|
93
127
|
bytes.force_encoding(source.encoding)
|
|
94
128
|
end
|
|
95
129
|
|
|
96
130
|
class Walker < Prism::Visitor
|
|
97
|
-
|
|
131
|
+
CONSTANT_WRITES = [
|
|
132
|
+
Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
|
|
133
|
+
Prism::ConstantOperatorWriteNode, Prism::ConstantPathOperatorWriteNode,
|
|
134
|
+
Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode,
|
|
135
|
+
Prism::ConstantAndWriteNode, Prism::ConstantPathAndWriteNode
|
|
136
|
+
].freeze
|
|
137
|
+
|
|
138
|
+
def initialize(source, allowlist, edits, keep_offset, keep_lines)
|
|
98
139
|
super()
|
|
140
|
+
@source = source
|
|
99
141
|
@allowlist = allowlist
|
|
100
142
|
@edits = edits
|
|
143
|
+
@keep_offset = keep_offset
|
|
144
|
+
@keep_lines = keep_lines
|
|
101
145
|
end
|
|
102
146
|
|
|
103
147
|
def visit_class_node(node)
|
|
@@ -126,14 +170,134 @@ class Evilution::Integration::Loading::BodyCallNeutralizer
|
|
|
126
170
|
return unless body_node.is_a?(Prism::StatementsNode)
|
|
127
171
|
|
|
128
172
|
body_node.body.each do |stmt|
|
|
129
|
-
next unless
|
|
130
|
-
|
|
131
|
-
|
|
173
|
+
next unless neutralizable?(stmt)
|
|
174
|
+
|
|
175
|
+
edit = edit_for(stmt)
|
|
176
|
+
if kept?(edit)
|
|
177
|
+
scan_kept(stmt)
|
|
178
|
+
elsif !registration_only?(stmt)
|
|
179
|
+
@edits << edit
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# A concern's `included do ... end` runs nothing where it is written: it
|
|
185
|
+
# hands the block over for the classes that include the concern from then
|
|
186
|
+
# on. Blanked, it would leave the concern without one -- ConcernStateCleaner
|
|
187
|
+
# has just dropped the block it had -- and a class including the concern
|
|
188
|
+
# during the run would get none of its declarations. Left as written, it
|
|
189
|
+
# registers the same block again. `prepended do ... end` is the same hook
|
|
190
|
+
# for the classes that prepend the concern.
|
|
191
|
+
def registration_only?(stmt)
|
|
192
|
+
block = Evilution::AST::IncludedBlock.of(stmt) || Evilution::AST::IncludedBlock.prepended(stmt)
|
|
193
|
+
!block.nil?
|
|
194
|
+
end
|
|
132
195
|
|
|
133
|
-
|
|
196
|
+
def scan_kept(stmt)
|
|
197
|
+
scan_machine(stmt)
|
|
198
|
+
scan_included(stmt)
|
|
199
|
+
wrap_callbacks(stmt)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A kept callback declaration runs inside a block handed to
|
|
203
|
+
# CallbackRedeclaration, opened and closed on the declaration's own lines.
|
|
204
|
+
def wrap_callbacks(stmt)
|
|
205
|
+
return unless @keep_lines && Evilution::AST::CallbackDeclaration.match?(stmt)
|
|
206
|
+
|
|
207
|
+
start_offset = stmt.location.start_offset
|
|
208
|
+
end_offset = stmt.location.end_offset
|
|
209
|
+
lines = "#{@keep_lines.first}..#{@keep_lines.last}"
|
|
210
|
+
@edits << [start_offset, start_offset, "#{CALLBACKS}.call(self, __FILE__, #{lines}) { "]
|
|
211
|
+
@edits << [end_offset, end_offset, " }"]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# A kept `included` block stays whole. Its other calls are guarded rather
|
|
215
|
+
# than blanked, and what follows it re-declares the spared one on the
|
|
216
|
+
# classes that include the concern already. A kept `prepended` block is
|
|
217
|
+
# treated the same, for the classes that prepend it.
|
|
218
|
+
def scan_included(stmt)
|
|
219
|
+
redeclare_hook(stmt, Evilution::AST::IncludedBlock.of(stmt), REDECLARE)
|
|
220
|
+
redeclare_hook(stmt, Evilution::AST::IncludedBlock.prepended(stmt), REDECLARE_PREPENDED)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def redeclare_hook(stmt, block, redeclare)
|
|
224
|
+
return unless block && block.body.is_a?(Prism::StatementsNode)
|
|
225
|
+
|
|
226
|
+
guard_others(block)
|
|
227
|
+
end_offset = stmt.location.end_offset
|
|
228
|
+
@edits << [end_offset, end_offset, redeclare]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Guards every call of a block but the one holding the offset. When that
|
|
232
|
+
# one is an `aasm` machine, its own declarations are guarded the same way
|
|
233
|
+
# rather than blanked: a class that includes the concern later needs the
|
|
234
|
+
# whole machine, and one that includes it already only the mutated event.
|
|
235
|
+
# When it is a callback declaration, it is wrapped as it would be in a
|
|
236
|
+
# class body: the block runs as the including class, so that is whose
|
|
237
|
+
# chains CallbackRedeclaration rebuilds.
|
|
238
|
+
# A method defined in the block is guarded like a call, unless it holds
|
|
239
|
+
# the offset: defined again on a class that includes the concern already,
|
|
240
|
+
# it would replace what that class has since defined under the same name.
|
|
241
|
+
# So is a constant assigned there, which would lose what it has gathered
|
|
242
|
+
# since. A local variable is assigned again: the rest of the block reads it.
|
|
243
|
+
def guard_others(block)
|
|
244
|
+
block.body.body.each do |statement|
|
|
245
|
+
case statement
|
|
246
|
+
when Prism::CallNode then guard_call(statement)
|
|
247
|
+
when Prism::DefNode, *CONSTANT_WRITES then guard(statement) unless holds_offset?(statement)
|
|
248
|
+
end
|
|
134
249
|
end
|
|
135
250
|
end
|
|
136
251
|
|
|
252
|
+
def guard_call(call)
|
|
253
|
+
return guard(call) unless kept?(edit_for(call))
|
|
254
|
+
|
|
255
|
+
machine = Evilution::AST::AasmDeclaration.machine_block(call)
|
|
256
|
+
guard_others(machine) if machine && machine.body.is_a?(Prism::StatementsNode)
|
|
257
|
+
wrap_callbacks(call)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def guard(statement)
|
|
261
|
+
start_offset = statement.location.start_offset
|
|
262
|
+
@edits << [start_offset, start_offset, GUARD]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def holds_offset?(node)
|
|
266
|
+
kept?([node.location.start_offset, node.location.end_offset])
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def neutralizable?(stmt)
|
|
270
|
+
return false unless stmt.is_a?(Prism::CallNode)
|
|
271
|
+
return false if @allowlist.include?(stmt.name)
|
|
272
|
+
|
|
273
|
+
stmt.receiver.nil? || stmt.receiver.is_a?(Prism::SelfNode)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Inside a kept `aasm` block every call is a declaration of the machine,
|
|
277
|
+
# so all of them but the one holding the offset go.
|
|
278
|
+
def scan_machine(stmt)
|
|
279
|
+
block = Evilution::AST::AasmDeclaration.machine_block(stmt)
|
|
280
|
+
return unless block && block.body.is_a?(Prism::StatementsNode)
|
|
281
|
+
|
|
282
|
+
block.body.body.grep(Prism::CallNode).each do |declaration|
|
|
283
|
+
edit = edit_for(declaration)
|
|
284
|
+
@edits << edit unless kept?(edit)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# The lines a blanked call took up are kept as empty lines: everything
|
|
289
|
+
# after it then sits on the line it has in the file, which is where a proc
|
|
290
|
+
# created by this source says it was written.
|
|
291
|
+
def edit_for(call)
|
|
292
|
+
start_offset = call.location.start_offset
|
|
293
|
+
end_offset = replacement_end_offset(call)
|
|
294
|
+
[start_offset, end_offset, REPLACEMENT + ("\n" * @source.byteslice(start_offset...end_offset).count("\n"))]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def kept?((start_offset, end_offset, _replacement))
|
|
298
|
+
@keep_offset && @keep_offset >= start_offset && @keep_offset < end_offset
|
|
299
|
+
end
|
|
300
|
+
|
|
137
301
|
# Prism CallNode location ends at the close of the call syntax (e.g. the
|
|
138
302
|
# closing `)` or the `<<~MARKER` opener for a heredoc argument). It does
|
|
139
303
|
# NOT include the heredoc body lines or the trailing terminator. Replacing
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
|
|
5
|
+
# Runs a callback declaration again and puts what it registers where the
|
|
6
|
+
# callbacks it registered before were.
|
|
7
|
+
#
|
|
8
|
+
# ActiveSupport::Callbacks does not replace on re-declaration. A callback
|
|
9
|
+
# named by a symbol is dropped and added again at the end of its chain; one
|
|
10
|
+
# that is a block or an object (a validator) is simply added, next to the
|
|
11
|
+
# first. Either way a mutated `if:` would run in a chain that is no longer
|
|
12
|
+
# the one the class was written with -- reordered, or with the unmutated
|
|
13
|
+
# original still answering.
|
|
14
|
+
#
|
|
15
|
+
# So the chains are read before the declaration runs and rebuilt afterwards:
|
|
16
|
+
# the callbacks this declaration registered last time give their place to the
|
|
17
|
+
# new ones, and everything else stays where it was, including what subclasses
|
|
18
|
+
# added themselves. See BodyCallNeutralizer for the source that calls this.
|
|
19
|
+
#
|
|
20
|
+
# "Last time" is the file's own load the first time round, and those
|
|
21
|
+
# callbacks are recognised by where their procs were written (or by having
|
|
22
|
+
# been dropped as duplicates). After that the module goes by what it
|
|
23
|
+
# registered itself: a mutation can add or remove lines, so a proc created by
|
|
24
|
+
# the mutated source may report a line that belongs to another declaration of
|
|
25
|
+
# the real file.
|
|
26
|
+
module Evilution::Integration::Loading::CallbackRedeclaration
|
|
27
|
+
CONDITIONS = %i[@if @unless].freeze
|
|
28
|
+
private_constant :CONDITIONS
|
|
29
|
+
|
|
30
|
+
# klass: the class whose body the declaration is written in. file, lines:
|
|
31
|
+
# where the declaration is in the file as it was loaded.
|
|
32
|
+
def self.call(klass, file, lines)
|
|
33
|
+
return yield unless klass.respond_to?(:__callbacks)
|
|
34
|
+
|
|
35
|
+
before = klass.__callbacks.transform_values(&:to_a)
|
|
36
|
+
result = yield
|
|
37
|
+
swaps = Settlement.new(klass, Origin.new(file, lines), registered).call(before)
|
|
38
|
+
swap_validators(klass, swaps)
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# What earlier calls registered, by declaration:
|
|
43
|
+
# { [class, file, first line] => callbacks }.
|
|
44
|
+
def self.registered
|
|
45
|
+
@registered ||= {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# `validates` also lists its validator for reflection (`Model.validators`),
|
|
49
|
+
# once per class; the old one is exchanged for the new there too.
|
|
50
|
+
def self.swap_validators(klass, swaps)
|
|
51
|
+
return unless klass.respond_to?(:_validators)
|
|
52
|
+
|
|
53
|
+
pairs = swaps.select { |old, new| old && new }.map { |old, new| [old.filter, new.filter] }
|
|
54
|
+
[klass, *descendants(klass)].each do |target|
|
|
55
|
+
target._validators.each_value do |validators|
|
|
56
|
+
pairs.each { |old, new| exchange(validators, old, new) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.exchange(validators, old, new)
|
|
62
|
+
return unless validators.any? { |validator| validator.equal?(old) }
|
|
63
|
+
|
|
64
|
+
validators.reject! { |validator| validator.equal?(new) }
|
|
65
|
+
validators[validators.index { |validator| validator.equal?(old) }] = new
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.descendants(klass)
|
|
69
|
+
klass.subclasses.flat_map { |subclass| [subclass, *descendants(subclass)] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private_class_method :registered, :swap_validators, :exchange, :descendants
|
|
73
|
+
|
|
74
|
+
# Callbacks are told apart by identity throughout: two that compare equal
|
|
75
|
+
# are still two registrations.
|
|
76
|
+
module Identity
|
|
77
|
+
def self.member?(callbacks, callback)
|
|
78
|
+
callbacks.any? { |candidate| candidate.equal?(callback) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.without(callbacks, others)
|
|
82
|
+
callbacks.reject { |callback| member?(others, callback) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
private_constant :Identity
|
|
86
|
+
|
|
87
|
+
# Rebuilds the chains of a class and its descendants after one declaration
|
|
88
|
+
# has run again.
|
|
89
|
+
class Settlement
|
|
90
|
+
def initialize(klass, origin, registered)
|
|
91
|
+
@klass = klass
|
|
92
|
+
@origin = origin
|
|
93
|
+
@registered = registered
|
|
94
|
+
@key = [klass, origin.file, origin.lines.first]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# before: { chain name => callbacks } as they were. Returns [old, new]
|
|
98
|
+
# callback pairs for what changed hands.
|
|
99
|
+
def call(before)
|
|
100
|
+
previous = @registered[@key]
|
|
101
|
+
mine = []
|
|
102
|
+
swaps = before.flat_map do |name, callbacks|
|
|
103
|
+
stale, fresh = settle(name, callbacks, previous)
|
|
104
|
+
mine.concat(fresh)
|
|
105
|
+
stale.zip(fresh)
|
|
106
|
+
end
|
|
107
|
+
@registered[@key] = mine unless mine.empty?
|
|
108
|
+
swaps
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def settle(name, before, previous)
|
|
114
|
+
after = @klass.__callbacks[name].to_a
|
|
115
|
+
fresh = Identity.without(after, before)
|
|
116
|
+
return [[], []] if fresh.empty?
|
|
117
|
+
|
|
118
|
+
stale = before.select { |callback| stale?(callback, previous) || !Identity.member?(after, callback) }
|
|
119
|
+
order = reorder(before, stale, fresh)
|
|
120
|
+
@klass.send(:__update_callbacks, name) { |target, chain| rebuild(target, name, chain, order, after) }
|
|
121
|
+
[stale, fresh]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# What this declaration registered before: what an earlier call recorded
|
|
125
|
+
# or, the first time, what was written on its lines -- unless an earlier
|
|
126
|
+
# call for another declaration put it there.
|
|
127
|
+
def stale?(callback, previous)
|
|
128
|
+
return Identity.member?(previous, callback) if previous
|
|
129
|
+
|
|
130
|
+
@origin.declared?(callback) && !Identity.member?(@registered.values.flatten, callback)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The new callbacks take the place of the first old one; with no old one
|
|
134
|
+
# to replace they stay where the declaration put them, at the end.
|
|
135
|
+
def reorder(before, stale, fresh)
|
|
136
|
+
return before + fresh if stale.empty?
|
|
137
|
+
|
|
138
|
+
before.flat_map do |callback|
|
|
139
|
+
next fresh if callback.equal?(stale.first)
|
|
140
|
+
|
|
141
|
+
Identity.member?(stale, callback) ? [] : [callback]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def rebuild(target, name, chain, order, after)
|
|
146
|
+
own = Identity.without(chain.to_a, after)
|
|
147
|
+
chain.clear
|
|
148
|
+
(order + own).each { |callback| chain.append(callback) }
|
|
149
|
+
target.send(:set_callbacks, name, chain)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
private_constant :Settlement
|
|
153
|
+
|
|
154
|
+
# Where a declaration is written, and whether a callback came from there.
|
|
155
|
+
class Origin
|
|
156
|
+
attr_reader :file, :lines
|
|
157
|
+
|
|
158
|
+
def initialize(file, lines)
|
|
159
|
+
@file = canonical(file)
|
|
160
|
+
@lines = lines
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def declared?(callback)
|
|
164
|
+
procs_of(callback).any? { |callable| here?(callable.source_location) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def procs_of(callback)
|
|
170
|
+
conditions = CONDITIONS.flat_map { |ivar| Array(callback.instance_variable_get(ivar)) }
|
|
171
|
+
[callback.filter, *conditions].grep(Proc)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def here?(location)
|
|
175
|
+
!location.nil? && canonical(location.first) == @file && @lines.cover?(location.last)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# The same file may have been loaded through a symlinked path.
|
|
179
|
+
def canonical(path)
|
|
180
|
+
File.realpath(path)
|
|
181
|
+
rescue SystemCallError
|
|
182
|
+
File.expand_path(path)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
private_constant :Origin
|
|
186
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loading"
|
|
4
|
+
require_relative "own_methods"
|
|
5
|
+
|
|
6
|
+
# Re-declares one statement of a concern's `included` block on the classes
|
|
7
|
+
# that already include the concern, or of its `prepended` block on those that
|
|
8
|
+
# prepend it.
|
|
9
|
+
#
|
|
10
|
+
# A mutation inside `included do ... scope :published, -> { } ... end` has two
|
|
11
|
+
# audiences. Classes that include the concern later run the whole block, so
|
|
12
|
+
# registering the mutated block is enough for them. Classes that already
|
|
13
|
+
# include it ran the original, and running the whole block on them again
|
|
14
|
+
# would add its validations and callbacks a second time.
|
|
15
|
+
#
|
|
16
|
+
# So the source evilution evaluates guards every other call of the block with
|
|
17
|
+
# `skipping?` and ends the block with `call(self)`: the block is run once more
|
|
18
|
+
# on each existing includer with the guard up, and only the mutated
|
|
19
|
+
# declaration takes effect there. What the block holds besides calls -- a
|
|
20
|
+
# method definition, an assignment -- runs again as well, which is how a
|
|
21
|
+
# mutated method of the block reaches those classes. A method the class has
|
|
22
|
+
# defined itself over one of the block's is kept (OwnMethods). See
|
|
23
|
+
# BodyCallNeutralizer.
|
|
24
|
+
module Evilution::Integration::Loading::ConcernRedeclaration
|
|
25
|
+
KEY = :evilution_concern_redeclaration
|
|
26
|
+
BLOCKS = { included: :@_included_block, prepended: :@_prepended_block }.freeze
|
|
27
|
+
|
|
28
|
+
# Called through UnboundMethods: ObjectSpace hands over every class in the
|
|
29
|
+
# VM, and one that defines `include?` or `superclass` of its own for its
|
|
30
|
+
# own purposes must not be asked that question.
|
|
31
|
+
INCLUDES = Module.instance_method(:include?)
|
|
32
|
+
SUPERCLASS = Class.instance_method(:superclass)
|
|
33
|
+
SINGLETON = Module.instance_method(:singleton_class?)
|
|
34
|
+
ANCESTORS = Module.instance_method(:ancestors)
|
|
35
|
+
private_constant :KEY, :BLOCKS, :INCLUDES, :SUPERCLASS, :SINGLETON, :ANCESTORS
|
|
36
|
+
|
|
37
|
+
def self.skipping?
|
|
38
|
+
Thread.current[KEY] == true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `hook` names the block: `:included` or `:prepended`.
|
|
42
|
+
def self.call(concern, hook = :included)
|
|
43
|
+
ivar = BLOCKS.fetch(hook)
|
|
44
|
+
return unless concern.instance_variable_defined?(ivar)
|
|
45
|
+
|
|
46
|
+
block = concern.instance_variable_get(ivar)
|
|
47
|
+
takers(concern, prepended: hook == :prepended).each { |klass| redeclare(klass, block) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The classes that took the concern themselves, by including it or by
|
|
51
|
+
# prepending it; a subclass inherits what its parent declares.
|
|
52
|
+
def self.takers(concern, prepended:)
|
|
53
|
+
ObjectSpace.each_object(Class).select do |klass|
|
|
54
|
+
next false if SINGLETON.bind_call(klass) || !INCLUDES.bind_call(klass, concern)
|
|
55
|
+
|
|
56
|
+
parent = SUPERCLASS.bind_call(klass)
|
|
57
|
+
inherited = !parent.nil? && INCLUDES.bind_call(parent, concern)
|
|
58
|
+
!inherited && prepended?(klass, concern) == prepended
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A prepended module comes before the class in its ancestors, an included
|
|
63
|
+
# one after it.
|
|
64
|
+
def self.prepended?(klass, concern)
|
|
65
|
+
ancestors = ANCESTORS.bind_call(klass)
|
|
66
|
+
ancestors.index(concern) < ancestors.index(klass)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.redeclare(klass, block)
|
|
70
|
+
previous = Thread.current[KEY]
|
|
71
|
+
Thread.current[KEY] = true
|
|
72
|
+
Evilution::Integration::Loading::OwnMethods.keeping(klass, block.source_location.first) do
|
|
73
|
+
klass.class_eval(&block)
|
|
74
|
+
end
|
|
75
|
+
ensure
|
|
76
|
+
Thread.current[KEY] = previous
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private_class_method :takers, :prepended?, :redeclare
|
|
80
|
+
end
|
|
@@ -13,19 +13,23 @@ class Evilution::Integration::Loading::ConcernStateCleaner
|
|
|
13
13
|
|
|
14
14
|
# ObjectSpace hands us every Module in the VM, including ones that answer no
|
|
15
15
|
# message honestly. ActiveSupport::Deprecation::DeprecatedConstantProxy
|
|
16
|
-
# subclasses Module and undefines all of its instance methods, so
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# for any method_missing-based wrapper, not just ActiveSupport's.
|
|
16
|
+
# subclasses Module and undefines all of its instance methods, so asking it
|
|
17
|
+
# anything lands in its method_missing and emits the deprecation; where the
|
|
18
|
+
# deprecator's behaviour is :raise (grape's spec_helper sets that), the
|
|
19
|
+
# sweep blows up on a module it merely walked past. So the module is never
|
|
20
|
+
# asked: `Concern === mod` is answered by Concern, and the ivars are reached
|
|
21
|
+
# through UnboundMethods, which bypass the object's own method table -- this
|
|
22
|
+
# holds for any method_missing-based wrapper, not just ActiveSupport's.
|
|
23
23
|
# EV-v2rc / GH #1581.
|
|
24
|
-
|
|
24
|
+
#
|
|
25
|
+
# `===` also leaves the module as it found it. Asking for its
|
|
26
|
+
# singleton_class instead would create one for every module in the VM, each
|
|
27
|
+
# of them a module the next sweep has to visit and give a singleton class
|
|
28
|
+
# of its own; a long in-process run doubles its module count per mutation.
|
|
25
29
|
IVAR_DEFINED = Object.instance_method(:instance_variable_defined?)
|
|
26
30
|
IVAR_GET = Object.instance_method(:instance_variable_get)
|
|
27
31
|
REMOVE_IVAR = Object.instance_method(:remove_instance_variable)
|
|
28
|
-
private_constant :
|
|
32
|
+
private_constant :IVAR_DEFINED, :IVAR_GET, :REMOVE_IVAR
|
|
29
33
|
|
|
30
34
|
def initialize(subpath_resolver: Evilution::LoadPath::SubpathResolver.new)
|
|
31
35
|
@subpath_resolver = subpath_resolver
|
|
@@ -34,11 +38,14 @@ class Evilution::Integration::Loading::ConcernStateCleaner
|
|
|
34
38
|
def call(file_path)
|
|
35
39
|
return unless defined?(ActiveSupport::Concern)
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
# Anchored on the project, not Dir.pwd: an isolated worker has chdir'd
|
|
42
|
+
# into a per-mutation sandbox, where a relative path names nothing and no
|
|
43
|
+
# concern would ever match.
|
|
44
|
+
absolute = File.expand_path(file_path, Evilution.project_base_dir)
|
|
45
|
+
subpath = @subpath_resolver.call(absolute)
|
|
39
46
|
|
|
40
47
|
ObjectSpace.each_object(Module) do |mod|
|
|
41
|
-
next unless
|
|
48
|
+
next unless ActiveSupport::Concern === mod # rubocop:disable Style/CaseEquality
|
|
42
49
|
|
|
43
50
|
clear_concern_ivars(mod, absolute, subpath)
|
|
44
51
|
end
|
|
@@ -51,10 +58,10 @@ class Evilution::Integration::Loading::ConcernStateCleaner
|
|
|
51
58
|
next unless IVAR_DEFINED.bind_call(mod, ivar)
|
|
52
59
|
|
|
53
60
|
block = IVAR_GET.bind_call(mod, ivar)
|
|
54
|
-
|
|
55
|
-
next
|
|
61
|
+
location = block.source_location
|
|
62
|
+
next if location.nil?
|
|
56
63
|
|
|
57
|
-
expanded = File.expand_path(
|
|
64
|
+
expanded = File.expand_path(location.first)
|
|
58
65
|
REMOVE_IVAR.bind_call(mod, ivar) if source_matches?(expanded, absolute, subpath)
|
|
59
66
|
end
|
|
60
67
|
end
|