rubocop-ast 0.0.3 → 0.4.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -4
  3. data/lib/rubocop/ast.rb +9 -1
  4. data/lib/rubocop/ast/builder.rb +8 -1
  5. data/lib/rubocop/ast/ext/range.rb +28 -0
  6. data/lib/rubocop/ast/ext/set.rb +12 -0
  7. data/lib/rubocop/ast/node.rb +81 -10
  8. data/lib/rubocop/ast/node/array_node.rb +2 -8
  9. data/lib/rubocop/ast/node/block_node.rb +1 -1
  10. data/lib/rubocop/ast/node/break_node.rb +1 -6
  11. data/lib/rubocop/ast/node/case_match_node.rb +3 -9
  12. data/lib/rubocop/ast/node/case_node.rb +13 -9
  13. data/lib/rubocop/ast/node/const_node.rb +65 -0
  14. data/lib/rubocop/ast/node/def_node.rb +5 -24
  15. data/lib/rubocop/ast/node/defined_node.rb +2 -0
  16. data/lib/rubocop/ast/node/float_node.rb +1 -0
  17. data/lib/rubocop/ast/node/forward_args_node.rb +15 -0
  18. data/lib/rubocop/ast/node/hash_node.rb +21 -8
  19. data/lib/rubocop/ast/node/if_node.rb +7 -14
  20. data/lib/rubocop/ast/node/index_node.rb +48 -0
  21. data/lib/rubocop/ast/node/indexasgn_node.rb +50 -0
  22. data/lib/rubocop/ast/node/int_node.rb +1 -0
  23. data/lib/rubocop/ast/node/lambda_node.rb +65 -0
  24. data/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +2 -8
  25. data/lib/rubocop/ast/node/mixin/method_identifier_predicates.rb +99 -3
  26. data/lib/rubocop/ast/node/mixin/parameterized_node.rb +56 -0
  27. data/lib/rubocop/ast/node/next_node.rb +12 -0
  28. data/lib/rubocop/ast/node/pair_node.rb +2 -2
  29. data/lib/rubocop/ast/node/regexp_node.rb +56 -0
  30. data/lib/rubocop/ast/node/resbody_node.rb +21 -0
  31. data/lib/rubocop/ast/node/rescue_node.rb +49 -0
  32. data/lib/rubocop/ast/node/return_node.rb +1 -13
  33. data/lib/rubocop/ast/node/send_node.rb +9 -2
  34. data/lib/rubocop/ast/node/super_node.rb +2 -0
  35. data/lib/rubocop/ast/node/when_node.rb +3 -9
  36. data/lib/rubocop/ast/node/yield_node.rb +2 -0
  37. data/lib/rubocop/ast/node_pattern.rb +184 -115
  38. data/lib/rubocop/ast/processed_source.rb +98 -16
  39. data/lib/rubocop/ast/traversal.rb +6 -4
  40. data/lib/rubocop/ast/version.rb +1 -1
  41. metadata +16 -9
  42. data/lib/rubocop/ast/node/retry_node.rb +0 -17
@@ -2,8 +2,11 @@
2
2
 
3
3
  module RuboCop
4
4
  module AST
5
+ # Requires implementing `arguments`.
6
+ #
5
7
  # Common functionality for nodes that are parameterized:
6
8
  # `send`, `super`, `zsuper`, `def`, `defs`
9
+ # and (modern only): `index`, `indexasgn`, `lambda`
7
10
  module ParameterizedNode
8
11
  # Checks whether this node's arguments are wrapped in parentheses.
9
12
  #
@@ -56,6 +59,59 @@ module RuboCop
56
59
  arguments? &&
57
60
  (last_argument.block_pass_type? || last_argument.blockarg_type?)
58
61
  end
