rubocop-ast 0.5.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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