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
@@ -5,6 +5,7 @@ module RuboCop
5
5
  # Common functionality for primitive numeric nodes: `int`, `float`, ...
6
6
  module NumericNode
7
7
  SIGN_REGEX = /\A[+-]/.freeze
8
+ private_constant :SIGN_REGEX
8
9
 
9
10
  # Checks whether this is literal has a sign.
10
11
  #
@@ -5,10 +5,14 @@ module RuboCop
5
5
  # Common functionality for nodes that are predicates:
6
6
  # `or`, `and` ...
7
7
  module PredicateOperatorNode
8
- LOGICAL_AND = '&&'
8
+ LOGICAL_AND = '&&'
9
+ private_constant :LOGICAL_AND
9
10
  SEMANTIC_AND = 'and'
10
- LOGICAL_OR = '||'
11
- SEMANTIC_OR = 'or'
11
+ private_constant :SEMANTIC_AND
12
+ LOGICAL_OR = '||'
13
+ private_constant :LOGICAL_OR
14
+ SEMANTIC_OR = 'or'
15
+ private_constant :SEMANTIC_OR
12
16
 
13
17
  # Returns the operator as a string.
14
18
  #
@@ -9,9 +9,13 @@ module RuboCop
9
9
  include HashElementNode
10
10
 
11
11
  HASH_ROCKET = '=>'
12
+ private_constant :HASH_ROCKET
12
13
  SPACED_HASH_ROCKET = ' => '
14
+ private_constant :SPACED_HASH_ROCKET
13
15
  COLON = ':'
16
+ private_constant :COLON
14
17
  SPACED_COLON = ': '
18
+ private_constant :SPACED_COLON
15
19
 
16
20
  # Checks whether the `pair` uses a hash rocket delimiter.
17
21
  #
@@ -13,13 +13,11 @@ module RuboCop
13
13
  n: Regexp::NOENCODING,
14
14
  o: 0
15
15
  }.freeze
16
+ private_constant :OPTIONS
16
17
 
17
- # Note: The 'o' option is ignored.
18
- #
19
18
  # @return [Regexp] a regexp of this node
20
19
  def to_regexp
21
- option = regopt.children.map { |opt| OPTIONS.fetch(opt) }.inject(:|)
22
- Regexp.new(content, option)
20
+ Regexp.new(content, options)
23
21
  end
24
22
 
25
23
  # @return [RuboCop::AST::Node] a regopt node
@@ -27,6 +25,13 @@ module RuboCop
27
25
  children.last
28
26
  end
29
27
 
28
+ # Note: The 'o' option is ignored.
29
+ #
30
+ # @return [Integer] the Regexp option bits as returned by Regexp#options
31
+ def options
32
+ regopt.children.map { |opt| OPTIONS.fetch(opt) }.inject(0, :|)
33
+ end
34
+
30
35
  # @return [String] a string of regexp content
31
36
  def content
32
37
  children.select(&:str_type?).map(&:str_content).join
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'delegate'
4
- require 'erb'
5
4
 
6
- # rubocop:disable Metrics/ClassLength, Metrics/CyclomaticComplexity
7
5
  module RuboCop
8
6
  module AST
9
7
  # This class performs a pattern-matching operation on an AST node.
10
8
  #
9
+ # Detailed syntax: /doc/modules/ROOT/pages/node_pattern.md
10
+ #
11
11
  # Initialize a new `NodePattern` with `NodePattern.new(pattern_string)`, then
12
12
  # pass an AST node to `NodePattern#match`. Alternatively, use one of the class
13
13
  # macros in `NodePattern::Macros` to define your own pattern-matching method.
@@ -23,838 +23,7 @@ module RuboCop
23
23
  # - With no block, but multiple captures: captures are returned as an array.
24
24
  # - With no block and no captures: #match returns `true`.
25
25
  #