62
+
63
+ # A specialized `ParameterizedNode` for node that have a single child
64
+ # containing either `nil`, an argument, or a `begin` node with all the
65
+ # arguments
66
+ module WrappedArguments
67
+ include ParameterizedNode
68
+ # @return [Array] The arguments of the node.
69
+ def arguments
70
+ first = children.first
71
+ if first&.begin_type?
72
+ first.children
73
+ else
74
+ children
75
+ end
76
+ end
77
+ end
78
+
79
+ # A specialized `ParameterizedNode`.
80
+ # Requires implementing `first_argument_index`
81
+ # Implements `arguments` as `children[first_argument_index..-1]`
82
+ # and optimizes other calls
83
+ module RestArguments
84
+ include ParameterizedNode
85
+ # @return [Array<Node>] arguments, if any
86
+ def arguments
87
+ children[first_argument_index..-1].freeze
88
+ end
89
+
90
+ # A shorthand for getting the first argument of the node.
91
+ # Equivalent to `arguments.first`.
92
+ #
93
+ # @return [Node, nil] the first argument of the node,
94
+ # or `nil` if there are no arguments
95
+ def first_argument
96
+ children[first_argument_index]
97
+ end
98
+
99
+ # A shorthand for getting the last argument of the node.
100
+ # Equivalent to `arguments.last`.
101
+ #
102
+ # @return [Node, nil] the last argument of the node,
103
+ # or `nil` if there are no arguments
104
+ def last_argument
105
+ children[-1] if arguments?
106
+ end
107
+
108
+ # Checks whether this node has any arguments.
109
+ #
110
+ # @return [Boolean] whether this node has any arguments
111
+ def arguments?
112
+ children.size > first_argument_index
113
+ end
114
+ end
59
115
  end
60
116
  end
61
117
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ # A node extension for `next` nodes. This will be used in place of a
6
+ # plain node when the builder constructs the AST, making its methods
7
+ # available to all `next` nodes within RuboCop.
8
+ class NextNode < Node
9
+ include ParameterizedNode::WrappedArguments
10
+ end
11
+ end
12
+ end
@@ -32,7 +32,7 @@ module RuboCop
32
32
  #
33
33
  # @param [Boolean] with_spacing whether to include spacing
34
34
  # @return [String] the delimiter of the `pair`
35
- def delimiter(with_spacing = false)
35
+ def delimiter(*deprecated, with_spacing: deprecated.first)
36
36
  if with_spacing
37
37
  hash_rocket? ? SPACED_HASH_ROCKET : SPACED_COLON
38
38
  else
@@ -44,7 +44,7 @@ module RuboCop
44
44
  #
45
45
  # @param [Boolean] with_spacing whether to include spacing
46
46
  # @return [String] the inverse delimiter of the `pair`
47
- def inverse_delimiter(with_spacing = false)
47
+ def inverse_delimiter(*deprecated, with_spacing: deprecated.first)
48
48
  if with_spacing
49
49
  hash_rocket? ? SPACED_COLON : SPACED_HASH_ROCKET
50
50
  else
@@ -31,6 +31,62 @@ module RuboCop
31
31
  def content
32
32
  children.select(&:str_type?).map(&:str_content).join
33
33
  end
34
+
35
+ # @return [Bool] if the regexp is a /.../ literal
36
+ def slash_literal?
37
+ loc.begin.source == '/'
38
+ end
39
+
40
+ # @return [Bool] if the regexp is a %r{...} literal (using any delimiters)
41
+ def percent_r_literal?
42
+ !slash_literal?
43
+ end
44
+
45
+ # @return [String] the regexp delimiters (without %r)
46
+ def delimiters
47
+ [loc.begin.source[-1], loc.end.source[0]]
48
+ end
49
+
50
+ # @return [Bool] if char is one of the delimiters
51
+ def delimiter?(char)
52
+ delimiters.include?(char)
53
+ end
54
+
55
+ # @return [Bool] if regexp contains interpolation
56
+ def interpolation?
57
+ children.any?(&:begin_type?)
58
+ end
59
+
60
+ # @return [Bool] if regexp uses the multiline regopt
61
+ def multiline_mode?
62
+ regopt_include?(:m)
63
+ end
64
+
65
+ # @return [Bool] if regexp uses the extended regopt
66
+ def extended?
67
+ regopt_include?(:x)
68
+ end
69
+
70
+ # @return [Bool] if regexp uses the ignore-case regopt
71
+ def ignore_case?
72
+ regopt_include?(:i)
73
+ end
74
+
75
+ # @return [Bool] if regexp uses the single-interpolation regopt
76
+ def single_interpolation?
77
+ regopt_include?(:o)
78
+ end
79
+
80
+ # @return [Bool] if regexp uses the no-encoding regopt
81
+ def no_encoding?
82
+ regopt_include?(:n)
83
+ end
84
+
85
+ private
86
+
87
+ def regopt_include?(option)
88
+ regopt.children.include?(option)
89
+ end
34
90
  end
