rubocop-ast 0.0.3 → 0.4.1

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