26
- # ## Pattern string format examples
27
- #
28
- # ':sym' # matches a literal symbol
29
- # '1' # matches a literal integer
30
- # 'nil' # matches a literal nil
31
- # 'send' # matches (send ...)
32
- # '(send)' # matches (send)
33
- # '(send ...)' # matches (send ...)
34
- # '(op-asgn)' # node types with hyphenated names also work
35
- # '{send class}' # matches (send ...) or (class ...)
36
- # '({send class})' # matches (send) or (class)
37
- # '(send const)' # matches (send (const ...))
38
- # '(send _ :new)' # matches (send <anything> :new)
39
- # '(send $_ :new)' # as above, but whatever matches the $_ is captured
40
- # '(send $_ $_)' # you can use as many captures as you want
41
- # '(send !const ...)' # ! negates the next part of the pattern
42
- # '$(send const ...)' # arbitrary matching can be performed on a capture
43
- # '(send _recv _msg)' # wildcards can be named (for readability)
44
- # '(send ... :new)' # you can match against the last children
45
- # '(array <str sym>)' # you can match children in any order. This
46
- # # would match `['x', :y]` as well as `[:y, 'x']
47
- # '(_ <str sym ...>)' # will match if arguments have at least a `str` and
48
- # # a `sym` node, but can have more.
49
- # '(array <$str $_>)' # captures are in the order of the pattern,
50
- # # irrespective of the actual order of the children
51
- # '(array int*)' # will match an array of 0 or more integers
52
- # '(array int ?)' # will match 0 or 1 integer.
53
- # # Note: Space needed to distinguish from int?
54
- # '(array int+)' # will match an array of 1 or more integers
55
- # '(array (int $_)+)' # as above and will capture the numbers in an array
56
- # '(send $...)' # capture all the children as an array
57
- # '(send $... int)' # capture all children but the last as an array
58
- # '(send _x :+ _x)' # unification is performed on named wildcards
59
- # # (like Prolog variables...)
60
- # # (#== is used to see if values unify)
61
- # '(int odd?)' # words which end with a ? are predicate methods,
62
- # # are are called on the target to see if it matches
63
- # # any Ruby method which the matched object supports
64
- # # can be used
65
- # # if a truthy value is returned, the match succeeds
66
- # '(int [!1 !2])' # [] contains multiple patterns, ALL of which must
67
- # # match in that position
68
- # # in other words, while {} is pattern union (logical
69
- # # OR), [] is intersection (logical AND)
70
- # '(send %1 _)' # % stands for a parameter which must be supplied to
71
- # # #match at matching time
72
- # # it will be compared to the corresponding value in
73
- # # the AST using #=== so you can pass Procs, Regexp,
74
- # # etc. in addition to Nodes or literals.
75
- # # `Array#===` will never match a node element, but
76
- # # `Set#===` is an alias to `Set#include?` (Ruby 2.5+
77
- # # only), and so can be very useful to match within
78
- # # many possible literals / Nodes.
79
- # # a bare '%' is the same as '%1'
80
- # # the number of extra parameters passed to #match
81
- # # must equal the highest % value in the pattern
82
- # # for consistency, %0 is the 'root node' which is
83
- # # passed as the 1st argument to #match, where the
84
- # # matching process starts
85
- # '(send _ %named)' # arguments can also be passed as named
86
- # # parameters (see `%1`)
87
- # # Note that the macros `def_node_matcher` and
88
- # # `def_node_search` accept default values for these.
89
- # '(send _ %CONST)' # the named constant will act like `%1` and `%named`.
90
- # '^^send' # each ^ ascends one level in the AST
91
- # # so this matches against the grandparent node
92
- # '`send' # descends any number of level in the AST
93
- # # so this matches against any descendant node
94
- # '#method' # we call this a 'funcall'; it calls a method in the
95
- # # context where a pattern-matching method is defined
96
- # # if that returns a truthy value, the match succeeds
97
- # 'equal?(%1)' # predicates can be given 1 or more extra args
98
- # '#method(%0, 1)' # funcalls can also be given 1 or more extra args
99
- # # These arguments can be patterns themselves, in
100
- # # which case a matcher responding to === will be
101
- # # passed.
102
- # '# comment' # comments are accepted at the end of lines
103
- #
104
- # You can nest arbitrarily deep:
105
- #
106
- # # matches node parsed from 'Const = Class.new' or 'Const = Module.new':
107
- # '(casgn nil? :Const (send (const nil? {:Class :Module}) :new))'
108
- # # matches a node parsed from an 'if', with a '==' comparison,
109
- # # and no 'else' branch:
110
- # '(if (send _ :== _) _ nil?)'
111
- #
112
- # Note that patterns like 'send' are implemented by calling `#send_type?` on
113
- # the node being matched, 'const' by `#const_type?`, 'int' by `#int_type?`,
114
- # and so on. Therefore, if you add methods which are named like
115
- # `#prefix_type?` to the AST node class, then 'prefix' will become usable as
116
- # a pattern.
117
26
  class NodePattern