35
91
  end
36
92
  end
@@ -13,12 +13,33 @@ module RuboCop
13
13
  node_parts[2]
14
14
  end
15
15
 
16
+ # Returns an array of all the exceptions in the `rescue` clause.
17
+ #
18
+ # @return [Array<Node>] an array of exception nodes
19
+ def exceptions
20
+ exceptions_node = node_parts[0]
21
+ if exceptions_node.nil?
22
+ []
23
+ elsif exceptions_node.array_type?
24
+ exceptions_node.values
25
+ else
26
+ [exceptions_node]
27
+ end
28
+ end
29
+
16
30
  # Returns the exception variable of the `rescue` clause.
17
31
  #
18
32
  # @return [Node, nil] The exception variable of the `resbody`.
19
33
  def exception_variable
20
34
  node_parts[1]
21
35
  end
36
+
37
+ # Returns the index of the `resbody` branch within the exception handling statement.
38
+ #
39
+ # @return [Integer] the index of the `resbody` branch
40
+ def branch_index
41
+ parent.resbody_branches.index(self)
42
+ end
22
43
  end
23
44
  end
24
45
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module AST
5
+ # A node extension for `rescue` nodes. This will be used in place of a
6
+ # plain node when the builder constructs the AST, making its methods
7
+ # available to all `rescue` nodes within RuboCop.
8
+ class RescueNode < Node
9
+ # Returns the body of the rescue node.
10
+ #
11
+ # @return [Node, nil] The body of the rescue node.
12
+ def body
13
+ node_parts[0]
14
+ end
15
+
16
+ # Returns an array of all the rescue branches in the exception handling statement.
17
+ #
18
+ # @return [Array<ResbodyNode>] an array of `resbody` nodes
19
+ def resbody_branches
20
+ node_parts[1...-1]
21
+ end
22
+
23
+ # Returns an array of all the rescue branches in the exception handling statement.
24
+ #
25
+ # @return [Array<Node, nil>] an array of the bodies of the rescue branches
26
+ # and the else (if any). Note that these bodies could be nil.
27
+ def branches
28
+ bodies = resbody_branches.map(&:body)
29
+ bodies.push(else_branch) if else?
30
+ bodies
31
+ end
32
+
33
+ # Returns the else branch of the exception handling statement, if any.
34
+ #
35
+ # @return [Node] the else branch node of the exception handling statement
36
+ # @return [nil] if the exception handling statement does not have an else branch.
37
+ def else_branch
38
+ node_parts[-1]
39
+ end
40
+
41
+ # Checks whether this exception handling statement has an `else` branch.
42
+ #
43
+ # @return [Boolean] whether the exception handling statement has an `else` branch
44
+ def else?
45
+ loc.else
46
+ end
47
+ end
48
+ end
49
+ end
@@ -6,19 +6,7 @@ module RuboCop
6
6
  # plain node when the builder constructs the AST, making its methods
7
7
  # available to all `return` nodes within RuboCop.
8
8
  class ReturnNode < Node
9
- include MethodDispatchNode
10
- include ParameterizedNode
11
-
12
- # Returns the arguments of the `return`.
13
- #
14
- # @return [Array] The arguments of the `return`.
15
- def arguments
16
- if node_parts.one? && node_parts.first.begin_type?
17
- node_parts.first.children
18
- else
19
- node_parts
20
- end
21
- end
9
+ include ParameterizedNode::WrappedArguments
22
10
  end
23
11
  end
24
12
  end
@@ -6,12 +6,19 @@ module RuboCop
6
6
  # node when the builder constructs the AST, making its methods available
7
7
  # to all `send` nodes within RuboCop.
8
8
  class SendNode < Node
