henitai 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +182 -0
- data/assets/schema/henitai.schema.json +123 -0
- data/exe/henitai +6 -0
- data/lib/henitai/arid_node_filter.rb +97 -0
- data/lib/henitai/cli.rb +341 -0
- data/lib/henitai/configuration.rb +132 -0
- data/lib/henitai/configuration_validator.rb +293 -0
- data/lib/henitai/coverage_bootstrapper.rb +75 -0
- data/lib/henitai/coverage_formatter.rb +112 -0
- data/lib/henitai/equivalence_detector.rb +85 -0
- data/lib/henitai/execution_engine.rb +174 -0
- data/lib/henitai/git_diff_analyzer.rb +82 -0
- data/lib/henitai/integration.rb +417 -0
- data/lib/henitai/mutant/activator.rb +234 -0
- data/lib/henitai/mutant.rb +68 -0
- data/lib/henitai/mutant_generator.rb +158 -0
- data/lib/henitai/mutant_history_store.rb +279 -0
- data/lib/henitai/operator.rb +96 -0
- data/lib/henitai/operators/arithmetic_operator.rb +46 -0
- data/lib/henitai/operators/array_declaration.rb +52 -0
- data/lib/henitai/operators/assignment_expression.rb +78 -0
- data/lib/henitai/operators/block_statement.rb +31 -0
- data/lib/henitai/operators/boolean_literal.rb +70 -0
- data/lib/henitai/operators/conditional_expression.rb +184 -0
- data/lib/henitai/operators/equality_operator.rb +41 -0
- data/lib/henitai/operators/hash_literal.rb +66 -0
- data/lib/henitai/operators/logical_operator.rb +84 -0
- data/lib/henitai/operators/method_expression.rb +56 -0
- data/lib/henitai/operators/pattern_match.rb +66 -0
- data/lib/henitai/operators/range_literal.rb +40 -0
- data/lib/henitai/operators/return_value.rb +105 -0
- data/lib/henitai/operators/safe_navigation.rb +34 -0
- data/lib/henitai/operators/string_literal.rb +64 -0
- data/lib/henitai/operators.rb +25 -0
- data/lib/henitai/parser_current.rb +7 -0
- data/lib/henitai/reporter.rb +432 -0
- data/lib/henitai/result.rb +170 -0
- data/lib/henitai/runner.rb +183 -0
- data/lib/henitai/sampling_strategy.rb +33 -0
- data/lib/henitai/scenario_execution_result.rb +71 -0
- data/lib/henitai/source_parser.rb +41 -0
- data/lib/henitai/static_filter.rb +186 -0
- data/lib/henitai/stillborn_filter.rb +34 -0
- data/lib/henitai/subject.rb +71 -0
- data/lib/henitai/subject_resolver.rb +232 -0
- data/lib/henitai/syntax_validator.rb +16 -0
- data/lib/henitai/test_prioritizer.rb +55 -0
- data/lib/henitai/unparse_helper.rb +24 -0
- data/lib/henitai/version.rb +5 -0
- data/lib/henitai/warning_silencer.rb +16 -0
- data/lib/henitai.rb +51 -0
- data/sig/configuration_validator.rbs +29 -0
- data/sig/henitai.rbs +594 -0
- data/sig/unparser.rbs +3 -0
- metadata +153 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../parser_current"
|
|
4
|
+
require "unparser"
|
|
5
|
+
|
|
6
|
+
module Henitai
|
|
7
|
+
class Mutant
|
|
8
|
+
# Activates a mutant inside the forked child process.
|
|
9
|
+
class Activator
|
|
10
|
+
SERIALIZER_METHODS = {
|
|
11
|
+
arg: :argument_parameter_fragment,
|
|
12
|
+
optarg: :optional_parameter_fragment,
|
|
13
|
+
restarg: :rest_parameter_fragment,
|
|
14
|
+
kwarg: :keyword_parameter_fragment,
|
|
15
|
+
kwoptarg: :optional_keyword_parameter_fragment,
|
|
16
|
+
kwrestarg: :keyword_rest_parameter_fragment,
|
|
17
|
+
blockarg: :block_parameter_fragment,
|
|
18
|
+
forward_arg: :forward_parameter_fragment
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def self.activate!(mutant)
|
|
22
|
+
new.activate!(mutant)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def activate!(mutant)
|
|
26
|
+
subject = mutant.subject
|
|
27
|
+
raise ArgumentError, "Cannot activate wildcard subjects" if subject.method_name.nil?
|
|
28
|
+
|
|
29
|
+
Henitai::WarningSilencer.silence do
|
|
30
|
+
target_for(subject).class_eval(method_source(mutant), __FILE__, __LINE__ + 1)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
rescue Unparser::UnsupportedNodeError
|
|
34
|
+
:compile_error
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def target_for(subject)
|
|
40
|
+
target = load_target(subject)
|
|
41
|
+
subject.method_type == :class ? target.singleton_class : target
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def method_source(mutant)
|
|
45
|
+
method_name = mutant.subject.method_name
|
|
46
|
+
parameters = parameter_source(mutant)
|
|
47
|
+
replacement = body_source(mutant)
|
|
48
|
+
|
|
49
|
+
<<~RUBY
|
|
50
|
+
define_method(:#{method_name}) do |#{parameters}|
|
|
51
|
+
#{replacement}
|
|
52
|
+
end
|
|
53
|
+
RUBY
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def body_source(mutant)
|
|
57
|
+
subject_node = mutant.subject.ast_node
|
|
58
|
+
return compile_safe_unparse(mutant.mutated_node) unless subject_node
|
|
59
|
+
|
|
60
|
+
mutated_subject = replace_node(
|
|
61
|
+
subject_node,
|
|
62
|
+
mutant.original_node,
|
|
63
|
+
mutant.mutated_node
|
|
64
|
+
)
|
|
65
|
+
body = method_body(mutated_subject) || Parser::AST::Node.new(:nil, [])
|
|
66
|
+
compile_safe_unparse(body)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def replace_node(node, original_node, mutated_node)
|
|
70
|
+
return mutated_node if same_node?(node, original_node)
|
|
71
|
+
return node unless node.is_a?(Parser::AST::Node)
|
|
72
|
+
|
|
73
|
+
updated_children = node.children.map do |child|
|
|
74
|
+
replace_child(child, original_node, mutated_node)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return node if updated_children == node.children
|
|
78
|
+
|
|
79
|
+
Parser::AST::Node.new(node.type, updated_children)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def same_node?(left, right)
|
|
83
|
+
left_location = node_location_signature(left)
|
|
84
|
+
right_location = node_location_signature(right)
|
|
85
|
+
return left.equal?(right) unless left_location && right_location
|
|
86
|
+
|
|
87
|
+
left_location == right_location
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def replace_child(child, original_node, mutated_node)
|
|
91
|
+
case child
|
|
92
|
+
when Parser::AST::Node
|
|
93
|
+
replace_node(child, original_node, mutated_node)
|
|
94
|
+
when Array
|
|
95
|
+
child.map { |item| replace_child(item, original_node, mutated_node) }
|
|
96
|
+
else
|
|
97
|
+
child
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def method_body(subject_node)
|
|
102
|
+
case subject_node.type
|
|
103
|
+
when :def
|
|
104
|
+
subject_node.children[2]
|
|
105
|
+
when :defs
|
|
106
|
+
subject_node.children[3]
|
|
107
|
+
when :block
|
|
108
|
+
block_body(subject_node)
|
|
109
|
+
else
|
|
110
|
+
subject_node
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parameter_source(mutant)
|
|
115
|
+
args_node = method_arguments(mutant.subject.ast_node)
|
|
116
|
+
return "" unless args_node
|
|
117
|
+
return forward_parameter_fragment(nil) if args_node.type == :forward_args
|
|
118
|
+
|
|
119
|
+
args_node.children.filter_map do |argument|
|
|
120
|
+
parameter_fragment(argument)
|
|
121
|
+
end.join(", ")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def method_arguments(subject_node)
|
|
125
|
+
case subject_node&.type
|
|
126
|
+
when :def
|
|
127
|
+
subject_node.children[1]
|
|
128
|
+
when :defs
|
|
129
|
+
subject_node.children[2]
|
|
130
|
+
when :block
|
|
131
|
+
block_arguments(subject_node)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def parameter_fragment(argument)
|
|
136
|
+
method_name = SERIALIZER_METHODS[argument&.type]
|
|
137
|
+
return unless method_name
|
|
138
|
+
|
|
139
|
+
send(method_name, argument)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def argument_parameter_fragment(argument)
|
|
143
|
+
argument.children[0].to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def optional_parameter_fragment(argument)
|
|
147
|
+
"#{argument.children[0]} = #{compile_safe_unparse(argument.children[1])}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def rest_parameter_fragment(argument)
|
|
151
|
+
prefixed_parameter(argument, "*")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def keyword_parameter_fragment(argument)
|
|
155
|
+
"#{argument.children[0]}:"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def optional_keyword_parameter_fragment(argument)
|
|
159
|
+
"#{argument.children[0]}: #{compile_safe_unparse(argument.children[1])}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def keyword_rest_parameter_fragment(argument)
|
|
163
|
+
prefixed_parameter(argument, "**")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def block_parameter_fragment(argument)
|
|
167
|
+
"&#{argument.children[0]}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def forward_parameter_fragment(_argument)
|
|
171
|
+
"*args, **kwargs, &block"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def prefixed_parameter(argument, prefix)
|
|
175
|
+
name = argument.children[0]
|
|
176
|
+
name ? "#{prefix}#{name}" : prefix
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def block_body(subject_node)
|
|
180
|
+
subject_node.children[2]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def block_arguments(subject_node)
|
|
184
|
+
subject_node.children[1]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def load_target(subject)
|
|
188
|
+
Object.const_get(subject.namespace.delete_prefix("::"))
|
|
189
|
+
rescue NameError
|
|
190
|
+
load_source_file(subject)
|
|
191
|
+
Object.const_get(subject.namespace.delete_prefix("::"))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def load_source_file(subject)
|
|
195
|
+
source_file = subject.source_file || source_file_from_ast(subject)
|
|
196
|
+
return unless source_file && File.file?(source_file)
|
|
197
|
+
|
|
198
|
+
load(source_file)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def source_file_from_ast(subject)
|
|
202
|
+
ast_node = subject.ast_node
|
|
203
|
+
return unless ast_node
|
|
204
|
+
|
|
205
|
+
location = ast_node.location
|
|
206
|
+
return unless location
|
|
207
|
+
|
|
208
|
+
expression = location.expression
|
|
209
|
+
return unless expression
|
|
210
|
+
|
|
211
|
+
expression.source_buffer.name
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def node_location_signature(node)
|
|
215
|
+
expression = node&.location&.expression
|
|
216
|
+
return unless expression
|
|
217
|
+
|
|
218
|
+
[
|
|
219
|
+
expression.source_buffer.name,
|
|
220
|
+
expression.line,
|
|
221
|
+
expression.column,
|
|
222
|
+
expression.last_line,
|
|
223
|
+
expression.last_column
|
|
224
|
+
]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def compile_safe_unparse(node)
|
|
228
|
+
Unparser.unparse(node)
|
|
229
|
+
rescue StandardError => e
|
|
230
|
+
raise Unparser::UnsupportedNodeError, e.message
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
# Represents a single syntactic mutation applied to a Subject.
|
|
7
|
+
#
|
|
8
|
+
# A Mutant holds:
|
|
9
|
+
# - the original and mutated AST nodes
|
|
10
|
+
# - the operator that generated it
|
|
11
|
+
# - the source location of the mutation
|
|
12
|
+
# - its current status in the pipeline
|
|
13
|
+
#
|
|
14
|
+
# Statuses follow the Stryker mutation-testing-report-schema vocabulary:
|
|
15
|
+
# :pending, :killed, :survived, :timeout, :compile_error, :runtime_error,
|
|
16
|
+
# :ignored, :no_coverage
|
|
17
|
+
class Mutant
|
|
18
|
+
autoload :Activator, "henitai/mutant/activator"
|
|
19
|
+
|
|
20
|
+
# Status-Vokabular folgt dem Stryker mutation-testing-report-schema.
|
|
21
|
+
# :equivalent ist ein Henitai-interner Status (wird im JSON als "Ignored" serialisiert,
|
|
22
|
+
# aber in der Scoring-Berechnung separat behandelt: confirmed equivalent mutants
|
|
23
|
+
# werden aus dem Nenner der MS-Berechnung herausgenommen).
|
|
24
|
+
STATUSES = %i[
|
|
25
|
+
pending
|
|
26
|
+
killed
|
|
27
|
+
survived
|
|
28
|
+
timeout
|
|
29
|
+
compile_error
|
|
30
|
+
runtime_error
|
|
31
|
+
ignored
|
|
32
|
+
no_coverage
|
|
33
|
+
equivalent
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
attr_reader :id, :subject, :operator, :original_node, :mutated_node,
|
|
37
|
+
:mutation_type, :description, :location
|
|
38
|
+
attr_accessor :status, :killing_test, :duration
|
|
39
|
+
|
|
40
|
+
# @param subject [Subject] the subject being mutated
|
|
41
|
+
# @param operator [Symbol] operator name, e.g. :ArithmeticOperator
|
|
42
|
+
# @param nodes [Hash] AST nodes with :original and :mutated entries
|
|
43
|
+
# @param description [String] human-readable description of the mutation
|
|
44
|
+
# @param location [Hash] { file:, start_line:, end_line:, start_col:, end_col: }
|
|
45
|
+
def initialize(subject:, operator:, nodes:, description:, location:)
|
|
46
|
+
@id = SecureRandom.uuid
|
|
47
|
+
@subject = subject
|
|
48
|
+
@operator = operator
|
|
49
|
+
@original_node = nodes.fetch(:original)
|
|
50
|
+
@mutated_node = nodes.fetch(:mutated)
|
|
51
|
+
@description = description
|
|
52
|
+
@location = location
|
|
53
|
+
@status = :pending
|
|
54
|
+
@killing_test = nil
|
|
55
|
+
@duration = nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def killed? = @status == :killed
|
|
59
|
+
def survived? = @status == :survived
|
|
60
|
+
def pending? = @status == :pending
|
|
61
|
+
def ignored? = @status == :ignored
|
|
62
|
+
def equivalent? = @status == :equivalent
|
|
63
|
+
|
|
64
|
+
def to_s
|
|
65
|
+
"#{operator}@#{location[:file]}:#{location[:start_line]} — #{description}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser_current"
|
|
4
|
+
require_relative "source_parser"
|
|
5
|
+
|
|
6
|
+
module Henitai
|
|
7
|
+
# Traverses a subject's AST and asks operators to build mutants.
|
|
8
|
+
class MutantGenerator
|
|
9
|
+
def generate(subjects, operators, config: nil)
|
|
10
|
+
normalized_operators = normalize_operators(operators)
|
|
11
|
+
arid_node_filter = AridNodeFilter.new
|
|
12
|
+
syntax_validator = SyntaxValidator.new
|
|
13
|
+
sampling_strategy = SamplingStrategy.new
|
|
14
|
+
|
|
15
|
+
mutants = Array(subjects).flat_map do |subject|
|
|
16
|
+
generate_for_subject(
|
|
17
|
+
subject,
|
|
18
|
+
normalized_operators,
|
|
19
|
+
config:,
|
|
20
|
+
arid_node_filter:,
|
|
21
|
+
syntax_validator:
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sample_mutants(mutants, config:, sampling_strategy:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def normalize_operators(operators)
|
|
31
|
+
Array(operators).map do |operator|
|
|
32
|
+
operator.is_a?(Class) ? operator.new : operator
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def generate_for_subject(subject, operators, config:, arid_node_filter:, syntax_validator:)
|
|
37
|
+
return [] unless subject.source_file && subject.source_range
|
|
38
|
+
|
|
39
|
+
visitor = SubjectVisitor.new(
|
|
40
|
+
subject,
|
|
41
|
+
operators,
|
|
42
|
+
config:,
|
|
43
|
+
arid_node_filter:,
|
|
44
|
+
syntax_validator:
|
|
45
|
+
)
|
|
46
|
+
visitor.process(SourceParser.parse_file(subject.source_file))
|
|
47
|
+
prune_mutants_per_line(
|
|
48
|
+
visitor.mutants,
|
|
49
|
+
max_mutants_per_line: config&.max_mutants_per_line || 1
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Depth-first pre-order AST visitor for a single subject.
|
|
54
|
+
class SubjectVisitor
|
|
55
|
+
attr_reader :mutants
|
|
56
|
+
|
|
57
|
+
def initialize(subject, operators, config:, arid_node_filter:, syntax_validator:)
|
|
58
|
+
@subject = subject
|
|
59
|
+
@config = config
|
|
60
|
+
@mutants = []
|
|
61
|
+
@arid_node_filter = arid_node_filter
|
|
62
|
+
@syntax_validator = syntax_validator
|
|
63
|
+
@operators_by_node_type = operators.each_with_object(
|
|
64
|
+
Hash.new { |hash, key| hash[key] = [] }
|
|
65
|
+
) do |operator, map|
|
|
66
|
+
operator.class.node_types.each do |node_type|
|
|
67
|
+
map[node_type] << operator
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def process(node)
|
|
73
|
+
walk(node)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def walk(node)
|
|
79
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
80
|
+
|
|
81
|
+
apply_operators(node) if node_within_subject_range?(node)
|
|
82
|
+
node.children.each do |child|
|
|
83
|
+
walk(child)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def apply_operators(node)
|
|
88
|
+
return if @arid_node_filter.suppressed?(node, @config)
|
|
89
|
+
|
|
90
|
+
@operators_by_node_type[node.type].each do |operator|
|
|
91
|
+
operator.mutate(node, subject: @subject).each do |mutant|
|
|
92
|
+
@mutants << mutant if @syntax_validator.valid?(mutant)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def node_within_subject_range?(node)
|
|
98
|
+
location = node.location&.expression
|
|
99
|
+
return true unless location && @subject.source_range
|
|
100
|
+
|
|
101
|
+
node_range = location.line..location.last_line
|
|
102
|
+
ranges_overlap?(node_range, @subject.source_range)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ranges_overlap?(left, right)
|
|
106
|
+
left.begin <= right.end && right.begin <= left.end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def prune_mutants_per_line(mutants, max_mutants_per_line:)
|
|
111
|
+
grouped = mutants.each_with_object({}) do |mutant, selected|
|
|
112
|
+
key = line_key(mutant)
|
|
113
|
+
selected[key] ||= []
|
|
114
|
+
selected[key] << mutant
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
grouped.values.flat_map do |mutants_for_line|
|
|
118
|
+
mutants_for_line.sort_by { |mutant| mutant_priority_key(mutant) }.take(max_mutants_per_line)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def sample_mutants(mutants, config:, sampling_strategy:)
|
|
123
|
+
sampling = config&.sampling
|
|
124
|
+
return mutants unless sampling
|
|
125
|
+
return mutants if sampling[:ratio].nil?
|
|
126
|
+
|
|
127
|
+
sampling_strategy.sample(
|
|
128
|
+
mutants,
|
|
129
|
+
ratio: sampling[:ratio],
|
|
130
|
+
strategy: sampling[:strategy] || :stratified
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def line_key(mutant)
|
|
135
|
+
[
|
|
136
|
+
mutant.location[:file],
|
|
137
|
+
mutant.location[:start_line]
|
|
138
|
+
]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def mutant_priority_key(mutant)
|
|
142
|
+
[
|
|
143
|
+
operator_priority(mutant.operator),
|
|
144
|
+
mutant.location[:start_col] || 0,
|
|
145
|
+
mutant.description
|
|
146
|
+
]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def operator_priority(operator_name)
|
|
150
|
+
operator_priority_map.fetch(operator_name, operator_priority_map.length)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def operator_priority_map
|
|
154
|
+
# The constant order defines signal priority for per-line pruning.
|
|
155
|
+
@operator_priority_map ||= Operator::FULL_SET.each_with_index.to_h
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|