118
- # @private
119
- Invalid = Class.new(StandardError)
120
-
121
- # @private
122
- # Builds Ruby code which implements a pattern
123
- class Compiler
124
- SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze
125
- IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze
126
- COMMENT = /#\s.*$/.freeze
127
-
128
- META = Regexp.union(
129
- %w"( ) { } [ ] $< < > $... $ ! ^ ` ... + * ?"
130
- ).freeze
131
- NUMBER = /-?\d+(?:\.\d+)?/.freeze
132
- STRING = /".+?"/.freeze
133
- METHOD_NAME = /\#?#{IDENTIFIER}[!?]?\(?/.freeze
134
- PARAM_CONST = /%[A-Z:][a-zA-Z_:]+/.freeze
135
- KEYWORD_NAME = /%[a-z_]+/.freeze
136
- PARAM_NUMBER = /%\d*/.freeze
137
-
138
- SEPARATORS = /\s+/.freeze
139
- ONLY_SEPARATOR = /\A#{SEPARATORS}\Z/.freeze
140
-
141
- TOKENS = Regexp.union(META, PARAM_CONST, KEYWORD_NAME, PARAM_NUMBER, NUMBER,
142
- METHOD_NAME, SYMBOL, STRING)
143
-
144
- TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze
145
-
146
- NODE = /\A#{IDENTIFIER}\Z/.freeze
147
- PREDICATE = /\A#{IDENTIFIER}\?\(?\Z/.freeze
148
- WILDCARD = /\A_(?:#{IDENTIFIER})?\Z/.freeze
149
-
150
- FUNCALL = /\A\##{METHOD_NAME}/.freeze
151
- LITERAL = /\A(?:#{SYMBOL}|#{NUMBER}|#{STRING})\Z/.freeze
152
- PARAM = /\A#{PARAM_NUMBER}\Z/.freeze
153
- CONST = /\A#{PARAM_CONST}\Z/.freeze
154
- KEYWORD = /\A#{KEYWORD_NAME}\Z/.freeze
155
- CLOSING = /\A(?:\)|\}|\])\Z/.freeze
156
-
157
- REST = '...'
158
- CAPTURED_REST = '$...'
159
-
160
- attr_reader :match_code, :tokens, :captures
161
-
162
- SEQ_HEAD_INDEX = -1
163
-
164
- # Placeholders while compiling, see with_..._context methods
165
- CUR_PLACEHOLDER = '@@@cur'
166
- CUR_NODE = "#{CUR_PLACEHOLDER} node@@@"
167
- CUR_ELEMENT = "#{CUR_PLACEHOLDER} element@@@"
168
- SEQ_HEAD_GUARD = '@@@seq guard head@@@'
169
- MULTIPLE_CUR_PLACEHOLDER = /#{CUR_PLACEHOLDER}.*#{CUR_PLACEHOLDER}/.freeze
170
-
171
- line = __LINE__
172
- ANY_ORDER_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
173
- <% if capture_rest %>(<%= capture_rest %> = []) && <% end -%>
174
- <% if capture_all %>(<%= capture_all %> = <% end -%>
175
- <%= CUR_NODE %>.children[<%= range %>]<% if capture_all %>)<% end -%>
176
- .each_with_object({}) { |<%= child %>, <%= matched %>|
177
- case
178
- <% patterns.each_with_index do |pattern, i| -%>
179
- when !<%= matched %>[<%= i %>] && <%=
180
- with_context(pattern, child, use_temp_node: false)
181
- %> then <%= matched %>[<%= i %>] = true
182
- <% end -%>
183
- <% if !rest %> else break({})
184
- <% elsif capture_rest %> else <%= capture_rest %> << <%= child %>
185
- <% end -%>
186
- end
187
- }.size == <%= patterns.size -%>
188
- RUBY
189
- ANY_ORDER_TEMPLATE.location = [__FILE__, line + 1]
190
-
191
- line = __LINE__
192
- REPEATED_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
193
- <% if captured %>(<%= accumulate %> = Array.new) && <% end %>
194
- <%= CUR_NODE %>.children[<%= range %>].all? do |<%= child %>|
195
- <%= with_context(expr, child, use_temp_node: false) %><% if captured %>&&
196
- <%= accumulate %>.push(<%= captured %>)<% end %>
197
- end <% if captured %>&&
198
- (<%= captured %> = if <%= accumulate %>.empty?
199
- <%= captured %>.map{[]} # Transpose hack won't work for empty case
200
- else
201
- <%= accumulate %>.transpose
202
- end) <% end -%>
203
- RUBY
204
- REPEATED_TEMPLATE.location = [__FILE__, line + 1]
205
-
206
- def initialize(str, root = 'node0', node_var = root)
207
- @string = str
208
- # For def_node_matcher, root == node_var
209
- # For def_node_search, root is the root node to search on,
210
- # and node_var is the current descendant being searched.
211
- @root = root
212
- @node_var = node_var
213
-
214
- @temps = 0 # avoid name clashes between temp variables
215
- @captures = 0 # number of captures seen
216
- @unify = {} # named wildcard -> temp variable
217
- @params = 0 # highest % (param) number seen
218
- @keywords = Set[] # keyword parameters seen
219
- run
220
- end
221
-
222
- def run
223
- @tokens = Compiler.tokens(@string)
224
-
225
- @match_code = with_context(compile_expr, @node_var, use_temp_node: false)
226
- @match_code.prepend("(captures = Array.new(#{@captures})) && ") \
227
- if @captures.positive?
228
-
229
- fail_due_to('unbalanced pattern') unless tokens.empty?
230
- end
231
-
232
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
233
- def compile_expr(token = tokens.shift)
234
- # read a single pattern-matching expression from the token stream,
235
- # return Ruby code which performs the corresponding matching operation
236
- #
237
- # the 'pattern-matching' expression may be a composite which
238
- # contains an arbitrary number of sub-expressions, but that composite
239
- # must all have precedence higher or equal to that of `&&`
240
- #
241
- # Expressions may use placeholders like:
242
- # CUR_NODE: Ruby code that evaluates to an AST node
243
- # CUR_ELEMENT: Either the node or the type if in first element of
244
- # a sequence (aka seq_head, e.g. "(seq_head first_node_arg ...")
245
- if (atom = compile_atom(token))
246
- return atom_to_expr(atom)
247
- end
248
-
249
- case token
250
- when '(' then compile_seq
251
- when '{' then compile_union
252
- when '[' then compile_intersect
253
- when '!' then compile_negation
254
- when '$' then compile_capture
255
- when '^' then compile_ascend
256
- when '`' then compile_descend
257
- when WILDCARD then compile_new_wildcard(token[1..-1])
258
- when FUNCALL then compile_funcall(token)
259
- when PREDICATE then compile_predicate(token)
260
- when NODE then compile_nodetype(token)
261
- else fail_due_to("invalid token #{token.inspect}")
262
- end
263
- end
264
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
265
-
266
- def tokens_until(stop, what)
267
- return to_enum __method__, stop, what unless block_given?
268
-
269
- fail_due_to("empty #{what}") if tokens.first == stop
270
- yield until tokens.first == stop
271
- tokens.shift
272
- end
273
-
274
- def compile_seq
275
- terms = tokens_until(')', 'sequence').map { variadic_seq_term }
276
- Sequence.new(self, *terms).compile
277
- end
278
-
279
- def compile_guard_clause
280
- "#{CUR_NODE}.is_a?(RuboCop::AST::Node)"
281
- end
282
-
283
- def variadic_seq_term
284
- token = tokens.shift
285
- case token
286
- when CAPTURED_REST then compile_captured_ellipsis
287
- when REST then compile_ellipsis
288
- when '$<' then compile_any_order(next_capture)
289
- when '<' then compile_any_order
290
- else compile_repeated_expr(token)
291
- end
292
- end
293
-
294
- def compile_repeated_expr(token)
295
- before = @captures
296
- expr = compile_expr(token)
297
- min, max = parse_repetition_token
298
- return [1, expr] if min.nil?
299
-
300
- if @captures != before
301
- captured = "captures[#{before}...#{@captures}]"
302
- accumulate = next_temp_variable(:accumulate)
303
- end
304
- arity = min..max || Float::INFINITY
305
-
306
- [arity, repeated_generator(expr, captured, accumulate)]
307
- end
308
-
309
- def repeated_generator(expr, captured, accumulate)
310
- with_temp_variables do |child|
311
- lambda do |range|
312
- fail_due_to 'repeated pattern at beginning of sequence' if range.begin == SEQ_HEAD_INDEX
313
- REPEATED_TEMPLATE.result(binding)
314
- end
315
- end
316
- end
317
-
318
- def parse_repetition_token
319
- case tokens.first
320
- when '*' then min = 0
321
- when '+' then min = 1
322
- when '?' then min = 0
323
- max = 1
324
- else return
325
- end
326
- tokens.shift
327
- [min, max]
328
- end
329
-
330
- # @private
331
- # Builds Ruby code for a sequence
332
- # (head *first_terms variadic_term *last_terms)
333
- class Sequence
334
- extend Forwardable
335
- def_delegators :@compiler, :compile_guard_clause, :with_seq_head_context,
336
- :with_child_context, :fail_due_to
337
-
338
- def initialize(compiler, *arity_term_list)
339
- @arities, @terms = arity_term_list.transpose
340
-
341
- @compiler = compiler
342
- @variadic_index = @arities.find_index { |a| a.is_a?(Range) }
343
- fail_due_to 'multiple variable patterns in same sequence' \
344
- if @variadic_index && !@arities.one? { |a| a.is_a?(Range) }
345
- end
346
-
347
- def compile
348
- [
349
- compile_guard_clause,
350
- compile_child_nb_guard,
351
- compile_seq_head,
352
- *compile_first_terms,
353
- compile_variadic_term,
354
- *compile_last_terms
355
- ].compact.join(" &&\n") << SEQ_HEAD_GUARD
356
- end
357
-
358
- private
359
-
360
- def first_terms_arity
361
- first_terms_range { |r| @arities[r].inject(0, :+) } || 0
362
- end
363
-
364
- def last_terms_arity
365
- last_terms_range { |r| @arities[r].inject(0, :+) } || 0
366
- end
367
-
368
- def variadic_term_min_arity
369
- @variadic_index ? @arities[@variadic_index].begin : 0
370
- end
371
-
372
- def first_terms_range
373
- yield 1..(@variadic_index || @terms.size) - 1 if seq_head?
374
- end
375
-
376
- def last_terms_range
377
- yield @variadic_index + 1...@terms.size if @variadic_index
378
- end
379
-
380
- def seq_head?
381
- @variadic_index != 0
382
- end
383
-
384
- def compile_child_nb_guard
385
- fixed = first_terms_arity + last_terms_arity
386
- min = fixed + variadic_term_min_arity
387
- op = if @variadic_index
388
- max_variadic = @arities[@variadic_index].end
389
- if max_variadic != Float::INFINITY
390
- range = min..fixed + max_variadic
391
- return "(#{range}).cover?(#{CUR_NODE}.children.size)"
392
- end
393
- '>='
394
- else
395
- '=='
396
- end
397
- "#{CUR_NODE}.children.size #{op} #{min}"
398
- end
399
-
400
- def term(index, range)
401
- t = @terms[index]
402
- if t.respond_to? :call
403
- t.call(range)
404
- else
405
- with_child_context(t, range.begin)
406
- end
407
- end
408
-
409
- def compile_seq_head
410
- return unless seq_head?
411
-
412
- fail_due_to 'sequences cannot start with <' \
413
- if @terms[0].respond_to? :call
414
-
415
- with_seq_head_context(@terms[0])
416
- end
417
-
418
- def compile_first_terms
419
- first_terms_range { |range| compile_terms(range, 0) }
420
- end
421
-
422
- def compile_last_terms
423
- last_terms_range { |r| compile_terms(r, -last_terms_arity) }
424
- end
425
-
426
- def compile_terms(index_range, start)
427
- index_range.map do |i|
428
- current = start
429
- start += @arities.fetch(i)
430
- term(i, current..start - 1)
431
- end
432
- end
433
-
434
- def compile_variadic_term
435
- variadic_arity { |arity| term(@variadic_index, arity) }
436
- end
437
-
438
- def variadic_arity
439
- return unless @variadic_index
440
-
441
- first = @variadic_index.positive? ? first_terms_arity : SEQ_HEAD_INDEX
442
- yield first..-last_terms_arity - 1
443
- end
444
- end
445
- private_constant :Sequence
446
-
447
- def compile_captured_ellipsis
448
- capture = next_capture
449
- block = lambda { |range|
450
- # Consider ($...) like (_ $...):
451
- range = 0..range.end if range.begin == SEQ_HEAD_INDEX
452
- "(#{capture} = #{CUR_NODE}.children[#{range}])"
453
- }
454
- [0..Float::INFINITY, block]
455
- end
456
-
457
- def compile_ellipsis
458
- [0..Float::INFINITY, 'true']
459
- end
460
-
461
- # rubocop:disable Metrics/MethodLength
462
- def compile_any_order(capture_all = nil)
463
- rest = capture_rest = nil
464
- patterns = []
465
- with_temp_variables do |child, matched|
466
- tokens_until('>', 'any child') do
467
- fail_due_to 'ellipsis must be at the end of <>' if rest
468
- token = tokens.shift
469
- case token
470
- when CAPTURED_REST then rest = capture_rest = next_capture
471
- when REST then rest = true
472
- else patterns << compile_expr(token)
473
- end
474
- end
475
- [rest ? patterns.size..Float::INFINITY : patterns.size,
476
- ->(range) { ANY_ORDER_TEMPLATE.result(binding) }]
477
- end
478
- end
479
- # rubocop:enable Metrics/MethodLength
480
-
481
- def insure_same_captures(enum, what)
482
- return to_enum __method__, enum, what unless block_given?
483
-
484
- captures_before = captures_after = nil
485
- enum.each do
486
- captures_before ||= @captures
487
- @captures = captures_before
488
- yield
489
- captures_after ||= @captures
490
- fail_due_to("each #{what} must have same # of captures") if captures_after != @captures
491
- end
492
- end
493
-
494
- def access_unify(name)
495
- var = @unify[name]
496
-
497
- if var == :forbidden_unification
498
- fail_due_to "Wildcard #{name} was first seen in a subset of a" \
499
- " union and can't be used outside that union"
500
- end
501
- var
502
- end
503
-
504
- def forbid_unification(*names)
505
- names.each do |name|
506
- @unify[name] = :forbidden_unification
507
- end
508
- end
509
-
510
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
511
- def unify_in_union(enum)
512
- # We need to reset @unify before each branch is processed.
513
- # Moreover we need to keep track of newly encountered wildcards.
514
- # Var `new_unify_intersection` will hold those that are encountered
515
- # in all branches; these are not a problem.
516
- # Var `partial_unify` will hold those encountered in only a subset
517
- # of the branches; these can't be used outside of the union.
518
-
519
- return to_enum __method__, enum unless block_given?
520
-
521
- new_unify_intersection = nil
522
- partial_unify = []
523
- unify_before = @unify.dup
524
-
525
- result = enum.each do |e|
526
- @unify = unify_before.dup if new_unify_intersection
527
- yield e
528
- new_unify = @unify.keys - unify_before.keys
529
- if new_unify_intersection.nil?
530
- # First iteration
531
- new_unify_intersection = new_unify
532
- else
533
- union = new_unify_intersection | new_unify
534
- new_unify_intersection &= new_unify
535
- partial_unify |= union - new_unify_intersection
536
- end
537
- end
538
-
539
- # At this point, all members of `new_unify_intersection` can be used
540
- # for unification outside of the union, but partial_unify may not
541
-
542
- forbid_unification(*partial_unify)
543
-
544
- result
545
- end
546
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
547
-
548
- def compile_union
549
- # we need to ensure that each branch of the {} contains the same
550
- # number of captures (since only one branch of the {} can actually
551
- # match, the same variables are used to hold the captures for each
552
- # branch)
553
- enum = tokens_until('}', 'union')
554
- enum = unify_in_union(enum)
555
- terms = insure_same_captures(enum, 'branch of {}')
556
- .map { compile_expr }
557
-
558
- "(#{terms.join(' || ')})"
559
- end
560
-
561
- def compile_intersect
562
- tokens_until(']', 'intersection')
563
- .map { compile_expr }
564
- .join(' && ')
565
- end
566
-
567
- def compile_capture
568
- "(#{next_capture} = #{CUR_ELEMENT}; #{compile_expr})"
569
- end
570
-
571
- def compile_negation
572
- "!(#{compile_expr})"
573
- end
574
-
575
- def compile_ascend
576
- with_context("#{CUR_NODE} && #{compile_expr}", "#{CUR_NODE}.parent")
577
- end
578
-
579
- def compile_descend
580
- with_temp_variables do |descendant|
581
- pattern = with_context(compile_expr, descendant,
582
- use_temp_node: false)
583
- [
584
- "RuboCop::AST::NodePattern.descend(#{CUR_ELEMENT}).",
585
- "any? do |#{descendant}|",
586
- " #{pattern}",
587
- 'end'
588
- ].join("\n")
589
- end
590
- end
591
-
592
- # Known wildcards are considered atoms, see `compile_atom`
593
- def compile_new_wildcard(name)
594
- return 'true' if name.empty?
595
-
596
- n = @unify[name] = "unify_#{name.gsub('-', '__')}"
597
- # double assign to avoid "assigned but unused variable"
598
- "(#{n} = #{CUR_ELEMENT}; #{n} = #{n}; true)"
599
- end
600
-
601
- def compile_predicate(predicate)
602
- if predicate.end_with?('(') # is there an arglist?
603
- args = compile_args
604
- predicate = predicate[0..-2] # drop the trailing (
605
- "#{CUR_ELEMENT}.#{predicate}(#{args.join(',')})"
606
- else
607
- "#{CUR_ELEMENT}.#{predicate}"
608
- end
609
- end
610
-
611
- def compile_funcall(method)
612
- # call a method in the context which this pattern-matching
613
- # code is used in. pass target value as an argument
614
- method = method[1..-1] # drop the leading #
615
- if method.end_with?('(') # is there an arglist?
616
- args = compile_args
617
- method = method[0..-2] # drop the trailing (
618
- "#{method}(#{CUR_ELEMENT},#{args.join(',')})"
619
- else
620
- "#{method}(#{CUR_ELEMENT})"
621
- end
622
- end
623
-
624
- def compile_nodetype(type)
625
- "#{compile_guard_clause} && #{CUR_NODE}.#{type.tr('-', '_')}_type?"
626
- end
627
-
628
- def compile_args
629
- tokens_until(')', 'call arguments').map do
630
- arg = compile_arg
631
- tokens.shift if tokens.first == ','
632
- arg
633
- end
634
- end
635
-
636
- def atom_to_expr(atom)
637
- "#{atom} === #{CUR_ELEMENT}"
638
- end
639
-
640
- def expr_to_atom(expr)
641
- with_temp_variables do |compare|
642
- in_context = with_context(expr, compare, use_temp_node: false)
643
- "::RuboCop::AST::NodePattern::Matcher.new{|#{compare}| #{in_context}}"
644
- end
645
- end
646
-
647
- # @return compiled atom (e.g. ":literal" or "SOME_CONST")
648
- # or nil if not a simple atom (unknown wildcard, other tokens)
649
- def compile_atom(token)
650
- case token
651
- when WILDCARD then access_unify(token[1..-1]) # could be nil
652
- when LITERAL then token
653
- when KEYWORD then get_keyword(token[1..-1])
654
- when CONST then get_const(token[1..-1])
655
- when PARAM then get_param(token[1..-1])
656
- when CLOSING then fail_due_to("#{token} in invalid position")
657
- when nil then fail_due_to('pattern ended prematurely')
658
- end
659
- end
660
-
661
- def compile_arg
662
- token = tokens.shift
663
- compile_atom(token) || expr_to_atom(compile_expr(token))
664
- end
665
-
666
- def next_capture
667
- index = @captures
668
- @captures += 1
669
- "captures[#{index}]"
670
- end
671
-
672
- def get_param(number)
673
- number = number.empty? ? 1 : Integer(number)
674
- @params = number if number > @params
675
- number.zero? ? @root : "param#{number}"
676
- end
677
-
678
- def get_keyword(name)
679
- @keywords << name
680
- name
681
- end
682
-
683
- def get_const(const)
684
- const # Output the constant exactly as given
685
- end
686
-
687
- def emit_yield_capture(when_no_capture = '')
688
- yield_val = if @captures.zero?
689
- when_no_capture
690
- elsif @captures == 1
691
- 'captures[0]' # Circumvent https://github.com/jruby/jruby/issues/5710
692
- else
693
- '*captures'
694
- end
695
- "yield(#{yield_val})"
696
- end
697
-
698
- def emit_retval
699
- if @captures.zero?
700
- 'true'
701
- elsif @captures == 1
702
- 'captures[0]'
703
- else
704
- 'captures'
705
- end
706
- end
707
-
708
- def emit_param_list
709
- (1..@params).map { |n| "param#{n}" }.join(',')
710
- end
711
-
712
- def emit_keyword_list(forwarding: false)
713
- pattern = "%<keyword>s: #{'%<keyword>s' if forwarding}"
714
- @keywords.map { |k| format(pattern, keyword: k) }.join(',')
715
- end
716
-
717
- def emit_params(*first, forwarding: false)
718
- params = emit_param_list
719
- keywords = emit_keyword_list(forwarding: forwarding)
720
- [*first, params, keywords].reject(&:empty?).join(',')
721
- end
722
-
723
- def emit_method_code
724
- <<~RUBY
725
- return unless #{@match_code}
726
- block_given? ? #{emit_yield_capture} : (return #{emit_retval})
727
- RUBY
728
- end
729
-
730
- def fail_due_to(message)
731
- raise Invalid, "Couldn't compile due to #{message}. Pattern: #{@string}"
732
- end
733
-
734
- def with_temp_node(cur_node)
735
- with_temp_variables do |node|
736
- yield "(#{node} = #{cur_node})", node
737
- end
738
- .gsub("\n", "\n ") # Nicer indent for debugging
739
- end
740
-
741
- def with_temp_variables(&block)
742
- names = block.parameters.map { |_, name| next_temp_variable(name) }
743
- yield(*names)
744
- end
745
-
746
- def next_temp_variable(name)
747
- "#{name}#{next_temp_value}"
748
- end
749
-
750
- def next_temp_value
751
- @temps += 1
752
- end
753
-
754
- def auto_use_temp_node?(code)
755
- code.match?(MULTIPLE_CUR_PLACEHOLDER)
756
- end
757
-
758
- # with_<...>_context methods are used whenever the context,
759
- # i.e the current node or the current element can be determined.
760
-
761
- def with_child_context(code, child_index)
762
- with_context(code, "#{CUR_NODE}.children[#{child_index}]")
763
- end
764
-
765
- def with_context(code, cur_node,
766
- use_temp_node: auto_use_temp_node?(code))
767
- if use_temp_node
768
- with_temp_node(cur_node) do |init, temp_var|
769
- substitute_cur_node(code, temp_var, first_cur_node: init)
770
- end
771
- else
772
- substitute_cur_node(code, cur_node)
773
- end
774
- end
775
-
776
- def with_seq_head_context(code)
777
- fail_due_to('parentheses at sequence head') if code.include?(SEQ_HEAD_GUARD)
778
-
779
- code.gsub CUR_ELEMENT, "#{CUR_NODE}.type"
780
- end
781
-
782
- def substitute_cur_node(code, cur_node, first_cur_node: cur_node)
783
- iter = 0
784
- code
785
- .gsub(CUR_ELEMENT, CUR_NODE)
786
- .gsub(CUR_NODE) do
787
- iter += 1
788
- iter == 1 ? first_cur_node : cur_node
789
- end
790
- .gsub(SEQ_HEAD_GUARD, '')
791
- end
792
-
793
- def self.tokens(pattern)
794
- pattern.gsub(COMMENT, '').scan(TOKEN).grep_v(ONLY_SEPARATOR)
795
- end
796
-
797
- # This method minimizes the closure for our method
798
- def wrapping_block(method_name, **defaults)
799
- proc do |*args, **values|
800
- send method_name, *args, **defaults, **values
801
- end
802
- end
803
-
804
- def def_helper(base, method_name, **defaults)
805
- location = caller_locations(3, 1).first
806
- unless defaults.empty?
807
- call = :"without_defaults_#{method_name}"
808
- base.send :define_method, method_name, &wrapping_block(call, **defaults)
809
- method_name = call
810
- end
811
- src = yield method_name
812
- base.class_eval(src, location.path, location.lineno)
813
- end
814
-
815
- def def_node_matcher(base, method_name, **defaults)
816
- def_helper(base, method_name, **defaults) do |name|
817
- <<~RUBY
818
- def #{name}(#{emit_params('node = self')})
819
- #{emit_method_code}
820
- end
821
- RUBY
822
- end
823
- end
824
-
825
- def def_node_search(base, method_name, **defaults)
826
- def_helper(base, method_name, **defaults) do |name|
827
- emit_node_search(name)
828
- end
829
- end
830
-
831
- def emit_node_search(method_name)
832
- if method_name.to_s.end_with?('?')
833
- on_match = 'return true'
834
- else
835
- args = emit_params(":#{method_name}", @root, forwarding: true)
836
- prelude = "return enum_for(#{args}) unless block_given?\n"
837
- on_match = emit_yield_capture(@node_var)
838
- end
839
- emit_node_search_body(method_name, prelude: prelude, on_match: on_match)
840
- end
841
-
842
- def emit_node_search_body(method_name, prelude:, on_match:)
843
- <<~RUBY
844
- def #{method_name}(#{emit_params(@root)})
845
- #{prelude}
846
- #{@root}.each_node do |#{@node_var}|
847
- if #{match_code}
848
- #{on_match}
849
- end
850
- end
851
- nil
852
- end
853
- RUBY
854
- end
855
- end
856
- private_constant :Compiler
857
-
858
27
  # Helpers for defining methods based on a pattern string