9
- include ParameterizedNode
9
+ include ParameterizedNode::RestArguments
10
10
  include MethodDispatchNode
11
11
 
12
12
  def_node_matcher :attribute_accessor?, <<~PATTERN
13
- (send nil? ${:attr_reader :attr_writer :attr_accessor :attr} $...)
13
+ [(send nil? ${:attr_reader :attr_writer :attr_accessor :attr} $...)
14
+ (_ _ _ _ ...)]
14
15
  PATTERN
16
+
17
+ private
18
+
19
+ def first_argument_index
20
+ 2
21
+ end
15
22
  end
16
23
  end
17
24
  end
@@ -16,6 +16,8 @@ module RuboCop
16
16
  def node_parts
17
17
  [nil, :super, *to_a]
18
18
  end
19
+
20
+ alias arguments children
19
21
  end
20
22
  end
21
23
  end
@@ -13,17 +13,11 @@ module RuboCop
13
13
  node_parts[0...-1]
14
14
  end
15
15
 
16
- # Calls the given block for each condition node in the `when` branch.
17
- # If no block is given, an `Enumerator` is returned.
18
- #
19
- # @return [self] if a block is given
20
- # @return [Enumerator] if no block is given
21
- def each_condition
16
+ # @deprecated Use `conditions.each`
17
+ def each_condition(&block)
22
18
  return conditions.to_enum(__method__) unless block_given?
23
19
 
24
- conditions.each do |condition|
25
- yield condition
26
- end
20
+ conditions.each(&block)
27
21
 
28
22
  self
29
23
  end
@@ -16,6 +16,8 @@ module RuboCop
16
16
  def node_parts
17
17
  [nil, :yield, *to_a]
18
18
  end
19
+
20
+ alias arguments children
19
21
  end
20
22
  end
21
23
  end
@@ -70,13 +70,23 @@ module RuboCop
70
70
  # '(send %1 _)' # % stands for a parameter which must be supplied to
71
71
  # # #match at matching time
72
72
  # # it will be compared to the corresponding value in
73
- # # the AST using #==
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.
74
79
  # # a bare '%' is the same as '%1'
75
80
  # # the number of extra parameters passed to #match
76
81
  # # must equal the highest % value in the pattern
77
82
  # # for consistency, %0 is the 'root node' which is
78
83
  # # passed as the 1st argument to #match, where the
79
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`.
80
90
  # '^^send' # each ^ ascends one level in the AST
81
91
  # # so this matches against the grandparent node
82
92
  # '`send' # descends any number of level in the AST
@@ -86,6 +96,10 @@ module RuboCop
86
96
  # # if that returns a truthy value, the match succeeds
87
97
  # 'equal?(%1)' # predicates can be given 1 or more extra args
88
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
89
103
  #
90
104
  # You can nest arbitrarily deep:
91
105
  #
@@ -100,11 +114,6 @@ module RuboCop
100
114
  # and so on. Therefore, if you add methods which are named like
101
115
  # `#prefix_type?` to the AST node class, then 'prefix' will become usable as
102
116
  # a pattern.
103
- #
104
- # Also note that if you need a "guard clause" to protect against possible nils
105
- # in a certain place in the AST, you can do it like this: `[!nil <pattern>]`
106
- #
107
- # The compiler code is very simple; don't be afraid to read through it!
108
117
  class NodePattern
109
118
  # @private
110
119
  Invalid = Class.new(StandardError)
@@ -114,17 +123,23 @@ module RuboCop
114
123
  class Compiler
115
124
  SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze
116
125
  IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze
126
+ COMMENT = /#\s.*$/.freeze
127
+
117
128
  META = Regexp.union(
118
129
  %w"( ) { } [ ] $< < > $... $ ! ^ ` ... + * ?"
119
130
  ).freeze
120
131
  NUMBER = /-?\d+(?:\.\d+)?/.freeze
121
132
  STRING = /".+?"/.freeze
122
- METHOD_NAME = /\#?#{IDENTIFIER}[\!\?]?\(?/.freeze
133
+ METHOD_NAME = /\#?#{IDENTIFIER}[!?]?\(?/.freeze
134
+ PARAM_CONST = /%[A-Z:][a-zA-Z_:]+/.freeze
135
+ KEYWORD_NAME = /%[a-z_]+/.freeze
123
136
  PARAM_NUMBER = /%\d*/.freeze
