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,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,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
|
|
6
|
+
# The body of a class, module or `class << self`: the code written outside
|
|
7
|
+
# every method, where no method subject reaches it.
|
|
8
|
+
#
|
|
9
|
+
# Operators that mutate such code attribute their mutants to one method, so
|
|
10
|
+
# each mutant has exactly one subject. That method is the scope's anchor: the
|
|
11
|
+
# first method written in the scope, in source order. Blocks of the body
|
|
12
|
+
# (`included do ... end`) are searched, nested scopes are not: their methods
|
|
13
|
+
# anchor the inner scope. A scope with no method of its own borrows the anchor
|
|
14
|
+
# of the first singleton class in its body that has one, so a class whose
|
|
15
|
+
# methods all sit in `class << self` is still reached.
|
|
16
|
+
class Evilution::AST::ClassBody
|
|
17
|
+
SCOPES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
18
|
+
# Nodes whose children belong to something else: a method, or another scope.
|
|
19
|
+
OPAQUE = [Prism::DefNode, *SCOPES].freeze
|
|
20
|
+
|
|
21
|
+
# The location that closes each kind of conditional or loop.
|
|
22
|
+
CLOSINGS = {
|
|
23
|
+
Prism::IfNode => :end_keyword_loc,
|
|
24
|
+
Prism::UnlessNode => :end_keyword_loc,
|
|
25
|
+
Prism::ElseNode => :end_keyword_loc,
|
|
26
|
+
Prism::WhileNode => :closing_loc,
|
|
27
|
+
Prism::UntilNode => :closing_loc
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :node
|
|
31
|
+
|
|
32
|
+
# The bodies anchored at the method starting on the given line, outermost
|
|
33
|
+
# first. A method anchors its own scope, and with it any enclosing scope
|
|
34
|
+
# that borrows from it.
|
|
35
|
+
def self.anchored_at(tree, line)
|
|
36
|
+
scopes_in(tree).map { |scope| new(scope) }.select { |body| body.anchor_line == line }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.scopes_in(node)
|
|
40
|
+
node.compact_child_nodes.flat_map do |child|
|
|
41
|
+
nested = scopes_in(child)
|
|
42
|
+
SCOPES.include?(child.class) ? [child, *nested] : nested
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private_class_method :scopes_in
|
|
47
|
+
|
|
48
|
+
def initialize(node)
|
|
49
|
+
@node = node
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def anchor_line
|
|
53
|
+
first_method = contents.find { |child| child.is_a?(Prism::DefNode) }
|
|
54
|
+
return first_method.location.start_line if first_method
|
|
55
|
+
|
|
56
|
+
singleton_classes = contents.grep(Prism::SingletonClassNode)
|
|
57
|
+
singleton_classes.filter_map { |scope| self.class.new(scope).anchor_line }.first
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The statements of the body the block selects, each one removable: cut
|
|
61
|
+
# out, it leaves code that still parses. Methods and nested scopes are
|
|
62
|
+
# neither offered nor searched, and neither is a statement guarded by a
|
|
63
|
+
# modifier (`include Foo if legacy?`) or an expression used as a value.
|
|
64
|
+
def declarations(&)
|
|
65
|
+
body = @node.body
|
|
66
|
+
return [] unless body
|
|
67
|
+
|
|
68
|
+
statements_in(body, removable: body.is_a?(Prism::StatementsNode)).select(&)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Every node written in the scope, in source order, down to the methods and
|
|
74
|
+
# nested scopes, which are listed but not entered. The scope's own header
|
|
75
|
+
# (its name, its superclass expression) is not part of the body.
|
|
76
|
+
def contents
|
|
77
|
+
@contents ||= @node.body ? descend(@node.body) : []
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def statements_in(node, removable:)
|
|
81
|
+
node.compact_child_nodes.flat_map do |child|
|
|
82
|
+
next [] if OPAQUE.include?(child.class)
|
|
83
|
+
|
|
84
|
+
nested = statements_in(child, removable: child.is_a?(Prism::StatementsNode) && !modifier?(node))
|
|
85
|
+
removable ? [child, *nested] : nested
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A conditional or loop written without its closing keyword: the modifier
|
|
90
|
+
# forms and the ternary, whose statements cannot be cut out.
|
|
91
|
+
def modifier?(node)
|
|
92
|
+
closing = CLOSINGS[node.class]
|
|
93
|
+
!closing.nil? && node.public_send(closing).nil?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def descend(node)
|
|
97
|
+
node.compact_child_nodes.flat_map do |child|
|
|
98
|
+
OPAQUE.include?(child.class) ? [child] : [child, *descend(child)]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
hook_block(node, :included)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The block node of a `prepended do ... end`, the same hook for the classes
|
|
18
|
+
# that prepend the module; nil when node is not one.
|
|
19
|
+
def self.prepended(node)
|
|
20
|
+
hook_block(node, :prepended)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.hook_block(node, name)
|
|
24
|
+
return nil unless node.is_a?(Prism::CallNode) && node.name == name
|
|
25
|
+
return nil unless node.receiver.nil? && node.arguments.nil?
|
|
26
|
+
|
|
27
|
+
node.block if node.block.is_a?(Prism::BlockNode)
|
|
28
|
+
end
|
|
29
|
+
private_class_method :hook_block
|
|
30
|
+
|
|
31
|
+
# The included and prepended blocks among a module body's direct statements.
|
|
32
|
+
def self.hooks_in_body(body)
|
|
33
|
+
direct_statements(body).filter_map { |node| of(node) || prepended(node) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.direct_statements(body)
|
|
37
|
+
statements = body.is_a?(Prism::BeginNode) ? body.statements : body
|
|
38
|
+
statements.is_a?(Prism::StatementsNode) ? statements.body : []
|
|
39
|
+
end
|
|
40
|
+
private_class_method :direct_statements
|
|
41
|
+
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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../ast"
|
|
5
|
+
|
|
6
|
+
# Expressions whose value is never nil whatever the program does: `self` and
|
|
7
|
+
# the literals other than `nil`. Anything read or computed (a variable, a
|
|
8
|
+
# constant, a call) is not one.
|
|
9
|
+
module Evilution::AST::NeverNil
|
|
10
|
+
NODE_TYPES = [
|
|
11
|
+
Prism::SelfNode,
|
|
12
|
+
Prism::StringNode,
|
|
13
|
+
Prism::InterpolatedStringNode,
|
|
14
|
+
Prism::XStringNode,
|
|
15
|
+
Prism::InterpolatedXStringNode,
|
|
16
|
+
Prism::SymbolNode,
|
|
17
|
+
Prism::InterpolatedSymbolNode,
|
|
18
|
+
Prism::IntegerNode,
|
|
19
|
+
Prism::FloatNode,
|
|
20
|
+
Prism::RationalNode,
|
|
21
|
+
Prism::ImaginaryNode,
|
|
22
|
+
Prism::ArrayNode,
|
|
23
|
+
Prism::HashNode,
|
|
24
|
+
Prism::RangeNode,
|
|
25
|
+
Prism::RegularExpressionNode,
|
|
26
|
+
Prism::InterpolatedRegularExpressionNode,
|
|
27
|
+
Prism::TrueNode,
|
|
28
|
+
Prism::FalseNode,
|
|
29
|
+
Prism::LambdaNode
|
|
30
|
+
].freeze
|
|
31
|
+
private_constant :NODE_TYPES
|
|
32
|
+
|
|
33
|
+
def self.node?(node)
|
|
34
|
+
NODE_TYPES.any? { |type| node.is_a?(type) }
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/evilution/ast/parser.rb
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
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"
|
|
5
10
|
|
|
6
11
|
module Evilution::AST
|
|
7
12
|
class Parser
|
|
@@ -40,55 +45,216 @@ module Evilution::AST
|
|
|
40
45
|
@file_path = file_path
|
|
41
46
|
@subjects = []
|
|
42
47
|
@context = []
|
|
48
|
+
@singleton = [false]
|
|
49
|
+
@hooks = []
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
def visit_module_node(node)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
within_context(constant_name(node.constant_path), rooted: rooted?(node.constant_path)) do
|
|
54
|
+
hooks = IncludedBlock.hooks_in_body(node.body)
|
|
55
|
+
within_hooks(hooks) do
|
|
56
|
+
hooks.each { |block| add_declaration_subjects(block.body) }
|
|
57
|
+
within_scope(singleton: false) { super }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
49
60
|
end
|
|
50
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.
|
|
51
65
|
def visit_class_node(node)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
within_context(constant_name(node.constant_path), rooted: rooted?(node.constant_path)) do
|
|
67
|
+
superclass = node.superclass
|
|
68
|
+
add_subject(superclass, @context.join("::"), :constant) if ValueObjectDefinition.match?(superclass)
|
|
69
|
+
add_declaration_subjects(node.body)
|
|
70
|
+
within_scope(singleton: false) { super }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A `def` inside `class << self` defines a method on the enclosing
|
|
75
|
+
# class's singleton, like `def self.name`; inside `class << Store` it
|
|
76
|
+
# defines one on Store.
|
|
77
|
+
def visit_singleton_class_node(node)
|
|
78
|
+
expression = node.expression
|
|
79
|
+
return within_scope(singleton: true) { super } unless constant_expression?(expression)
|
|
80
|
+
|
|
81
|
+
within_context(constant_name(expression), rooted: rooted?(expression)) do
|
|
82
|
+
within_scope(singleton: true) { super }
|
|
83
|
+
end
|
|
55
84
|
end
|
|
56
85
|
|
|
57
86
|
def visit_def_node(node)
|
|
58
|
-
separator = node.receiver ? "." : "#"
|
|
59
|
-
add_subject(node, separator)
|
|
87
|
+
separator = node.receiver || @singleton.last ? "." : "#"
|
|
88
|
+
add_subject(node, "#{@context.join("::")}#{separator}#{node.name}", :method)
|
|
89
|
+
super
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# An assignment to a constant cannot be written inside a method, so it is
|
|
93
|
+
# a subject of its own: the whole assignment, named after the constant. A
|
|
94
|
+
# value-object definition keeps its narrower constant subject instead.
|
|
95
|
+
def visit_constant_write_node(node)
|
|
96
|
+
return within_definition(node.value, node.name.to_s) { super } if ValueObjectDefinition.match?(node.value)
|
|
97
|
+
|
|
98
|
+
add_subject(node, scoped_name(node.name), :constant_write)
|
|
99
|
+
super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def visit_constant_path_write_node(node)
|
|
103
|
+
name = path_name(node.target)
|
|
104
|
+
rooted = rooted?(node.target)
|
|
105
|
+
return within_definition(node.value, name, rooted: rooted) { super } if ValueObjectDefinition.match?(node.value)
|
|
106
|
+
|
|
107
|
+
add_subject(node, rooted ? name : scoped_name(name), :constant_write)
|
|
108
|
+
super
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# `LIMIT += 1` assigns the constant again and is a subject like the
|
|
112
|
+
# assignment it follows, under the same name.
|
|
113
|
+
def visit_constant_operator_write_node(node)
|
|
114
|
+
add_subject(node, scoped_name(node.name), :constant_write)
|
|
115
|
+
super
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def visit_constant_path_operator_write_node(node)
|
|
119
|
+
name = path_name(node.target)
|
|
120
|
+
add_subject(node, rooted?(node.target) ? name : scoped_name(name), :constant_write)
|
|
60
121
|
super
|
|
61
122
|
end
|
|
62
123
|
|
|
63
124
|
private
|
|
64
125
|
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
126
|
+
def within_scope(singleton:)
|
|
127
|
+
@singleton.push(singleton)
|
|
128
|
+
yield
|
|
129
|
+
ensure
|
|
130
|
+
@singleton.pop
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# What the block finds inside one of these hook blocks of a concern is
|
|
134
|
+
# marked as written there.
|
|
135
|
+
def within_hooks(blocks)
|
|
136
|
+
enclosing = @hooks
|
|
137
|
+
@hooks = blocks.map { |block| block.location.start_offset...block.location.end_offset }
|
|
138
|
+
yield
|
|
139
|
+
ensure
|
|
140
|
+
@hooks = enclosing
|
|
141
|
+
end
|
|
72
142
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
143
|
+
def in_hook?(location)
|
|
144
|
+
@hooks.any? { |range| range.cover?(location.start_offset) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def scoped_name(name)
|
|
148
|
+
[*@context, name].join("::")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# `::LIMIT` and `::Shared::LIMIT` name a constant from the top level,
|
|
152
|
+
# whatever scope they are written in.
|
|
153
|
+
def rooted?(path)
|
|
154
|
+
path.slice.start_with?("::")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def constant_expression?(node)
|
|
158
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# A value-object definition assigned to a constant is a subject of its
|
|
162
|
+
# own, and a scope for what its block defines: `def area` in
|
|
163
|
+
# `Size = Struct.new(:w, :h) do ... end` is `Size#area`.
|
|
164
|
+
def within_definition(definition, name, rooted: false)
|
|
165
|
+
within_context(name, rooted: rooted) do
|
|
166
|
+
add_subject(definition, @context.join("::"), :constant)
|
|
167
|
+
yield
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Names what the block finds under `name`: within the scope being
|
|
172
|
+
# visited, or from the top level when the name was written rooted.
|
|
173
|
+
def within_context(name, rooted:)
|
|
174
|
+
enclosing = @context
|
|
175
|
+
@context = rooted ? [name] : [*enclosing, name]
|
|
176
|
+
yield
|
|
177
|
+
ensure
|
|
178
|
+
@context = enclosing
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The callables handed to the declarations of a class body, or of a
|
|
182
|
+
# concern's `included` or `prepended` block.
|
|
183
|
+
def add_declaration_subjects(body)
|
|
184
|
+
add_scope_subjects(body)
|
|
185
|
+
add_aasm_subjects(body)
|
|
186
|
+
add_callback_subjects(body)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# `scope :recent, -> { ... }` defines the class method `recent`. The
|
|
190
|
+
# subject spans the whole declaration and mutates its body.
|
|
191
|
+
# Written in a concern's `included` or `prepended` block it defines that
|
|
192
|
+
# method on every class taking the concern, and is named after the concern.
|
|
193
|
+
def add_scope_subjects(body)
|
|
194
|
+
ScopeDeclaration.in_body(body).each do |declaration|
|
|
195
|
+
name = "#{@context.join("::")}.#{ScopeDeclaration.scope_name(declaration)}"
|
|
196
|
+
add_subject(ScopeDeclaration.body_of(declaration), name, :scope, span: declaration)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# A guard or callback written out inside `aasm do ... end` runs when the
|
|
201
|
+
# method its event or state defines is called (`ship`, `paid?`), and is
|
|
202
|
+
# named after it. The subject spans that declaration and mutates the
|
|
203
|
+
# callable's body; one event may hold several. A machine declared in a
|
|
204
|
+
# concern's `included` block is named after the concern.
|
|
205
|
+
def add_aasm_subjects(body)
|
|
206
|
+
AasmDeclaration.in_body(body).each do |callable|
|
|
207
|
+
name = "#{@context.join("::")}##{callable.method_name}"
|
|
208
|
+
add_subject(callable.body, name, :aasm, span: callable.declaration)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# A condition or a callback written out in a declaration such as
|
|
213
|
+
# `validate :credit_limit, if: -> { ... }` defines no method to be named
|
|
214
|
+
# after, so it is named the way the declaration reads:
|
|
215
|
+
# `Order.validate(:credit_limit)`. The subject spans the declaration and
|
|
216
|
+
# mutates the callable's body. In a concern's `included` block it is named
|
|
217
|
+
# after the concern.
|
|
218
|
+
def add_callback_subjects(body)
|
|
219
|
+
CallbackDeclaration.in_body(body).each do |callable|
|
|
220
|
+
add_subject(callable.body, "#{@context.join("::")}.#{callable.label}", :callback, span: callable.declaration)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def add_subject(node, name, kind, span: node)
|
|
225
|
+
loc = span.location
|
|
226
|
+
source = @source.byteslice(loc.start_offset, loc.end_offset - loc.start_offset)
|
|
227
|
+
.force_encoding(@source.encoding)
|
|
76
228
|
|
|
77
229
|
@subjects << Evilution::Subject.new(
|
|
78
230
|
name: name,
|
|
79
231
|
file_path: @file_path,
|
|
80
232
|
line_number: loc.start_line,
|
|
81
|
-
source:
|
|
82
|
-
node: node
|
|
233
|
+
source: source,
|
|
234
|
+
node: node,
|
|
235
|
+
kind: kind,
|
|
236
|
+
concern_hook: in_hook?(loc)
|
|
83
237
|
)
|
|
84
238
|
end
|
|
85
239
|
|
|
240
|
+
# `self::Unit` has no static full name; it is named within the scope it
|
|
241
|
+
# is written in, like a plain `Unit`.
|
|
242
|
+
def path_name(target)
|
|
243
|
+
target.full_name.delete_prefix("::")
|
|
244
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
|
245
|
+
target.name.to_s
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# `class self::Unit` and `class << self::Unit` have no static full name
|
|
249
|
+
# either; like `path_name`, they are named by their own constant.
|
|
86
250
|
def constant_name(node)
|
|
87
251
|
if node.respond_to?(:full_name)
|
|
88
|
-
node.full_name
|
|
252
|
+
node.full_name.delete_prefix("::")
|
|
89
253
|
else
|
|
90
254
|
node.name.to_s
|
|
91
255
|
end
|
|
256
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
|
257
|
+
node.name.to_s
|
|
92
258
|
end
|
|
93
259
|
end
|
|
94
260
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../pattern"
|
|
4
|
+
|
|
5
|
+
# Reads a method name the way an attribute value spells it: an identifier with
|
|
6
|
+
# an optional `?`, `!` or `=`, a bare operator, or anything else in quotes.
|
|
7
|
+
class Evilution::AST::Pattern::MethodName
|
|
8
|
+
# Longest first, so `<=>` is not read as `<=`. Operators starting with `!`,
|
|
9
|
+
# `*` or `|` already mean negation, a wildcard or an alternative in a
|
|
10
|
+
# pattern, and have to be quoted.
|
|
11
|
+
OPERATORS = %w[<=> === == =~ []= [] <= << < >= >> > +@ -@ + - / % & ^ ~].freeze
|
|
12
|
+
IDENTIFIER = /\G[a-zA-Z_][a-zA-Z0-9_]*[?!=]?/
|
|
13
|
+
QUOTES = %w[' "].freeze
|
|
14
|
+
|
|
15
|
+
# The name starting at +pos+ in +input+, and the position just after it.
|
|
16
|
+
def self.scan(input, pos)
|
|
17
|
+
new(input, pos).scan
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(input, pos)
|
|
21
|
+
@input = input
|
|
22
|
+
@pos = pos
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scan
|
|
26
|
+
char = @input[@pos]
|
|
27
|
+
raise Evilution::ConfigError, "unexpected end of pattern at position #{@pos}" if char.nil?
|
|
28
|
+
|
|
29
|
+
name = QUOTES.include?(char) ? quoted(char) : operator || identifier(char)
|
|
30
|
+
[name, @pos]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def quoted(quote)
|
|
36
|
+
finish = @input.index(quote, @pos + 1)
|
|
37
|
+
raise Evilution::ConfigError, "unterminated quoted name starting at position #{@pos}" if finish.nil?
|
|
38
|
+
|
|
39
|
+
name = @input[(@pos + 1)...finish]
|
|
40
|
+
raise Evilution::ConfigError, "empty quoted name at position #{@pos}" if name.empty?
|
|
41
|
+
|
|
42
|
+
@pos = finish + 1
|
|
43
|
+
name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def operator
|
|
47
|
+
name = OPERATORS.find { |candidate| @input[@pos, candidate.length] == candidate }
|
|
48
|
+
@pos += name.length if name
|
|
49
|
+
name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def identifier(char)
|
|
53
|
+
match = IDENTIFIER.match(@input, @pos)
|
|
54
|
+
if match.nil?
|
|
55
|
+
raise Evilution::ConfigError,
|
|
56
|
+
"invalid name starting with '#{char}' at position #{@pos}; " \
|
|
57
|
+
"quote names that are not identifiers or operators, e.g. name='|'"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@pos = match.end(0)
|
|
61
|
+
match[0]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../pattern"
|
|
4
4
|
require_relative "matcher"
|
|
5
|
+
require_relative "method_name"
|
|
5
6
|
|
|
6
7
|
class Evilution::AST::Pattern::Parser
|
|
7
8
|
def initialize(input)
|
|
@@ -39,17 +40,18 @@ class Evilution::AST::Pattern::Parser
|
|
|
39
40
|
node_type = consume_identifier
|
|
40
41
|
skip_whitespace
|
|
41
42
|
|
|
42
|
-
attributes = {}
|
|
43
|
-
if current_char == "{"
|
|
44
|
-
advance(1)
|
|
45
|
-
attributes = parse_attributes
|
|
46
|
-
skip_whitespace
|
|
47
|
-
expect_char("}")
|
|
48
|
-
end
|
|
49
|
-
|
|
43
|
+
attributes = current_char == "{" ? parse_braced_attributes : {}
|
|
50
44
|
Evilution::AST::Pattern::NodeMatcher.new(node_type, attributes)
|
|
51
45
|
end
|
|
52
46
|
|
|
47
|
+
def parse_braced_attributes
|
|
48
|
+
advance(1)
|
|
49
|
+
attributes = parse_attributes
|
|
50
|
+
skip_whitespace
|
|
51
|
+
expect_char("}")
|
|
52
|
+
attributes
|
|
53
|
+
end
|
|
54
|
+
|
|
53
55
|
def parse_attributes
|
|
54
56
|
attrs = {}
|
|
55
57
|
skip_whitespace
|
|
@@ -109,15 +111,11 @@ class Evilution::AST::Pattern::Parser
|
|
|
109
111
|
end
|
|
110
112
|
|
|
111
113
|
def parse_value_or_nested
|
|
112
|
-
id =
|
|
114
|
+
id, @pos = Evilution::AST::Pattern::MethodName.scan(@input, @pos)
|
|
113
115
|
skip_whitespace
|
|
114
116
|
|
|
115
117
|
if current_char == "{"
|
|
116
|
-
|
|
117
|
-
attrs = parse_attributes
|
|
118
|
-
skip_whitespace
|
|
119
|
-
expect_char("}")
|
|
120
|
-
Evilution::AST::Pattern::NodeMatcher.new(id, attrs)
|
|
118
|
+
Evilution::AST::Pattern::NodeMatcher.new(id, parse_braced_attributes)
|
|
121
119
|
else
|
|
122
120
|
parse_alternatives_from(id)
|
|
123
121
|
end
|
|
@@ -133,7 +131,8 @@ class Evilution::AST::Pattern::Parser
|
|
|
133
131
|
advance(1)
|
|
134
132
|
values << "*"
|
|
135
133
|
else
|
|
136
|
-
|
|
134
|
+
name, @pos = Evilution::AST::Pattern::MethodName.scan(@input, @pos)
|
|
135
|
+
values << name
|
|
137
136
|
end
|
|
138
137
|
end
|
|
139
138
|
|