859
28
  module Macros
860
29
  # Define a method which applies a pattern to an AST node
@@ -865,8 +34,7 @@ module RuboCop
865
34
  # If the node matches, and no block is provided, the new method will
866
35
  # return the captures, or `true` if there were none.
867
36
  def def_node_matcher(method_name, pattern_str, **keyword_defaults)
868
- Compiler.new(pattern_str, 'node')
869
- .def_node_matcher(self, method_name, **keyword_defaults)
37
+ NodePattern.new(pattern_str).def_node_matcher(self, method_name, **keyword_defaults)
870
38
  end
871
39
 
872
40
  # Define a method which recurses over the descendants of an AST node,
@@ -876,48 +44,60 @@ module RuboCop
876
44
  # as soon as it finds a descendant which matches. Otherwise, it will
877
45
  # yield all descendants which match.
878
46
  def def_node_search(method_name, pattern_str, **keyword_defaults)
879
- Compiler.new(pattern_str, 'node0', 'node')
880
- .def_node_search(self, method_name, **keyword_defaults)
47
+ NodePattern.new(pattern_str).def_node_search(self, method_name, **keyword_defaults)
881
48
  end
882
49
  end
883
50
 
884
- attr_reader :pattern
51
+ extend Forwardable
52
+ include MethodDefiner
53
+ Invalid = Class.new(StandardError)
885
54
 