124
137
 
125
- SEPARATORS = /[\s]+/.freeze
126
- TOKENS = Regexp.union(META, PARAM_NUMBER, NUMBER,
127
- METHOD_NAME, SYMBOL, STRING)
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)
128
143
 
129
144
  TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze
130
145
 
@@ -135,6 +150,8 @@ module RuboCop
135
150
  FUNCALL = /\A\##{METHOD_NAME}/.freeze
136
151
  LITERAL = /\A(?:#{SYMBOL}|#{NUMBER}|#{STRING})\Z/.freeze
137
152
  PARAM = /\A#{PARAM_NUMBER}\Z/.freeze
153
+ CONST = /\A#{PARAM_CONST}\Z/.freeze
154
+ KEYWORD = /\A#{KEYWORD_NAME}\Z/.freeze
138
155
  CLOSING = /\A(?:\)|\}|\])\Z/.freeze
139
156
 
140
157
  REST = '...'
@@ -149,6 +166,7 @@ module RuboCop
149
166
  CUR_NODE = "#{CUR_PLACEHOLDER} node@@@"
150
167
  CUR_ELEMENT = "#{CUR_PLACEHOLDER} element@@@"
151
168
  SEQ_HEAD_GUARD = '@@@seq guard head@@@'
169
+ MULTIPLE_CUR_PLACEHOLDER = /#{CUR_PLACEHOLDER}.*#{CUR_PLACEHOLDER}/.freeze
152
170
 
153
171
  line = __LINE__
154
172
  ANY_ORDER_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
@@ -185,21 +203,26 @@ module RuboCop
185
203
  RUBY
186
204
  REPEATED_TEMPLATE.location = [__FILE__, line + 1]
187
205
 
188
- def initialize(str, node_var = 'node0')
206
+ def initialize(str, root = 'node0', node_var = root)
189
207
  @string = str
190
- @root = node_var
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
191
213
 
192
214
  @temps = 0 # avoid name clashes between temp variables
193
215
  @captures = 0 # number of captures seen
194
216
  @unify = {} # named wildcard -> temp variable
195
217
  @params = 0 # highest % (param) number seen
196
- run(node_var)
218
+ @keywords = Set[] # keyword parameters seen
219
+ run
197
220
  end
198
221
 
199
- def run(node_var)
222
+ def run
200
223
  @tokens = Compiler.tokens(@string)
201
224
 
202
- @match_code = with_context(compile_expr, node_var, use_temp_node: false)
225
+ @match_code = with_context(compile_expr, @node_var, use_temp_node: false)
203
226
  @match_code.prepend("(captures = Array.new(#{@captures})) && ") \
204
227
  if @captures.positive?
205
228
 
@@ -219,6 +242,10 @@ module RuboCop
219
242
  # CUR_NODE: Ruby code that evaluates to an AST node
220
243
  # CUR_ELEMENT: Either the node or the type if in first element of
221
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
+
222
249
  case token
223
250
  when '(' then compile_seq
224
251
  when '{' then compile_union
@@ -227,14 +254,10 @@ module RuboCop
227
254
  when '$' then compile_capture
228
255
  when '^' then compile_ascend
229
256
  when '`' then compile_descend
230
- when WILDCARD then compile_wildcard(token[1..-1])
257
+ when WILDCARD then compile_new_wildcard(token[1..-1])
231
258
  when FUNCALL then compile_funcall(token)
232
- when LITERAL then compile_literal(token)
233
259
  when PREDICATE then compile_predicate(token)
234
260
  when NODE then compile_nodetype(token)
235
- when PARAM then compile_param(token[1..-1])
236
- when CLOSING then fail_due_to("#{token} in invalid position")
237
- when nil then fail_due_to('pattern ended prematurely')
238
261
  else fail_due_to("invalid token #{token.inspect}")
239
262
  end
240
263
  end
@@ -243,7 +266,7 @@ module RuboCop
243
266
  def tokens_until(stop, what)
