evilution 1.2.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 +100 -0
- data/.rubocop_todo.yml +14 -0
- data/CHANGELOG.md +124 -0
- data/README.md +142 -28
- data/docs/architecture.md +123 -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 +120 -16
- data/lib/evilution/ast/pattern/filter.rb +1 -0
- data/lib/evilution/ast/pattern/method_name.rb +63 -0
- data/lib/evilution/ast/pattern/parser.rb +15 -15
- data/lib/evilution/ast/pattern.rb +2 -0
- data/lib/evilution/ast/regexp_pattern.rb +104 -0
- data/lib/evilution/ast/scope_declaration.rb +45 -0
- data/lib/evilution/ast/sorbet_sig_detector.rb +1 -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 +101 -42
- data/lib/evilution/child_output.rb +1 -1
- data/lib/evilution/cli/command.rb +1 -0
- data/lib/evilution/cli/commands/tests_list.rb +4 -3
- data/lib/evilution/cli/commands.rb +2 -0
- data/lib/evilution/cli/dispatcher.rb +2 -0
- data/lib/evilution/cli/exit_guard.rb +2 -0
- data/lib/evilution/cli/parsed_args.rb +2 -0
- data/lib/evilution/cli/parser/command_extractor.rb +2 -0
- data/lib/evilution/cli/parser/file_args.rb +4 -1
- data/lib/evilution/cli/parser/options_builder.rb +4 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +1 -0
- data/lib/evilution/cli/parser.rb +1 -0
- data/lib/evilution/cli/printers/tests_list.rb +5 -4
- data/lib/evilution/cli/printers.rb +2 -0
- data/lib/evilution/cli/result.rb +2 -0
- data/lib/evilution/cli.rb +8 -2
- data/lib/evilution/config/builders.rb +2 -0
- data/lib/evilution/config/env_loader.rb +2 -0
- data/lib/evilution/config/file_loader.rb +1 -0
- data/lib/evilution/config/sources.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_cache.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_fallback.rb +1 -0
- data/lib/evilution/config/validators/example_targeting_strategy.rb +1 -0
- data/lib/evilution/config/validators/fail_fast.rb +1 -0
- data/lib/evilution/config/validators/hooks.rb +1 -0
- data/lib/evilution/config/validators/ignore_patterns.rb +1 -0
- data/lib/evilution/config/validators/integration.rb +1 -0
- data/lib/evilution/config/validators/isolation.rb +1 -0
- data/lib/evilution/config/validators/jobs.rb +1 -0
- data/lib/evilution/config/validators/preload.rb +1 -0
- data/lib/evilution/config/validators/profile.rb +1 -0
- data/lib/evilution/config/validators/spec_mappings.rb +3 -1
- data/lib/evilution/config/validators/spec_pattern.rb +1 -0
- data/lib/evilution/config/validators/warmup.rb +17 -0
- data/lib/evilution/config/validators.rb +2 -0
- data/lib/evilution/config.rb +7 -5
- data/lib/evilution/coverage.rb +1 -1
- data/lib/evilution/coverage_example_filter.rb +1 -1
- data/lib/evilution/diagnostic.rb +1 -1
- 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 +46 -1
- 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 +139 -12
- 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/loading/test_load_path.rb +21 -13
- 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/configuration_state.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/configuration_streams.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/internals.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +1 -2
- data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +1 -0
- data/lib/evilution/integration/rspec/state_guard.rb +10 -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/actions/environment.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/feedback.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/statuses.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +1 -0
- data/lib/evilution/mcp/info_tool/actions/tests.rb +1 -0
- data/lib/evilution/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 +8 -3
- data/lib/evilution/mutator/base.rb +49 -5
- data/lib/evilution/mutator/operator/alias_removal.rb +126 -0
- data/lib/evilution/mutator/operator/argument_list_removal.rb +52 -0
- data/lib/evilution/mutator/operator/argument_order_permutation.rb +101 -0
- data/lib/evilution/mutator/operator/argument_propagation.rb +54 -0
- data/lib/evilution/mutator/operator/argument_removal.rb +9 -1
- data/lib/evilution/mutator/operator/array_coercion_to_literal.rb +34 -0
- data/lib/evilution/mutator/operator/attribute_write_to_read.rb +34 -0
- data/lib/evilution/mutator/operator/bang_method.rb +11 -3
- data/lib/evilution/mutator/operator/binary_operand_promotion.rb +63 -0
- data/lib/evilution/mutator/operator/block_body_promotion.rb +45 -0
- data/lib/evilution/mutator/operator/block_body_to_nil.rb +52 -0
- data/lib/evilution/mutator/operator/block_body_to_raise.rb +34 -0
- data/lib/evilution/mutator/operator/call_to_nil.rb +34 -0
- data/lib/evilution/mutator/operator/coercion_emptying.rb +60 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +19 -9
- data/lib/evilution/mutator/operator/comparison_operand_swap.rb +54 -0
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -12
- data/lib/evilution/mutator/operator/const_get_to_constant_path.rb +50 -0
- data/lib/evilution/mutator/operator/data_struct_member.rb +82 -0
- data/lib/evilution/mutator/operator/dig_to_fetch_chain.rb +45 -0
- data/lib/evilution/mutator/operator/double_negation_removal.rb +22 -0
- data/lib/evilution/mutator/operator/dynamic_dispatch_resolution.rb +79 -0
- data/lib/evilution/mutator/operator/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/inequality_to_negated_identity.rb +43 -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_argument_removal.rb +47 -0
- 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/proc_to_lambda.rb +43 -0
- data/lib/evilution/mutator/operator/receiver_constructor_swap.rb +51 -0
- data/lib/evilution/mutator/operator/reduce_to_sum.rb +58 -0
- data/lib/evilution/mutator/operator/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_anchor_to_predicate.rb +136 -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/safe_navigation_removal.rb +67 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +44 -6
- data/lib/evilution/mutator/operator/splat_operator.rb +38 -13
- data/lib/evilution/mutator/operator/statement_reorder.rb +153 -0
- data/lib/evilution/mutator/operator/symbol_to_proc_replacement.rb +59 -0
- data/lib/evilution/mutator/operator/to_i_to_integer.rb +38 -0
- data/lib/evilution/mutator/primitives.rb +25 -0
- data/lib/evilution/mutator/registry.rb +54 -2
- data/lib/evilution/mutator/rescue_handlers.rb +81 -0
- data/lib/evilution/parallel/pool.rb +1 -0
- data/lib/evilution/parallel_db_warning.rb +1 -1
- data/lib/evilution/process_supervisor.rb +3 -2
- data/lib/evilution/rails_warmup.rb +61 -0
- 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/html/report.rb +1 -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/coverage_gap_grouper.rb +1 -0
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/subject_scorer.rb +1 -0
- data/lib/evilution/result/summary.rb +42 -2
- data/lib/evilution/runner/baseline_runner.rb +26 -11
- data/lib/evilution/runner/canary.rb +2 -57
- data/lib/evilution/runner/canary_failure_message.rb +101 -0
- data/lib/evilution/runner/isolation_resolver.rb +23 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +35 -14
- 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/source_ast_cache.rb +1 -1
- data/lib/evilution/spec_ast_cache.rb +1 -1
- data/lib/evilution/subject.rb +8 -2
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +51 -0
- data/script/memory_check +62 -16
- metadata +108 -4
data/docs/ast_pattern_syntax.md
CHANGED
|
@@ -66,6 +66,29 @@ after calling `.to_s` on both sides:
|
|
|
66
66
|
call{name=log} # matches when node.name.to_s == "log"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
**Method names** that end in `?`, `!` or `=` are written as they are, and so are
|
|
70
|
+
operator methods:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
call{name=valid?} # record.valid?
|
|
74
|
+
call{name=strip_sources!} # strip_sources!
|
|
75
|
+
call{name=value=} # record.value = 1
|
|
76
|
+
call{name=<=>} # a <=> b
|
|
77
|
+
call{name=-@} # -a (unary minus)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The bare operators are `<=>`, `===`, `==`, `=~`, `[]=`, `[]`, `<=`, `<<`, `<`,
|
|
81
|
+
`>=`, `>>`, `>`, `+@`, `-@`, `+`, `-`, `/`, `%`, `&`, `^` and `~`. Any other
|
|
82
|
+
name — `|`, `!`, `!=`, `!~`, `*`, `**`, a backtick — goes in single or double
|
|
83
|
+
quotes, since unquoted these characters mean alternation, negation or a wildcard.
|
|
84
|
+
A quoted name is matched literally:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
call{name='|'} # a | b, not an alternation
|
|
88
|
+
call{name='*'} # a * b, not a wildcard
|
|
89
|
+
call{name='!='} # a != b, not a negation
|
|
90
|
+
```
|
|
91
|
+
|
|
69
92
|
**Alternatives** use `|` (OR logic) within a single attribute value:
|
|
70
93
|
|
|
71
94
|
```
|
|
@@ -139,6 +162,13 @@ ignore_patterns:
|
|
|
139
162
|
|
|
140
163
|
# Suppress mutations on constant `ENV` reads
|
|
141
164
|
- "call{receiver=constant_read{name=ENV}}"
|
|
165
|
+
|
|
166
|
+
# Suppress mutations on bang and predicate calls, and on `<=>`
|
|
167
|
+
- "call{name=strip_sources!|eager_load!|valid?}"
|
|
168
|
+
- "call{name=<=>}"
|
|
169
|
+
|
|
170
|
+
# Quote names made of reserved characters
|
|
171
|
+
- "call{name='|'}"
|
|
142
172
|
```
|
|
143
173
|
|
|
144
174
|
## Pattern Matching Semantics
|
|
@@ -164,12 +194,21 @@ value = "!" value
|
|
|
164
194
|
| "*"
|
|
165
195
|
nested_pattern = node_type "{" attributes "}"
|
|
166
196
|
alternatives = atom { "|" atom }
|
|
167
|
-
atom =
|
|
197
|
+
atom = name | "*"
|
|
198
|
+
name = identifier [ "?" | "!" | "=" ]
|
|
199
|
+
| operator
|
|
200
|
+
| "'" { any character except "'" } "'"
|
|
201
|
+
| '"' { any character except '"' } '"'
|
|
202
|
+
operator = "<=>" | "===" | "==" | "=~" | "[]=" | "[]" | "<=" | "<<" | "<"
|
|
203
|
+
| ">=" | ">>" | ">" | "+@" | "-@" | "+" | "-" | "/" | "%"
|
|
204
|
+
| "&" | "^" | "~"
|
|
168
205
|
identifier = [a-zA-Z_] [a-zA-Z0-9_]*
|
|
169
206
|
```
|
|
170
207
|
|
|
171
|
-
A bare `
|
|
172
|
-
An `identifier` followed by `{` is parsed as a nested pattern.
|
|
208
|
+
A bare `name` without `{` is parsed as a scalar value (matched via `.to_s`).
|
|
209
|
+
An `identifier` followed by `{` is parsed as a nested pattern. Operators are
|
|
210
|
+
read longest first, so `<=>` is never taken for `<=`. Node types and attribute
|
|
211
|
+
names are always plain identifiers. A quoted name is at least one character long.
|
|
173
212
|
|
|
174
213
|
## Examples
|
|
175
214
|
|
|
@@ -183,6 +222,9 @@ An `identifier` followed by `{` is parsed as a nested pattern.
|
|
|
183
222
|
| `call{receiver=**}` | `foo()`, `x.foo()`, `a.b.foo()` | *(matches all)* |
|
|
184
223
|
| `def{name=to_s}` | `def to_s; ... end` | `def to_str; ... end` |
|
|
185
224
|
| `call{name=!log}` | `debug()`, `info()` | `log()` |
|
|
225
|
+
| `call{name=valid?\|save!}` | `x.valid?`, `save!` | `valid`, `save` |
|
|
226
|
+
| `call{name=<=>}` | `a <=> b` | `a <= b` |
|
|
227
|
+
| `call{name='\|'}` | `a \| b` | `a & b` |
|
|
186
228
|
|
|
187
229
|
## Design Decisions
|
|
188
230
|
|
|
@@ -191,8 +233,11 @@ An `identifier` followed by `{` is parsed as a nested pattern.
|
|
|
191
233
|
write patterns immediately.
|
|
192
234
|
|
|
193
235
|
2. **Unquoted values**: Attribute values don't require quotes. This keeps YAML clean
|
|
194
|
-
and avoids escaping issues.
|
|
195
|
-
|
|
236
|
+
and avoids escaping issues. Method names ending in `?`, `!` or `=` and operators
|
|
237
|
+
that cannot be confused with pattern syntax are written bare too. Only names
|
|
238
|
+
built from reserved characters (`|`, `!`, `*`) or characters outside a method
|
|
239
|
+
name need quotes; there is no escaping inside them. In YAML, use one kind of
|
|
240
|
+
quote for the YAML string and the other inside it: `- "call{name='|'}"`.
|
|
196
241
|
|
|
197
242
|
3. **Implicit wildcards for unspecified attributes**: `call{name=log}` matches
|
|
198
243
|
regardless of receiver, arguments, etc. Only specified attributes constrain the
|
data/docs/isolation.md
CHANGED
|
@@ -22,8 +22,8 @@ transaction block with a half-delivered exception. The mask is
|
|
|
22
22
|
[load-bearing for transaction correctness][rails-handle-interrupt].
|
|
23
23
|
|
|
24
24
|
That mask interacts badly with `Timeout.timeout`. The Timeout gem schedules a
|
|
25
|
-
timer thread which fires `Thread#raise
|
|
26
|
-
|
|
25
|
+
timer thread which fires `Thread#raise` at the main thread when the deadline
|
|
26
|
+
hits. Ruby receives the `raise`, inspects the interrupt mask, sees
|
|
27
27
|
`Exception => :never`, and **queues the exception for later delivery** — "later"
|
|
28
28
|
meaning "when the masked block exits". If the mutant is stuck in an infinite
|
|
29
29
|
loop inside the transaction, the block never exits, the queued exception is
|
|
@@ -57,6 +57,26 @@ The same hazard applies to any Ruby code that uses
|
|
|
57
57
|
blocks that wrap "must complete" sections. If your target code can touch any
|
|
58
58
|
of those, prefer `--isolation fork`.
|
|
59
59
|
|
|
60
|
+
## How `in_process` ends a run that is out of time
|
|
61
|
+
|
|
62
|
+
The timeout raises `Evilution::Isolation::InProcess::Expired` into the run. It
|
|
63
|
+
is a `SignalException`, not a `Timeout::Error`, because it has to end the
|
|
64
|
+
whole test run. A test framework rescues nearly everything a test raises:
|
|
65
|
+
handed a `Timeout::Error`, it records one failed test and moves on to the
|
|
66
|
+
next, which may reach the same endless loop with no timer left to stop it.
|
|
67
|
+
Minitest, Test::Unit and RSpec all let a `SignalException` through, so the run
|
|
68
|
+
stops at once and the mutation is reported as timed out rather than killed.
|
|
69
|
+
|
|
70
|
+
It is not an `Interrupt` either. Tooling that handles Ctrl-C — maxitest, for
|
|
71
|
+
one — rescues `Interrupt` and skips every test from then on, which would turn
|
|
72
|
+
each later mutation in the process into a survivor.
|
|
73
|
+
|
|
74
|
+
One raise is all there is. Code under test, or a test, that rescues
|
|
75
|
+
`Exception` or `SignalException` and then loops again cannot be stopped from
|
|
76
|
+
inside its own process; use `--isolation fork` for it. The same goes for a
|
|
77
|
+
mutant that crashes the Ruby VM: under `fork` that costs one child, under
|
|
78
|
+
`in_process` the whole run.
|
|
79
|
+
|
|
60
80
|
## Parent-process preload
|
|
61
81
|
|
|
62
82
|
Fork isolation's one downside is that every child pays the cost of loading
|
|
@@ -132,6 +152,23 @@ the MCP server is a long-lived process that handles runs from different
|
|
|
132
152
|
projects — preloading one project's Rails stack into a shared process would
|
|
133
153
|
poison subsequent runs.
|
|
134
154
|
|
|
155
|
+
## Test files under `in_process` (Minitest, Test::Unit)
|
|
156
|
+
|
|
157
|
+
Under `in_process` one process runs every mutation, and Minitest and
|
|
158
|
+
Test::Unit learn of a test class only when it is first defined: loading its
|
|
159
|
+
file a second time reopens the class and registers nothing. Each test file is
|
|
160
|
+
therefore loaded once per process, and the classes it registered are
|
|
161
|
+
dispatched again for every later mutation
|
|
162
|
+
(`Integration::Loading::TestClassCache`).
|
|
163
|
+
|
|
164
|
+
The consequence is that code in the body of a test file runs once. Tests
|
|
165
|
+
generated at load time from the code under test (`OPS.each { |op|
|
|
166
|
+
define_method("test_#{op}") { … } }`) keep the set built on the first load,
|
|
167
|
+
and state held on a test class carries over from one mutation to the next.
|
|
168
|
+
Under `fork` every child loads the file itself, so use `--isolation fork` for
|
|
169
|
+
a suite that depends on either. RSpec is unaffected: it is reset and its spec
|
|
170
|
+
files are loaded again for each mutation.
|
|
171
|
+
|
|
135
172
|
## Sandboxed working directory
|
|
136
173
|
|
|
137
174
|
Every isolator runs `test_command.call` inside a per-mutation scratch
|
|
@@ -142,7 +142,7 @@ Key-by-key:
|
|
|
142
142
|
| `matcher.ignore` | `ignore_patterns` / `# evilution:disable` | AST patterns or inline comments |
|
|
143
143
|
| `fail_fast` | `fail_fast` | integer N or `null` |
|
|
144
144
|
| `--score` gate (CLI; default `1.0`) | `min_score` | evilution defaults to `0.0` — set your own gate |
|
|
145
|
-
| _(n/a)_ | `profile: strict` | opt into aggressive truthiness
|
|
145
|
+
| _(n/a)_ | `profile: strict` | opt into aggressive mutators (truthiness, swallowed errors, statement order) |
|
|
146
146
|
|
|
147
147
|
Run `evilution init` for a fully commented template, and point your editor at
|
|
148
148
|
`schema/evilution.config.schema.json` for autocomplete/validation.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
require_relative "literal_callable"
|
|
6
|
+
|
|
7
|
+
# The guards and callbacks of an AASM state machine that are written out as
|
|
8
|
+
# literal callables:
|
|
9
|
+
#
|
|
10
|
+
# aasm do
|
|
11
|
+
# state :paid, before_exit: -> { ... }
|
|
12
|
+
# event :ship, guard: -> { ... } do
|
|
13
|
+
# before { ... }
|
|
14
|
+
# transitions from: :paid, to: :shipped, guard: -> { ... }
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Each belongs to the `event` or `state` declaration it is written in. AASM
|
|
19
|
+
# keeps what a declaration holds by value, so running that one declaration
|
|
20
|
+
# again replaces it, and a mutated body takes effect. The machine's own
|
|
21
|
+
# callbacks (`after_all_transitions`) accumulate instead, and are left out.
|
|
22
|
+
#
|
|
23
|
+
# Only a receiver-less `aasm` call with a block, written directly in a class
|
|
24
|
+
# body, counts: that is the call the mutated file re-runs.
|
|
25
|
+
module Evilution::AST::AasmDeclaration
|
|
26
|
+
# body: the node holding the callable's body. declaration: its `event` or
|
|
27
|
+
# `state` call. method_name: the instance method that declaration defines.
|
|
28
|
+
Callable = Data.define(:body, :declaration, :method_name)
|
|
29
|
+
|
|
30
|
+
DECLARATIONS = %i[event state].freeze
|
|
31
|
+
EVENT_CALLBACKS = %i[
|
|
32
|
+
before before_transaction before_success success after after_transaction after_commit error ensure
|
|
33
|
+
].freeze
|
|
34
|
+
private_constant :DECLARATIONS, :EVENT_CALLBACKS
|
|
35
|
+
|
|
36
|
+
# The callables of every machine among a class body's direct statements.
|
|
37
|
+
def self.in_body(body)
|
|
38
|
+
statements_of(body).flat_map { |node| machine_callables(node) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The block of an `aasm` call, or nil when node is not one.
|
|
42
|
+
def self.machine_block(node)
|
|
43
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == :aasm && node.receiver.nil?
|
|
44
|
+
|
|
45
|
+
node.block if node.block.is_a?(Prism::BlockNode)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The name specs call for whatever sits on a line of a machine: the event,
|
|
49
|
+
# or the first state, declared there. nil outside every declaration.
|
|
50
|
+
def self.token_at(machine, line)
|
|
51
|
+
declaration = declarations_of(machine).find do |node|
|
|
52
|
+
line.between?(node.location.start_line, node.location.end_line)
|
|
53
|
+
end
|
|
54
|
+
declaration && declared_name(declaration)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.machine_callables(machine)
|
|
58
|
+
declarations_of(machine).flat_map do |declaration|
|
|
59
|
+
bodies_of(declaration).map do |body|
|
|
60
|
+
Callable.new(body: body, declaration: declaration, method_name: method_name(declaration))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.declarations_of(machine)
|
|
66
|
+
block = machine_block(machine)
|
|
67
|
+
return [] unless block
|
|
68
|
+
|
|
69
|
+
statements_of(block.body).select { |node| declaration?(node) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.declaration?(node)
|
|
73
|
+
bare_call?(node) && DECLARATIONS.include?(node.name) && arguments_of(node).first.is_a?(Prism::SymbolNode)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.declared_name(declaration)
|
|
77
|
+
arguments_of(declaration).first.unescaped
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# `event :ship` defines `ship`; `state :paid` defines `paid?`.
|
|
81
|
+
def self.method_name(declaration)
|
|
82
|
+
declaration.name == :event ? declared_name(declaration) : "#{declared_name(declaration)}?"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.bodies_of(declaration)
|
|
86
|
+
keyword_bodies(declaration) + (declaration.name == :event ? event_block_bodies(declaration) : [])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# What an event's block holds: its transitions, with keyword callables of
|
|
90
|
+
# their own, and callbacks given as blocks (`before { }`).
|
|
91
|
+
def self.event_block_bodies(event)
|
|
92
|
+
return [] unless event.block.is_a?(Prism::BlockNode)
|
|
93
|
+
|
|
94
|
+
statements_of(event.block.body).select { |node| bare_call?(node) }.flat_map do |node|
|
|
95
|
+
if node.name == :transitions then keyword_bodies(node)
|
|
96
|
+
elsif callback_block?(node) then [node.block]
|
|
97
|
+
else []
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.callback_block?(node)
|
|
103
|
+
EVENT_CALLBACKS.include?(node.name) && node.arguments.nil? && node.block.is_a?(Prism::BlockNode)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# `guard: -> { }` and `guards: [-> { }, :named]` alike.
|
|
107
|
+
def self.keyword_bodies(call)
|
|
108
|
+
keywords = arguments_of(call).last
|
|
109
|
+
return [] unless keywords.is_a?(Prism::KeywordHashNode)
|
|
110
|
+
|
|
111
|
+
keywords.elements.grep(Prism::AssocNode).flat_map do |pair|
|
|
112
|
+
values = pair.value.is_a?(Prism::ArrayNode) ? pair.value.elements : [pair.value]
|
|
113
|
+
values.filter_map { |value| Evilution::AST::LiteralCallable.body_of(value) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.bare_call?(node)
|
|
118
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.arguments_of(call)
|
|
122
|
+
call.arguments ? call.arguments.arguments : []
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.statements_of(body)
|
|
126
|
+
body = body.statements if body.is_a?(Prism::BeginNode)
|
|
127
|
+
body.is_a?(Prism::StatementsNode) ? body.body : []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private_class_method :machine_callables, :declarations_of, :declaration?, :declared_name, :method_name,
|
|
131
|
+
:bodies_of, :event_block_bodies, :callback_block?, :keyword_bodies, :bare_call?,
|
|
132
|
+
:arguments_of, :statements_of
|
|
133
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
require_relative "literal_callable"
|
|
6
|
+
|
|
7
|
+
# The callables written out in a callback or validation declaration of a
|
|
8
|
+
# class body:
|
|
9
|
+
#
|
|
10
|
+
# validate :credit_limit, if: -> { paid? && total.positive? }
|
|
11
|
+
# validates :title, presence: true, unless: [-> { draft? }, :imported?]
|
|
12
|
+
# before_save { self.slug = title.parameterize }
|
|
13
|
+
# after_commit -> { notify }, on: :create
|
|
14
|
+
#
|
|
15
|
+
# That is the condition given to `if:` / `unless:`, and the callback itself
|
|
16
|
+
# when it is a block or a lambda. A condition or callback named by a symbol
|
|
17
|
+
# is a method, and a subject of its own already.
|
|
18
|
+
#
|
|
19
|
+
# Declarations are recognised by name -- `validate`, `validates`,
|
|
20
|
+
# `validates_*`, `before_*`, `after_*`, `around_*` -- and only receiver-less,
|
|
21
|
+
# directly in a class body: that is the call the mutated file re-runs.
|
|
22
|
+
module Evilution::AST::CallbackDeclaration
|
|
23
|
+
# body: the node holding the callable's body. declaration: the call it is
|
|
24
|
+
# written in. label: how the declaration reads, for naming the subject.
|
|
25
|
+
Callable = Data.define(:body, :declaration, :label)
|
|
26
|
+
|
|
27
|
+
NAME = /\A(?:validate|validates|validates_\w+|(?:before|after|around)_\w+)\z/
|
|
28
|
+
CONDITIONS = %w[if unless].freeze
|
|
29
|
+
private_constant :NAME, :CONDITIONS
|
|
30
|
+
|
|
31
|
+
# The callables of every callback declaration among a class body's direct
|
|
32
|
+
# statements.
|
|
33
|
+
def self.in_body(body)
|
|
34
|
+
statements = body.is_a?(Prism::BeginNode) ? body.statements : body
|
|
35
|
+
return [] unless statements.is_a?(Prism::StatementsNode)
|
|
36
|
+
|
|
37
|
+
statements.body.select { |node| match?(node) }.flat_map { |declaration| callables(declaration) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Whether node is a callback declaration that can be run again on its own.
|
|
41
|
+
# One holding a heredoc cannot be wrapped on its own lines, and is left out.
|
|
42
|
+
def self.match?(node)
|
|
43
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && NAME.match?(node.name.to_s) && !heredoc?(node)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.callables(declaration)
|
|
47
|
+
label = label_of(declaration)
|
|
48
|
+
bodies_of(declaration).map { |body| Callable.new(body: body, declaration: declaration, label: label) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# `validate :credit_limit, if: ...` reads as `validate(:credit_limit)`; a
|
|
52
|
+
# declaration naming nothing, as its own name.
|
|
53
|
+
def self.label_of(declaration)
|
|
54
|
+
named = arguments_of(declaration).find { |argument| argument.is_a?(Prism::SymbolNode) }
|
|
55
|
+
named ? "#{declaration.name}(:#{named.unescaped})" : declaration.name.to_s
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.bodies_of(declaration)
|
|
59
|
+
arguments = arguments_of(declaration)
|
|
60
|
+
bodies = arguments.filter_map { |argument| Evilution::AST::LiteralCallable.body_of(argument) }
|
|
61
|
+
bodies.concat(condition_bodies(arguments.last))
|
|
62
|
+
bodies << declaration.block if declaration.block.is_a?(Prism::BlockNode)
|
|
63
|
+
bodies
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# `if: -> { }` and `unless: [-> { }, :named]` alike.
|
|
67
|
+
def self.condition_bodies(keywords)
|
|
68
|
+
return [] unless keywords.is_a?(Prism::KeywordHashNode)
|
|
69
|
+
|
|
70
|
+
keywords.elements.select { |pair| condition?(pair) }.flat_map do |pair|
|
|
71
|
+
values = pair.value.is_a?(Prism::ArrayNode) ? pair.value.elements : [pair.value]
|
|
72
|
+
values.filter_map { |value| Evilution::AST::LiteralCallable.body_of(value) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.condition?(pair)
|
|
77
|
+
pair.is_a?(Prism::AssocNode) && pair.key.is_a?(Prism::SymbolNode) && CONDITIONS.include?(pair.key.unescaped)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.arguments_of(call)
|
|
81
|
+
call.arguments ? call.arguments.arguments : []
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.heredoc?(node)
|
|
85
|
+
return true if node.respond_to?(:heredoc?) && node.heredoc?
|
|
86
|
+
|
|
87
|
+
node.compact_child_nodes.any? { |child| heredoc?(child) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_class_method :callables, :label_of, :bodies_of, :condition_bodies, :condition?, :arguments_of, :heredoc?
|
|
91
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
|
|
6
|
+
# The block of an `ActiveSupport::Concern`'s `included do ... end`: code
|
|
7
|
+
# written in a module body that runs in the body of each class including the
|
|
8
|
+
# module. Only the receiver-less, argument-less form with a literal block
|
|
9
|
+
# counts -- `def self.included(base)` and `included(base) { }` are plain Ruby
|
|
10
|
+
# hooks with rules of their own.
|
|
11
|
+
module Evilution::AST::IncludedBlock
|
|
12
|
+
# The block node, or nil when node is not an `included` block call.
|
|
13
|
+
def self.of(node)
|
|
14
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == :included
|
|
15
|
+
return nil unless node.receiver.nil? && node.arguments.nil?
|
|
16
|
+
|
|
17
|
+
node.block if node.block.is_a?(Prism::BlockNode)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The included blocks among a module body's direct statements.
|
|
21
|
+
def self.in_body(body)
|
|
22
|
+
statements = body.is_a?(Prism::BeginNode) ? body.statements : body
|
|
23
|
+
return [] unless statements.is_a?(Prism::StatementsNode)
|
|
24
|
+
|
|
25
|
+
statements.body.filter_map { |node| of(node) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
|
|
6
|
+
# A callable written out where it is used: `-> { }`, `lambda { }` or
|
|
7
|
+
# `proc { }`. Its body is what mutations go into. A constant, a variable or
|
|
8
|
+
# `method(:name)` in the same place has no body of its own to mutate.
|
|
9
|
+
module Evilution::AST::LiteralCallable
|
|
10
|
+
BLOCK_CALLS = %i[lambda proc].freeze
|
|
11
|
+
private_constant :BLOCK_CALLS
|
|
12
|
+
|
|
13
|
+
# The node holding the callable's body -- a LambdaNode, or the BlockNode of
|
|
14
|
+
# a `lambda` / `proc` call -- or nil when node is not a literal callable.
|
|
15
|
+
def self.body_of(node)
|
|
16
|
+
return node if node.is_a?(Prism::LambdaNode)
|
|
17
|
+
return nil unless node.is_a?(Prism::CallNode) && node.receiver.nil? && BLOCK_CALLS.include?(node.name)
|
|
18
|
+
|
|
19
|
+
node.block if node.block.is_a?(Prism::BlockNode)
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/evilution/ast/parser.rb
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
require_relative "value_object_definition"
|
|
6
|
+
require_relative "scope_declaration"
|
|
7
|
+
require_relative "aasm_declaration"
|
|
8
|
+
require_relative "included_block"
|
|
9
|
+
require_relative "callback_declaration"
|
|
4
10
|
|
|
5
11
|
module Evilution::AST
|
|
6
12
|
class Parser
|
|
@@ -39,55 +45,153 @@ module Evilution::AST
|
|
|
39
45
|
@file_path = file_path
|
|
40
46
|
@subjects = []
|
|
41
47
|
@context = []
|
|
48
|
+
@singleton = [false]
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
def visit_module_node(node)
|
|
45
52
|
@context.push(constant_name(node.constant_path))
|
|
46
|
-
|
|
53
|
+
IncludedBlock.in_body(node.body).each do |block|
|
|
54
|
+
add_scope_subjects(block.body)
|
|
55
|
+
add_aasm_subjects(block.body)
|
|
56
|
+
add_callback_subjects(block.body)
|
|
57
|
+
end
|
|
58
|
+
within_scope(singleton: false) { super }
|
|
47
59
|
@context.pop
|
|
48
60
|
end
|
|
49
61
|
|
|
62
|
+
# A class whose superclass is a value-object definition
|
|
63
|
+
# (`class Coord < Data.define(:lat, :lng)`) makes a constant subject of
|
|
64
|
+
# that definition, named after the class.
|
|
50
65
|
def visit_class_node(node)
|
|
51
66
|
@context.push(constant_name(node.constant_path))
|
|
52
|
-
|
|
67
|
+
superclass = node.superclass
|
|
68
|
+
add_subject(superclass, @context.join("::"), :constant) if ValueObjectDefinition.match?(superclass)
|
|
69
|
+
add_scope_subjects(node.body)
|
|
70
|
+
add_aasm_subjects(node.body)
|
|
71
|
+
add_callback_subjects(node.body)
|
|
72
|
+
within_scope(singleton: false) { super }
|
|
53
73
|
@context.pop
|
|
54
74
|
end
|
|
55
75
|
|
|
76
|
+
# A `def` inside `class << self` defines a method on the enclosing
|
|
77
|
+
# class's singleton, like `def self.name`; inside `class << Store` it
|
|
78
|
+
# defines one on Store.
|
|
79
|
+
def visit_singleton_class_node(node)
|
|
80
|
+
constant = constant_expression?(node.expression)
|
|
81
|
+
@context.push(constant_name(node.expression)) if constant
|
|
82
|
+
within_scope(singleton: true) { super }
|
|
83
|
+
@context.pop if constant
|
|
84
|
+
end
|
|
85
|
+
|
|
56
86
|
def visit_def_node(node)
|
|
57
|
-
separator = node.receiver ? "." : "#"
|
|
58
|
-
add_subject(node, separator)
|
|
87
|
+
separator = node.receiver || @singleton.last ? "." : "#"
|
|
88
|
+
add_subject(node, "#{@context.join("::")}#{separator}#{node.name}", :method)
|
|
59
89
|
super
|
|
60
90
|
end
|
|
61
91
|
|
|
92
|
+
def visit_constant_write_node(node)
|
|
93
|
+
return super unless ValueObjectDefinition.match?(node.value)
|
|
94
|
+
|
|
95
|
+
within_definition(node.value, node.name.to_s) { super }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def visit_constant_path_write_node(node)
|
|
99
|
+
return super unless ValueObjectDefinition.match?(node.value)
|
|
100
|
+
|
|
101
|
+
within_definition(node.value, path_name(node.target)) { super }
|
|
102
|
+
end
|
|
103
|
+
|
|
62
104
|
private
|
|
63
105
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
def within_scope(singleton:)
|
|
107
|
+
@singleton.push(singleton)
|
|
108
|
+
yield
|
|
109
|
+
ensure
|
|
110
|
+
@singleton.pop
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def constant_expression?(node)
|
|
114
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A value-object definition assigned to a constant is a subject of its
|
|
118
|
+
# own, and a scope for what its block defines: `def area` in
|
|
119
|
+
# `Size = Struct.new(:w, :h) do ... end` is `Size#area`.
|
|
120
|
+
def within_definition(definition, name)
|
|
121
|
+
@context.push(name)
|
|
122
|
+
add_subject(definition, @context.join("::"), :constant)
|
|
123
|
+
yield
|
|
124
|
+
@context.pop
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# `scope :recent, -> { ... }` defines the class method `recent`. The
|
|
128
|
+
# subject spans the whole declaration and mutates its body.
|
|
129
|
+
# Written in a concern's `included` block it defines that method on every
|
|
130
|
+
# class including the concern, and is named after the concern.
|
|
131
|
+
def add_scope_subjects(body)
|
|
132
|
+
ScopeDeclaration.in_body(body).each do |declaration|
|
|
133
|
+
name = "#{@context.join("::")}.#{ScopeDeclaration.scope_name(declaration)}"
|
|
134
|
+
add_subject(ScopeDeclaration.body_of(declaration), name, :scope, span: declaration)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# A guard or callback written out inside `aasm do ... end` runs when the
|
|
139
|
+
# method its event or state defines is called (`ship`, `paid?`), and is
|
|
140
|
+
# named after it. The subject spans that declaration and mutates the
|
|
141
|
+
# callable's body; one event may hold several. A machine declared in a
|
|
142
|
+
# concern's `included` block is named after the concern.
|
|
143
|
+
def add_aasm_subjects(body)
|
|
144
|
+
AasmDeclaration.in_body(body).each do |callable|
|
|
145
|
+
name = "#{@context.join("::")}##{callable.method_name}"
|
|
146
|
+
add_subject(callable.body, name, :aasm, span: callable.declaration)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
71
149
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
150
|
+
# A condition or a callback written out in a declaration such as
|
|
151
|
+
# `validate :credit_limit, if: -> { ... }` defines no method to be named
|
|
152
|
+
# after, so it is named the way the declaration reads:
|
|
153
|
+
# `Order.validate(:credit_limit)`. The subject spans the declaration and
|
|
154
|
+
# mutates the callable's body. In a concern's `included` block it is named
|
|
155
|
+
# after the concern.
|
|
156
|
+
def add_callback_subjects(body)
|
|
157
|
+
CallbackDeclaration.in_body(body).each do |callable|
|
|
158
|
+
add_subject(callable.body, "#{@context.join("::")}.#{callable.label}", :callback, span: callable.declaration)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def add_subject(node, name, kind, span: node)
|
|
163
|
+
loc = span.location
|
|
164
|
+
source = @source.byteslice(loc.start_offset, loc.end_offset - loc.start_offset)
|
|
165
|
+
.force_encoding(@source.encoding)
|
|
75
166
|
|
|
76
167
|
@subjects << Evilution::Subject.new(
|
|
77
168
|
name: name,
|
|
78
169
|
file_path: @file_path,
|
|
79
170
|
line_number: loc.start_line,
|
|
80
|
-
source:
|
|
81
|
-
node: node
|
|
171
|
+
source: source,
|
|
172
|
+
node: node,
|
|
173
|
+
kind: kind
|
|
82
174
|
)
|
|
83
175
|
end
|
|
84
176
|
|
|
177
|
+
# `self::Unit` has no static full name; it is named within the scope it
|
|
178
|
+
# is written in, like a plain `Unit`.
|
|
179
|
+
def path_name(target)
|
|
180
|
+
target.full_name.delete_prefix("::")
|
|
181
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
|
182
|
+
target.name.to_s
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# `class self::Unit` and `class << self::Unit` have no static full name
|
|
186
|
+
# either; like `path_name`, they are named by their own constant.
|
|
85
187
|
def constant_name(node)
|
|
86
188
|
if node.respond_to?(:full_name)
|
|
87
189
|
node.full_name
|
|
88
190
|
else
|
|
89
191
|
node.name.to_s
|
|
90
192
|
end
|
|
193
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
|
194
|
+
node.name.to_s
|
|
91
195
|
end
|
|
92
196
|
end
|
|
93
197
|
end
|