886
- def initialize(str)
55
+ VAR = 'node'
56
+
57
+ attr_reader :pattern, :ast, :match_code
58
+
59
+ def_delegators :@compiler, :captures, :named_parameters, :positional_parameters
60
+
61
+ def initialize(str, compiler: Compiler.new)
887
62
  @pattern = str
888
- compiler = Compiler.new(str, 'node0')
889
- src = "def match(#{compiler.emit_params('node0')});" \
890
- "#{compiler.emit_method_code}end"
891
- instance_eval(src, __FILE__, __LINE__ + 1)
63
+ @ast = compiler.parser.parse(str)
64
+ @compiler = compiler
65
+ @match_code = @compiler.compile_as_node_pattern(@ast, var: VAR)
66
+ @cache = {}
892
67
  end
893
68
 
894
- def match(*args, **rest)
895
- # If we're here, it's because the singleton method has not been defined,
896
- # either because we've been dup'ed or serialized through YAML
897
- initialize(pattern)
898
- if rest.empty?
899
- match(*args)
900
- else
901
- match(*args, **rest)
902
- end
69
+ def match(*args, **rest, &block)
70
+ @cache[:lambda] ||= as_lambda
71
+ @cache[:lambda].call(*args, block: block, **rest)
72
+ end
73
+
74
+ def ==(other)
75
+ other.is_a?(NodePattern) && other.ast == ast
76
+ end
77
+ alias eql? ==
78
+
79
+ def to_s
80
+ "#<#{self.class} #{pattern}>"
903
81
  end