244
267
  return to_enum __method__, stop, what unless block_given?
245
268
 
246
- fail_due_to("empty #{what}") if tokens.first == stop && what
269
+ fail_due_to("empty #{what}") if tokens.first == stop
247
270
  yield until tokens.first == stop
248
271
  tokens.shift
249
272
  end
@@ -307,11 +330,15 @@ module RuboCop
307
330
  # @private
308
331
  # Builds Ruby code for a sequence
309
332
  # (head *first_terms variadic_term *last_terms)
310
- class Sequence < SimpleDelegator
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
+
311
338
  def initialize(compiler, *arity_term_list)
312
339
  @arities, @terms = arity_term_list.transpose
313
340
 
314
- super(compiler)
341
+ @compiler = compiler
315
342
  @variadic_index = @arities.find_index { |a| a.is_a?(Range) }
316
343
  fail_due_to 'multiple variable patterns in same sequence' \
317
344
  if @variadic_index && !@arities.one? { |a| a.is_a?(Range) }
@@ -431,7 +458,6 @@ module RuboCop
431
458
  [0..Float::INFINITY, 'true']
432
459
  end
433
460
 
434
- # rubocop:disable Metrics/AbcSize
435
461
  # rubocop:disable Metrics/MethodLength
436
462
  def compile_any_order(capture_all = nil)
437
463
  rest = capture_rest = nil
@@ -451,7 +477,6 @@ module RuboCop
451
477
  end
452
478
  end
453
479
  # rubocop:enable Metrics/MethodLength
454
- # rubocop:enable Metrics/AbcSize
455
480
 
456
481
  def insure_same_captures(enum, what)
457
482
  return to_enum __method__, enum, what unless block_given?
@@ -564,29 +589,18 @@ module RuboCop
564
589
  end
565
590
  end
566
591
 
567
- def compile_wildcard(name)
568
- if name.empty?
569
- 'true'
570
- elsif @unify.key?(name)
571
- # we have already seen a wildcard with this name before
572
- # so the value it matched the first time will already be stored
573
- # in a temp. check if this value matches the one stored in the temp
574
- "#{CUR_ELEMENT} == #{access_unify(name)}"
575
- else
576
- n = @unify[name] = "unify_#{name.gsub('-', '__')}"
577
- # double assign to avoid "assigned but unused variable"
578
- "(#{n} = #{CUR_ELEMENT}; " \
579
- "#{n} = #{n}; true)"
580
- end
581
- end
592
+ # Known wildcards are considered atoms, see `compile_atom`
593
+ def compile_new_wildcard(name)
594
+ return 'true' if name.empty?
582
595
 
583
- def compile_literal(literal)
584
- "#{CUR_ELEMENT} == #{literal}"
596
+ n = @unify[name] = "unify_#{name.gsub('-', '__')}"
597
+ # double assign to avoid "assigned but unused variable"
598
+ "(#{n} = #{CUR_ELEMENT}; #{n} = #{n}; true)"
585
599
  end
586
600
 
587
601
  def compile_predicate(predicate)
588
602
  if predicate.end_with?('(') # is there an arglist?
589
- args = compile_args(tokens)
603
+ args = compile_args
590
604
  predicate = predicate[0..-2] # drop the trailing (
591
605
  "#{CUR_ELEMENT}.#{predicate}(#{args.join(',')})"
592
606
  else
@@ -599,7 +613,7 @@ module RuboCop
599
613
  # code is used in. pass target value as an argument
600
614
  method = method[1..-1] # drop the leading #
601
615
  if method.end_with?('(') # is there an arglist?
602
- args = compile_args(tokens)
616
+ args = compile_args
603
617
  method = method[0..-2] # drop the trailing (
604
618
  "#{method}(#{CUR_ELEMENT},#{args.join(',')})"
605
619
  else
@@ -611,33 +625,44 @@ module RuboCop
611
625
  "#{compile_guard_clause} && #{CUR_NODE}.#{type.tr('-', '_')}_type?"
612
626
  end
613
627
 
614
- def compile_param(number)
615
- "#{CUR_ELEMENT} == #{get_param(number)}"
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
616
634
  end
