rubocop-ast 0.5.1 → 1.0.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rubocop/ast.rb +17 -0
  3. data/lib/rubocop/ast/builder.rb +1 -0
  4. data/lib/rubocop/ast/node.rb +44 -125
  5. data/lib/rubocop/ast/node/array_node.rb +1 -0
  6. data/lib/rubocop/ast/node/block_node.rb +1 -0
  7. data/lib/rubocop/ast/node/def_node.rb +5 -0
  8. data/lib/rubocop/ast/node/keyword_splat_node.rb +1 -0
  9. data/lib/rubocop/ast/node/mixin/collection_node.rb +1 -0
  10. data/lib/rubocop/ast/node/mixin/descendence.rb +116 -0
  11. data/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +2 -0
  12. data/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +9 -0
  13. data/lib/rubocop/ast/node/mixin/numeric_node.rb +1 -0
  14. data/lib/rubocop/ast/node/mixin/predicate_operator_node.rb +7 -3
  15. data/lib/rubocop/ast/node/pair_node.rb +4 -0
  16. data/lib/rubocop/ast/node/regexp_node.rb +9 -4
  17. data/lib/rubocop/ast/node_pattern.rb +44 -870
  18. data/lib/rubocop/ast/node_pattern/builder.rb +72 -0
  19. data/lib/rubocop/ast/node_pattern/comment.rb +45 -0
  20. data/lib/rubocop/ast/node_pattern/compiler.rb +104 -0
  21. data/lib/rubocop/ast/node_pattern/compiler/atom_subcompiler.rb +56 -0
  22. data/lib/rubocop/ast/node_pattern/compiler/binding.rb +78 -0
  23. data/lib/rubocop/ast/node_pattern/compiler/debug.rb +168 -0
  24. data/lib/rubocop/ast/node_pattern/compiler/node_pattern_subcompiler.rb +146 -0
  25. data/lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb +420 -0
  26. data/lib/rubocop/ast/node_pattern/compiler/subcompiler.rb +57 -0
  27. data/lib/rubocop/ast/node_pattern/lexer.rb +70 -0
  28. data/lib/rubocop/ast/node_pattern/lexer.rex +39 -0
  29. data/lib/rubocop/ast/node_pattern/lexer.rex.rb +182 -0
  30. data/lib/rubocop/ast/node_pattern/method_definer.rb +143 -0
  31. data/lib/rubocop/ast/node_pattern/node.rb +275 -0
  32. data/lib/rubocop/ast/node_pattern/parser.racc.rb +470 -0
  33. data/lib/rubocop/ast/node_pattern/parser.rb +66 -0
  34. data/lib/rubocop/ast/node_pattern/parser.y +103 -0
  35. data/lib/rubocop/ast/node_pattern/sets.rb +37 -0
  36. data/lib/rubocop/ast/node_pattern/with_meta.rb +111 -0
  37. data/lib/rubocop/ast/processed_source.rb +5 -1
  38. data/lib/rubocop/ast/traversal.rb +149 -172
  39. data/lib/rubocop/ast/version.rb +1 -1
  40. metadata +37 -3
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ class NodePattern
6
+ class Compiler
7
+ # Compiles code that evalues to true or false
8
+ # for a given value `var` (typically a RuboCop::AST::Node)
9
+ # or it's `node.type` if `seq_head` is true
10
+ #
11
+ # Doc on how this fits in the compiling process:
12
+ # /doc/modules/ROOT/pages/node_pattern.md
13
+ class NodePatternSubcompiler < Subcompiler
14
+ attr_reader :access, :seq_head
15
+
16
+ def initialize(compiler, var: nil, access: var, seq_head: false)
17
+ super(compiler)
18
+ @var = var
19
+ @access = access
20
+ @seq_head = seq_head
21
+ end
22
+
23
+ private
24
+
25
+ def visit_negation
26
+ expr = compile(node.child)
27
+ "!(#{expr})"
28
+ end
29
+
30
+ def visit_ascend
31
+ compiler.with_temp_variables do |ascend|
32
+ expr = compiler.compile_as_node_pattern(node.child, var: ascend)
33
+ "(#{ascend} = #{access_node}) && (#{ascend} = #{ascend}.parent) && #{expr}"
34
+ end
35
+ end
36
+
37
+ def visit_descend
38
+ compiler.with_temp_variables { |descendant| <<~RUBY.chomp }
39
+ ::RuboCop::AST::NodePattern.descend(#{access}).any? do |#{descendant}|
40
+ #{compiler.compile_as_node_pattern(node.child, var: descendant)}
41
+ end
42
+ RUBY
43
+ end
44
+
45
+ def visit_wildcard
46
+ 'true'
47
+ end
48
+
49
+ def visit_unify
50
+ name = compiler.bind(node.child) do |unify_name|
51
+ # double assign to avoid "assigned but unused variable"
52
+ return "(#{unify_name} = #{access_element}; #{unify_name} = #{unify_name}; true)"
53
+ end
54
+
55
+ compile_value_match(name)
56
+ end
57
+
58
+ def visit_capture
59
+ "(#{compiler.next_capture} = #{access_element}; #{compile(node.child)})"
60
+ end
61
+
62
+ ### Lists
63
+
64
+ def visit_union
65
+ multiple_access(:union) do
66
+ terms = compiler.each_union(node.children)
67
+ .map { |child| compile(child) }
68
+
69
+ "(#{terms.join(' || ')})"
70
+ end
71
+ end
72
+
73
+ def visit_intersection
74
+ multiple_access(:intersection) do
75
+ node.children.map { |child| compile(child) }
76
+ .join(' && ')
77
+ end
78
+ end
79
+
80
+ def visit_predicate
81
+ "#{access_element}.#{node.method_name}#{compile_args(node.arg_list)}"
82
+ end
83
+
84
+ def visit_function_call
85
+ "#{node.method_name}#{compile_args(node.arg_list, first: access_element)}"
86
+ end
87
+
88
+ def visit_node_type
89
+ "#{access_node}.#{node.child.to_s.tr('-', '_')}_type?"
90
+ end
91
+
92
+ def visit_sequence
93
+ multiple_access(:sequence) do |var|
94
+ term = compiler.compile_sequence(node, var: var)
95
+ "#{compile_guard_clause} && #{term}"
96
+ end
97
+ end
98
+
99
+ # Assumes other types are atoms.
100
+ def visit_other_type
101
+ value = compiler.compile_as_atom(node)
102
+ compile_value_match(value)
103
+ end
104
+
105
+ # Compiling helpers
106
+
107
+ def compile_value_match(value)
108
+ "#{value} === #{access_element}"
109
+ end
110
+
111
+ # @param [Array<Node>, nil]
112
+ # @return [String, nil]
113
+ def compile_args(arg_list, first: nil)
114
+ args = arg_list&.map { |arg| compiler.compile_as_atom(arg) }
115
+ args = [first, *args] if first
116
+ "(#{args.join(', ')})" if args
117
+ end
118
+
119
+ def access_element
120
+ seq_head ? "#{access}.type" : access
121
+ end
122
+
123
+ def access_node
124
+ return access if seq_head
125
+
126
+ "#{compile_guard_clause} && #{access}"
127
+ end
128
+
129
+ def compile_guard_clause
130
+ "#{access}.is_a?(::RuboCop::AST::Node)"
131
+ end
132
+
133
+ def multiple_access(kind)
134
+ return yield @var if @var
135
+
136
+ compiler.with_temp_variables(kind) do |var|
137
+ memo = "#{var} = #{access}"
138
+ @var = @access = var
139
+ "(#{memo}; #{yield @var})"
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,420 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ class NodePattern
6
+ class Compiler
7
+ # Compiles terms within a sequence to code that evalues to true or false.
8
+ # Compilation of the nodes that can match only a single term is deferred to
9
+ # `NodePatternSubcompiler`; only nodes that can match multiple terms are
10
+ # compiled here.
11
+ # Assumes the given `var` is a `::RuboCop::AST::Node`
12
+ #
13
+ # Doc on how this fits in the compiling process:
14
+ # /doc/modules/ROOT/pages/node_pattern.md
15
+ #
16
+ # rubocop:disable Metrics/ClassLength
17
+ class SequenceSubcompiler < Subcompiler
18
+ DELTA = 1
19
+ POSITIVE = :positive?.to_proc
20
+ private_constant :POSITIVE
21
+
22
+ # Calls `compile_sequence`; the actual `compile` method
23
+ # will be used for the different terms of the sequence.
24
+ # The only case of re-entrant call to `compile` is `visit_capture`
25
+ def initialize(compiler, sequence:, var:)
26
+ @seq = sequence # The node to be compiled
27
+ @seq_var = var # Holds the name of the variable holding the AST::Node we are matching
28
+ super(compiler)
29
+ end
30
+
31
+ def compile_sequence
32
+ # rubocop:disable Layout/CommentIndentation
33
+ compiler.with_temp_variables do |cur_child, cur_index, previous_index|
34
+ @cur_child_var = cur_child # To hold the current child node
35
+ @cur_index_var = cur_index # To hold the current child index (always >= 0)
36
+ @prev_index_var = previous_index # To hold the child index before we enter the
37
+ # variadic nodes
38
+ @cur_index = :seq_head # Can be any of:
39
+ # :seq_head : when the current child is actually the
40
+ # sequence head
41
+ # :variadic_mode : child index held by @cur_index_var
42
+ # >= 0 : when the current child index is known
43
+ # (from the begining)
44
+ # < 0 : when the index is known from the end,
45
+ # where -1 is *past the end*,
46
+ # -2 is the last child, etc...
47
+ # This shift of 1 from standard Ruby indices
48
+ # is stored in DELTA
49
+ @in_sync = false # `true` iff `@cur_child_var` and `@cur_index_var`
50
+ # correspond to `@cur_index`
51
+ # Must be true if `@cur_index` is `:variadic_mode`
52
+ compile_terms
53
+ end
54
+ # rubocop:enable Layout/CommentIndentation
55
+ end
56
+
57
+ private
58
+
59
+ private :compile # Not meant to be called from outside
60
+
61
+ # Single node patterns are all handled here
62
+ def visit_other_type
63
+ access = case @cur_index
64
+ when :seq_head
65
+ { var: @seq_var,
66
+ seq_head: true }
67
+ when :variadic_mode
68
+ { var: @cur_child_var }
69
+ else
70
+ idx = @cur_index + (@cur_index.negative? ? DELTA : 0)
71
+ { access: "#{@seq_var}.children[#{idx}]" }
72
+ end
73
+
74
+ term = compiler.compile_as_node_pattern(node, **access)
75
+ compile_and_advance(term)
76
+ end
77
+
78
+ def visit_repetition
79
+ within_loop do
80
+ child_captures = node.child.nb_captures
81
+ child_code = compile(node.child)
82
+ next compile_loop(child_code) if child_captures.zero?
83
+
84
+ compile_captured_repetition(child_code, child_captures)
85
+ end
86
+ end
87
+
88
+ def visit_any_order
89
+ within_loop do
90
+ compiler.with_temp_variables do |matched|
91
+ case_terms = compile_any_order_branches(matched)
92
+ else_code, init = compile_any_order_else
93
+ term = "#{compile_case(case_terms, else_code)} && #{compile_loop_advance}"
94
+
95
+ all_matched_check = "&&\n#{matched}.size == #{node.term_nodes.size}" if node.rest_node
96
+ <<~RUBY
97
+ (#{init}#{matched} = {}; true) &&
98
+ #{compile_loop(term)} #{all_matched_check} \\
99
+ RUBY
100
+ end
101
+ end
102
+ end
103
+
104
+ def visit_union
105
+ return visit_other_type if node.arity == 1
106
+
107
+ # The way we implement complex unions is by "forking", i.e.
108
+ # making a copy of the present subcompiler to compile each branch
109
+ # of the union.
110
+ # We then use the resulting state of the subcompilers to
111
+ # reset ourselves.
112
+ forks = compile_union_forks
113
+ preserve_union_start(forks)
114
+ merge_forks!(forks)
115
+ expr = forks.values.join(" || \n")
116
+ "(#{expr})"
117
+ end
118
+
119
+ def compile_case(when_branches, else_code)
120
+ <<~RUBY
121
+ case
122
+ #{when_branches.join(' ')}
123
+ else #{else_code}
124
+ end \\
125
+ RUBY
126
+ end
127
+
128
+ def compile_any_order_branches(matched_var)
129
+ node.term_nodes.map.with_index do |node, i|
130
+ code = compiler.compile_as_node_pattern(node, var: @cur_child_var, seq_head: false)
131
+ var = "#{matched_var}[#{i}]"
132
+ "when !#{var} && #{code} then #{var} = true"
133
+ end
134
+ end
135
+
136
+ # @return [Array<String>] Else code, and init code (if any)
137
+ def compile_any_order_else
138
+ rest = node.rest_node
139
+ if !rest
140
+ 'false'
141
+ elsif rest.capture?
142
+ capture_rest = compiler.next_capture
143
+ init = "#{capture_rest} = [];"
144
+ ["#{capture_rest} << #{@cur_child_var}", init]
145
+ else
146
+ 'true'
147
+ end
148
+ end
149
+
150
+ def visit_capture
151
+ return visit_other_type if node.child.arity == 1
152
+
153
+ storage = compiler.next_capture
154
+ term = compile(node.child)
155
+ capture = "#{@seq_var}.children[#{compile_matched(:range)}]"
156
+ "#{term} && (#{storage} = #{capture})"
157
+ end
158
+
159
+ def visit_rest
160
+ empty_loop
161
+ end
162
+
163
+ # Compilation helpers
164
+
165
+ def compile_and_advance(term)
166
+ case @cur_index
167
+ when :variadic_mode
168
+ "#{term} && #{compile_loop_advance}"
169
+ when :seq_head
170
+ # @in_sync = false # already the case
171
+ @cur_index = 0
172
+ term
173
+ else
174
+ @in_sync = false
175
+ @cur_index += 1
176
+ term
177
+ end
178
+ end
179
+
180
+ def compile_captured_repetition(child_code, child_captures)
181
+ captured_range = "#{compiler.captures - child_captures}...#{compiler.captures}"
182
+ captured = "captures[#{captured_range}]"
183
+ compiler.with_temp_variables do |accumulate|
184
+ code = "#{child_code} && #{accumulate}.push(#{captured})"
185
+ <<~RUBY
186
+ (#{accumulate} = Array.new) &&
187
+ #{compile_loop(code)} &&
188
+ (#{captured} = if #{accumulate}.empty?
189
+ (#{captured_range}).map{[]} # Transpose hack won't work for empty case
190
+ else
191
+ #{accumulate}.transpose
192
+ end) \\
193
+ RUBY
194
+ end
195
+ end
196
+
197
+ # Assumes `@cur_index` is already updated
198
+ def compile_matched(kind)
199
+ to = compile_cur_index
200
+ from = if @prev_index == :variadic_mode
201
+ @prev_index_used = true
202
+ @prev_index_var
203
+ else
204
+ compile_index(@prev_index)
205
+ end
206
+ case kind
207
+ when :range
208
+ "#{from}...#{to}"
209
+ when :length
210
+ "#{to} - #{from}"
211
+ end
212
+ end
213
+
214
+ def handle_prev
215
+ @prev_index = @cur_index
216
+ @prev_index_used = false
217
+ code = yield
218
+ if @prev_index_used
219
+ @prev_index_used = false
220
+ code = "(#{@prev_index_var} = #{@cur_index_var}; true) && #{code}"
221
+ end
222
+
223
+ code
224
+ end
225
+
226
+ def compile_terms(children = @seq.children, last_arity = 0..0)
227
+ arities = remaining_arities(children, last_arity)
228
+ total_arity = arities.shift
229
+ guard = compile_child_nb_guard(total_arity)
230
+ return guard if children.empty?
231
+
232
+ @remaining_arity = total_arity
233
+ terms = children.map do |child|
234
+ use_index_from_end
235
+ @remaining_arity = arities.shift
236
+ handle_prev { compile(child) }
237
+ end
238
+ [guard, terms].join(" &&\n")
239
+ end
240
+
241
+ # yield `sync_code` iff not already in sync
242
+ def sync
243
+ return if @in_sync
244
+
245
+ code = compile_loop_advance("= #{compile_cur_index}")
246
+ @in_sync = true
247
+ yield code
248
+ end
249
+
250
+ # @api private
251
+ attr_reader :in_sync, :cur_index
252
+
253
+ public :in_sync
254
+ protected :cur_index, :compile_terms, :sync
255
+
256
+ # @return [Array<Range>] total arities (as Ranges) of remaining children nodes
257
+ # E.g. For sequence `(_ _? <_ _>)`, arities are: 1, 0..1, 2
258
+ # and remaining arities are: 3..4, 2..3, 2..2, 0..0
259
+ def remaining_arities(children, last_arity)
260
+ last = last_arity
261
+ arities = children
262
+ .reverse
263
+ .map(&:arity_range)
264
+ .map { |r| last = last.begin + r.begin..last.max + r.max }
265
+ .reverse!
266
+ arities.push last_arity
267
+ end
268
+
269
+ # @return [String] code that evaluates to `false` if the matched arity is too small
270
+ def compile_min_check
271
+ return 'false' unless node.variadic?
272
+
273
+ unless @remaining_arity.end.infinite?
274
+ not_too_much_remaining = "#{compile_remaining} <= #{@remaining_arity.max}"
275
+ end
276
+ min_to_match = node.arity_range.begin
277
+ if min_to_match.positive?
278
+ enough_matched = "#{compile_matched(:length)} >= #{min_to_match}"
279
+ end
280
+ return 'true' unless not_too_much_remaining || enough_matched
281
+
282
+ [not_too_much_remaining, enough_matched].compact.join(' && ')
283
+ end
284
+
285
+ def compile_remaining
286
+ offset = case @cur_index
287
+ when :seq_head
288
+ ' + 1'
289
+ when :variadic_mode
290
+ " - #{@cur_index_var}"
291
+ when 0
292
+ ''
293
+ when POSITIVE
294
+ " - #{@cur_index}"
295
+ else
296
+ # odd compiling condition, result may not be expected
297
+ # E.g: `(... {a | b c})` => the b c branch can never match
298
+ return - (@cur_index + DELTA)
299
+ end
300
+
301
+ "#{@seq_var}.children.size #{offset}"
302
+ end
303
+
304
+ def compile_max_matched
305
+ return node.arity unless node.variadic?
306
+
307
+ min_remaining_children = "#{compile_remaining} - #{@remaining_arity.begin}"
308
+ return min_remaining_children if node.arity.end.infinite?
309
+
310
+ "[#{min_remaining_children}, #{node.arity.max}].min"
311
+ end
312
+
313
+ def empty_loop
314
+ @cur_index = -@remaining_arity.begin - DELTA
315
+ @in_sync = false
316
+ 'true'
317
+ end
318
+
319
+ def compile_cur_index
320
+ return @cur_index_var if @in_sync
321
+
322
+ compile_index
323
+ end
324
+
325
+ def compile_index(cur = @cur_index)
326
+ return cur if cur >= 0
327
+
328
+ "#{@seq_var}.children.size - #{-(cur + DELTA)}"
329
+ end
330
+
331
+ # Note: assumes `@cur_index != :seq_head`. Node types using `within_loop` must
332
+ # have `def in_sequence_head; :raise; end`
333
+ def within_loop
334
+ sync do |sync_code|
335
+ @cur_index = :variadic_mode
336
+ "#{sync_code} && #{yield}"
337
+ end || yield
338
+ end
339
+
340
+ # returns truthy iff `@cur_index` switched to relative from end mode (i.e. < 0)
341
+ def use_index_from_end
342
+ return if @cur_index == :seq_head || @remaining_arity.begin != @remaining_arity.max
343
+
344
+ @cur_index = -@remaining_arity.begin - DELTA
345
+ end
346
+
347
+ def compile_loop_advance(to = '+=1')
348
+ # The `#{@cur_child_var} ||` is just to avoid unused variable warning
349
+ "(#{@cur_child_var} = #{@seq_var}.children[#{@cur_index_var} #{to}]; " \
350
+ "#{@cur_child_var} || true)"
351
+ end
352
+
353
+ def compile_loop(term)
354
+ <<~RUBY
355
+ (#{compile_max_matched}).times do
356
+ break #{compile_min_check} unless #{term}
357
+ end \\
358
+ RUBY
359
+ end
360
+
361
+ def compile_child_nb_guard(arity_range)
362
+ case arity_range.max
363
+ when Float::INFINITY
364
+ "#{compile_remaining} >= #{arity_range.begin}"
365
+ when arity_range.begin
366
+ "#{compile_remaining} == #{arity_range.begin}"
367
+ else
368
+ "(#{arity_range.begin}..#{arity_range.max}).cover?(#{compile_remaining})"
369
+ end
370
+ end
371
+
372
+ # @return [Hash] of {subcompiler => code}
373
+ def compile_union_forks
374
+ compiler.each_union(node.children).map do |child|
375
+ subsequence_terms = child.is_a?(Node::Subsequence) ? child.children : [child]
376
+ fork = dup
377
+ code = fork.compile_terms(subsequence_terms, @remaining_arity)
378
+ @in_sync = false if @cur_index != :variadic_mode
379
+ [fork, code]
380
+ end.to_h # we could avoid map if RUBY_VERSION >= 2.6...
381
+ end
382
+
383
+ # Modifies in place `forks` to insure that `cur_{child|index}_var` are ok
384
+ def preserve_union_start(forks)
385
+ return if @cur_index != :variadic_mode || forks.size <= 1
386
+
387
+ compiler.with_temp_variables do |union_reset|
388
+ cur = "(#{union_reset} = [#{@cur_child_var}, #{@cur_index_var}]) && "
389
+ reset = "(#{@cur_child_var}, #{@cur_index_var} = #{union_reset}) && "
390
+ forks.transform_values! do |code|
391
+ code = "#{cur}#{code}"
392
+ cur = reset
393
+ code
394
+ end
395
+ end
396
+ end
397
+
398
+ # Modifies in place `forks`
399
+ # Syncs our state
400
+ def merge_forks!(forks)
401
+ sub_compilers = forks.keys
402
+ if !node.variadic? # e.g {a b | c d}
403
+ @cur_index = sub_compilers.first.cur_index # all cur_index should be equivalent
404
+ elsif use_index_from_end
405
+ # nothing to do
406
+ else
407
+ # can't use index from end, so we must sync all forks
408
+ @cur_index = :variadic_mode
409
+ forks.each do |sub, code|
410
+ sub.sync { |sync_code| forks[sub] = "#{code} && #{sync_code}" }
411
+ end
412
+ end
413
+ @in_sync = sub_compilers.all?(&:in_sync)
414
+ end
415
+ end
416
+ # rubocop:enable Metrics/ClassLength
417
+ end
418
+ end
419
+ end
420
+ end