904
82
 
905
- def marshal_load(pattern)
83
+ def marshal_load(pattern) #:nodoc:
906
84
  initialize pattern
907
85
  end
908
86
 
909
- def marshal_dump
87
+ def marshal_dump #:nodoc:
910
88
  pattern
911
89
  end
912
90
 
913
- def ==(other)
914
- other.is_a?(NodePattern) &&
915
- Compiler.tokens(other.pattern) == Compiler.tokens(pattern)
91
+ def as_json(_options = nil) #:nodoc:
92
+ pattern
916
93
  end
917
- alias eql? ==
918
94
 
919
- def to_s
920
- "#<#{self.class} #{pattern}>"
95
+ def encode_with(coder) #:nodoc:
96
+ coder['pattern'] = pattern
97
+ end
98
+
99
+ def init_with(coder) #:nodoc:
100
+ initialize(coder['pattern'])
921
101
  end
922
102
 
923
103
  # Yields its argument and any descendants, depth-first.
@@ -936,17 +116,11 @@ module RuboCop
936
116
  nil
937
117
  end
938
118
 
939
- # @api private
940
- class Matcher
941
- def initialize(&block)
942
- @block = block
943
- end
944
-
945
- def ===(compare)
946
- @block.call(compare)
947
- end
119
+ def freeze
120
+ @match_code.freeze
121
+ @compiler.freeze
122
+ super
948
123
  end
949
124
  end
950
125
  end
951
126
  end
952
- # rubocop:enable Metrics/ClassLength, Metrics/CyclomaticComplexity