617
635
 
618
- def compile_args(tokens)
619
- index = tokens.find_index { |token| token == ')' }
620
-
621
- tokens.slice!(0..index).each_with_object([]) do |token, args|
622
- next if [')', ','].include?(token)
636
+ def atom_to_expr(atom)
637
+ "#{atom} === #{CUR_ELEMENT}"
638
+ end
623
639
 
624
- args << compile_arg(token)
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}}"
625
644
  end
626
645
  end
627
646
 
628
- def compile_arg(token)
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)
629
650
  case token
630
- when WILDCARD then
631
- name = token[1..-1]
632
- access_unify(name) || fail_due_to('invalid in arglist: ' + token)
651
+ when WILDCARD then access_unify(token[1..-1]) # could be nil
633
652
  when LITERAL then token
653
+ when KEYWORD then get_keyword(token[1..-1])
654
+ when CONST then get_const(token[1..-1])
634
655
  when PARAM then get_param(token[1..-1])
635
656
  when CLOSING then fail_due_to("#{token} in invalid position")
636
657
  when nil then fail_due_to('pattern ended prematurely')
637
- else fail_due_to("invalid token in arglist: #{token.inspect}")
638
658
  end
639
659
  end
640
660
 
661
+ def compile_arg
662
+ token = tokens.shift
663
+ compile_atom(token) || expr_to_atom(compile_expr(token))
664
+ end
665
+
641
666
  def next_capture
642
667
  index = @captures
643
668
  @captures += 1
@@ -650,6 +675,15 @@ module RuboCop
650
675
  number.zero? ? @root : "param#{number}"
651
676
  end
652
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
+
653
687
  def emit_yield_capture(when_no_capture = '')
654
688
  yield_val = if @captures.zero?
655
689
  when_no_capture
@@ -675,9 +709,15 @@ module RuboCop
675
709
  (1..@params).map { |n| "param#{n}" }.join(',')
676
710
  end
677
711
 
678
- def emit_trailing_params
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)
679
718
  params = emit_param_list
680
- params.empty? ? '' : ",#{params}"
719
+ keywords = emit_keyword_list(forwarding: forwarding)
720
+ [*first, params, keywords].reject(&:empty?).join(',')
681
721
  end
682
722
 
683
723
  def emit_method_code
@@ -712,7 +752,7 @@ module RuboCop
712
752
  end
713
753
 
714
754
  def auto_use_temp_node?(code)
715
- code.scan(CUR_PLACEHOLDER).count > 1
755
+ code.match?(MULTIPLE_CUR_PLACEHOLDER)
716
756
  end
717
757
 
718
758
  # with_<...>_context methods are used whenever the context,
@@ -751,72 +791,59 @@ module RuboCop
751
791
  end
752
792
 
753
793
  def self.tokens(pattern)
754
- pattern.scan(TOKEN).reject { |token| token =~ /\A#{SEPARATORS}\Z/ }
794
+ pattern.gsub(COMMENT, '').scan(TOKEN).grep_v(ONLY_SEPARATOR)
755
795
  end
756
- end
757
- private_constant :Compiler
758
-
759
- # Helpers for defining methods based on a pattern string
760
- module Macros
761
- # Define a method which applies a pattern to an AST node
762
- #
763
- # The new method will return nil if the node does not match
764
- # If the node matches, and a block is provided, the new method will
765
- # yield to the block (passing any captures as block arguments).
766
- # If the node matches, and no block is provided, the new method will
767
- # return the captures, or `true` if there were none.
768
- def def_node_matcher(method_name, pattern_str)
769
- compiler = Compiler.new(pattern_str, 'node')
770
- src = "def #{method_name}(node = self" \
771
- "#{compiler.emit_trailing_params});" \
772
- "#{compiler.emit_method_code};end"
773
796
 
774
- location = caller_locations(1, 1).first
775
- class_eval(src, location.path, location.lineno)
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
776
802
  end
777
803
 
778
- # Define a method which recurses over the descendants of an AST node,
779
- # checking whether any of them match the provided pattern
780
- #
781
- # If the method name ends with '?', the new method will return `true`
782
- # as soon as it finds a descendant which matches. Otherwise, it will
783
- # yield all descendants which match.
784
- def def_node_search(method_name, pattern_str)
785
- compiler = Compiler.new(pattern_str, 'node')
786
- called_from = caller(1..1).first.split(':')
787
-
788
- if method_name.to_s.end_with?('?')
789
- node_search_first(method_name, compiler, called_from)
790
- else
791
- node_search_all(method_name, compiler, called_from)
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
792
810
  end
811
+ src = yield method_name
812
+ base.class_eval(src, location.path, location.lineno)
793
813
  end
794
814
 
795
- def node_search_first(method_name, compiler, called_from)
796
- node_search(method_name, compiler, 'return true', '', called_from)
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
797
823
  end
798
824
 
799
- def node_search_all(method_name, compiler, called_from)
800
- yield_code = compiler.emit_yield_capture('node')
801
- prelude = "return enum_for(:#{method_name}, node0" \
802
- "#{compiler.emit_trailing_params}) unless block_given?"
803
-
804
- node_search(method_name, compiler, yield_code, prelude, called_from)
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
805
829
  end
806
830
 
807
- def node_search(method_name, compiler, on_match, prelude, called_from)
808
- src = node_search_body(method_name, compiler.emit_trailing_params,
809
- prelude, compiler.match_code, on_match)
810
- filename, lineno = *called_from
811
- class_eval(src, filename, lineno.to_i)
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)
812
840
  end
813
841
 
814
- def node_search_body(method_name, trailing_params, prelude, match_code,
815
- on_match)
842
+ def emit_node_search_body(method_name, prelude:, on_match:)
816
843
  <<~RUBY
817
- def #{method_name}(node0#{trailing_params})
844
+ def #{method_name}(#{emit_params(@root)})
818
845
  #{prelude}
819
- node0.each_node do |node|
846
+ #{@root}.each_node do |#{@node_var}|
820
847
  if #{match_code}
821
848
  #{on_match}
822
849
  end
@@ -826,22 +853,53 @@ module RuboCop
826
853
  RUBY
827
854
  end
828
855
  end
856
+ private_constant :Compiler
857
+
858
+ # Helpers for defining methods based on a pattern string
859
+ module Macros
860
+ # Define a method which applies a pattern to an AST node
861
+ #
862
+ # The new method will return nil if the node does not match
863
+ # If the node matches, and a block is provided, the new method will
864
+ # yield to the block (passing any captures as block arguments).
865
+ # If the node matches, and no block is provided, the new method will
866
+ # return the captures, or `true` if there were none.
867
+ 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)
870
+ end
871
+
872
+ # Define a method which recurses over the descendants of an AST node,
873
+ # checking whether any of them match the provided pattern
874
+ #
875
+ # If the method name ends with '?', the new method will return `true`
876
+ # as soon as it finds a descendant which matches. Otherwise, it will
877
+ # yield all descendants which match.
878
+ 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)
881
+ end
882
+ end
829
883
 
830
884
  attr_reader :pattern
831
885
 
832
886
  def initialize(str)
833
887
  @pattern = str
834
- compiler = Compiler.new(str)
835
- src = "def match(node0#{compiler.emit_trailing_params});" \
888
+ compiler = Compiler.new(str, 'node0')
889
+ src = "def match(#{compiler.emit_params('node0')});" \
836
890
  "#{compiler.emit_method_code}end"
837
891
  instance_eval(src, __FILE__, __LINE__ + 1)
838
892
  end
839
893
 
840
- def match(*args)
894
+ def match(*args, **rest)
841
895
  # If we're here, it's because the singleton method has not been defined,
842
896
  # either because we've been dup'ed or serialized through YAML
843
897
  initialize(pattern)
844
- match(*args)
898
+ if rest.empty?
899
+ match(*args)
900
+ else
901
+ match(*args, **rest)
902
+ end
845
903
  end
846
904
 
847
905
  def marshal_load(pattern)
@@ -877,6 +935,17 @@ module RuboCop
877
935
 
878
936
  nil
879
937
  end
938
+
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
948
+ end
880
949
  end
881
950
  end
882
951
  end