rubocop-ast 0.1.0 → 0.4.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -5
  3. data/lib/rubocop/ast.rb +5 -1
  4. data/lib/rubocop/ast/builder.rb +5 -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 +63 -8
  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 +4 -23
  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/hash_node.rb +21 -8
  18. data/lib/rubocop/ast/node/if_node.rb +7 -14
  19. data/lib/rubocop/ast/node/index_node.rb +5 -3
  20. data/lib/rubocop/ast/node/indexasgn_node.rb +5 -3
  21. data/lib/rubocop/ast/node/int_node.rb +1 -0
  22. data/lib/rubocop/ast/node/lambda_node.rb +10 -3
  23. data/lib/rubocop/ast/node/mixin/method_dispatch_node.rb +0 -7
  24. data/lib/rubocop/ast/node/mixin/parameterized_node.rb +55 -0
  25. data/lib/rubocop/ast/node/next_node.rb +12 -0
  26. data/lib/rubocop/ast/node/pair_node.rb +2 -2
  27. data/lib/rubocop/ast/node/resbody_node.rb +21 -0
  28. data/lib/rubocop/ast/node/rescue_node.rb +49 -0
  29. data/lib/rubocop/ast/node/return_node.rb +1 -13
  30. data/lib/rubocop/ast/node/send_node.rb +7 -1
  31. data/lib/rubocop/ast/node/super_node.rb +2 -0
  32. data/lib/rubocop/ast/node/when_node.rb +3 -9
  33. data/lib/rubocop/ast/node/yield_node.rb +2 -0
  34. data/lib/rubocop/ast/node_pattern.rb +99 -86
  35. data/lib/rubocop/ast/processed_source.rb +94 -16
  36. data/lib/rubocop/ast/traversal.rb +2 -2
  37. data/lib/rubocop/ast/version.rb +1 -1
  38. metadata +12 -8
  39. data/lib/rubocop/ast/node/retry_node.rb +0 -17
@@ -6,13 +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
13
  [(send nil? ${:attr_reader :attr_writer :attr_accessor :attr} $...)
14
14
  (_ _ _ _ ...)]
15
15
  PATTERN
16
+
17
+ private
18
+
19
+ def first_argument_index
20
+ 2
21
+ end
16
22
  end
17
23
  end
18
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
@@ -84,7 +84,7 @@ module RuboCop
84
84
  # # matching process starts
85
85
  # '(send _ %named)' # arguments can also be passed as named
86
86
  # # parameters (see `%1`)
87
- # # Note that the macros `def_node_pattern` and
87
+ # # Note that the macros `def_node_matcher` and
88
88
  # # `def_node_search` accept default values for these.
89
89
  # '(send _ %CONST)' # the named constant will act like `%1` and `%named`.
90
90
  # '^^send' # each ^ ascends one level in the AST
@@ -96,6 +96,10 @@ module RuboCop
96
96
  # # if that returns a truthy value, the match succeeds
97
97
  # 'equal?(%1)' # predicates can be given 1 or more extra args
98
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
99
103
  #
100
104
  # You can nest arbitrarily deep:
101
105
  #
@@ -110,11 +114,6 @@ module RuboCop
110
114
  # and so on. Therefore, if you add methods which are named like
111
115
  # `#prefix_type?` to the AST node class, then 'prefix' will become usable as
112
116
  # a pattern.
113
- #
114
- # Also note that if you need a "guard clause" to protect against possible nils
115
- # in a certain place in the AST, you can do it like this: `[!nil <pattern>]`
116
- #
117
- # The compiler code is very simple; don't be afraid to read through it!
118
117
  class NodePattern
119
118
  # @private
120
119
  Invalid = Class.new(StandardError)
@@ -124,6 +123,8 @@ module RuboCop
124
123
  class Compiler
125
124
  SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze
126
125
  IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze
126
+ COMMENT = /#\s.*$/.freeze
127
+
127
128
  META = Regexp.union(
128
129
  %w"( ) { } [ ] $< < > $... $ ! ^ ` ... + * ?"
129
130
  ).freeze
@@ -135,8 +136,10 @@ module RuboCop
135
136
  PARAM_NUMBER = /%\d*/.freeze
136
137
 
137
138
  SEPARATORS = /\s+/.freeze
138
- TOKENS = Regexp.union(META, PARAM_CONST, KEYWORD_NAME, PARAM_NUMBER, NUMBER,
139
- METHOD_NAME, SYMBOL, STRING)
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)
140
143
 
141
144
  TOKEN = /\G(?:#{SEPARATORS}|#{TOKENS}|.)/.freeze
142
145
 
@@ -163,6 +166,7 @@ module RuboCop
163
166
  CUR_NODE = "#{CUR_PLACEHOLDER} node@@@"
164
167
  CUR_ELEMENT = "#{CUR_PLACEHOLDER} element@@@"
165
168
  SEQ_HEAD_GUARD = '@@@seq guard head@@@'
169
+ MULTIPLE_CUR_PLACEHOLDER = /#{CUR_PLACEHOLDER}.*#{CUR_PLACEHOLDER}/.freeze
166
170
 
167
171
  line = __LINE__
168
172
  ANY_ORDER_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
@@ -199,22 +203,26 @@ module RuboCop
199
203
  RUBY
200
204
  REPEATED_TEMPLATE.location = [__FILE__, line + 1]
201
205
 
202
- def initialize(str, node_var = 'node0')
206
+ def initialize(str, root = 'node0', node_var = root)
203
207
  @string = str
204
- @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
205
213
 
206
214
  @temps = 0 # avoid name clashes between temp variables
207
215
  @captures = 0 # number of captures seen
208
216
  @unify = {} # named wildcard -> temp variable
209
217
  @params = 0 # highest % (param) number seen
210
218
  @keywords = Set[] # keyword parameters seen
211
- run(node_var)
219
+ run
212
220
  end
213
221
 
214
- def run(node_var)
222
+ def run
215
223
  @tokens = Compiler.tokens(@string)
216
224
 
217
- @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)
218
226
  @match_code.prepend("(captures = Array.new(#{@captures})) && ") \
219
227
  if @captures.positive?
220
228
 
@@ -234,6 +242,10 @@ module RuboCop
234
242
  # CUR_NODE: Ruby code that evaluates to an AST node
235
243
  # CUR_ELEMENT: Either the node or the type if in first element of
236
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
+
237
249
  case token
238
250
  when '(' then compile_seq
239
251
  when '{' then compile_union
@@ -242,16 +254,10 @@ module RuboCop
242
254
  when '$' then compile_capture
243
255
  when '^' then compile_ascend
244
256
  when '`' then compile_descend
245
- when WILDCARD then compile_wildcard(token[1..-1])
257
+ when WILDCARD then compile_new_wildcard(token[1..-1])
246
258
  when FUNCALL then compile_funcall(token)
247
- when LITERAL then compile_literal(token)
248
259
  when PREDICATE then compile_predicate(token)
249
260
  when NODE then compile_nodetype(token)
250
- when KEYWORD then compile_keyword(token[1..-1])
251
- when CONST then compile_const(token[1..-1])
252
- when PARAM then compile_param(token[1..-1])
253
- when CLOSING then fail_due_to("#{token} in invalid position")
254
- when nil then fail_due_to('pattern ended prematurely')
255
261
  else fail_due_to("invalid token #{token.inspect}")
256
262
  end
257
263
  end
@@ -260,7 +266,7 @@ module RuboCop
260
266
  def tokens_until(stop, what)
261
267
  return to_enum __method__, stop, what unless block_given?
262
268
 
263
- fail_due_to("empty #{what}") if tokens.first == stop && what
269
+ fail_due_to("empty #{what}") if tokens.first == stop
264
270
  yield until tokens.first == stop
265
271
  tokens.shift
266
272
  end
@@ -324,11 +330,15 @@ module RuboCop
324
330
  # @private
325
331
  # Builds Ruby code for a sequence
326
332
  # (head *first_terms variadic_term *last_terms)
327
- 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
+
328
338
  def initialize(compiler, *arity_term_list)
329
339
  @arities, @terms = arity_term_list.transpose
330
340
 
331
- super(compiler)
341
+ @compiler = compiler
332
342
  @variadic_index = @arities.find_index { |a| a.is_a?(Range) }
333
343
  fail_due_to 'multiple variable patterns in same sequence' \
334
344
  if @variadic_index && !@arities.one? { |a| a.is_a?(Range) }
@@ -448,7 +458,6 @@ module RuboCop
448
458
  [0..Float::INFINITY, 'true']
449
459
  end
450
460
 
451
- # rubocop:disable Metrics/AbcSize
452
461
  # rubocop:disable Metrics/MethodLength
453
462
  def compile_any_order(capture_all = nil)
454
463
  rest = capture_rest = nil
@@ -468,7 +477,6 @@ module RuboCop
468
477
  end
469
478
  end
470
479
  # rubocop:enable Metrics/MethodLength
471
- # rubocop:enable Metrics/AbcSize
472
480
 
473
481
  def insure_same_captures(enum, what)
474
482
  return to_enum __method__, enum, what unless block_given?
@@ -581,29 +589,18 @@ module RuboCop
581
589
  end
582
590
  end
583
591
 
584
- def compile_wildcard(name)
585
- if name.empty?
586
- 'true'
587
- elsif @unify.key?(name)
588
- # we have already seen a wildcard with this name before
589
- # so the value it matched the first time will already be stored
590
- # in a temp. check if this value matches the one stored in the temp
591
- "#{CUR_ELEMENT} == #{access_unify(name)}"
592
- else
593
- n = @unify[name] = "unify_#{name.gsub('-', '__')}"
594
- # double assign to avoid "assigned but unused variable"
595
- "(#{n} = #{CUR_ELEMENT}; " \
596
- "#{n} = #{n}; true)"
597
- end
598
- end
592
+ # Known wildcards are considered atoms, see `compile_atom`
593
+ def compile_new_wildcard(name)
594
+ return 'true' if name.empty?
599
595
 
600
- def compile_literal(literal)
601
- "#{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)"
602
599
  end
603
600
 
604
601
  def compile_predicate(predicate)
605
602
  if predicate.end_with?('(') # is there an arglist?
606
- args = compile_args(tokens)
603
+ args = compile_args
607
604
  predicate = predicate[0..-2] # drop the trailing (
608
605
  "#{CUR_ELEMENT}.#{predicate}(#{args.join(',')})"
609
606
  else
@@ -616,7 +613,7 @@ module RuboCop
616
613
  # code is used in. pass target value as an argument
617
614
  method = method[1..-1] # drop the leading #
618
615
  if method.end_with?('(') # is there an arglist?
619
- args = compile_args(tokens)
616
+ args = compile_args
620
617
  method = method[0..-2] # drop the trailing (
621
618
  "#{method}(#{CUR_ELEMENT},#{args.join(',')})"
622
619
  else
@@ -628,43 +625,44 @@ module RuboCop
628
625
  "#{compile_guard_clause} && #{CUR_NODE}.#{type.tr('-', '_')}_type?"
629
626
  end
630
627
 
631
- def compile_param(number)
632
- "#{get_param(number)} === #{CUR_ELEMENT}"
633
- end
634
-
635
- def compile_const(const)
636
- "#{get_const(const)} === #{CUR_ELEMENT}"
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
637
634
  end
638
635
 
639
- def compile_keyword(keyword)
640
- "#{get_keyword(keyword)} === #{CUR_ELEMENT}"
636
+ def atom_to_expr(atom)
637
+ "#{atom} === #{CUR_ELEMENT}"
641
638
  end
642
639
 
643
- def compile_args(tokens)
644
- index = tokens.find_index { |token| token == ')' }
645
-
646
- tokens.slice!(0..index).each_with_object([]) do |token, args|
647
- next if [')', ','].include?(token)
648
-
649
- 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}}"
650
644
  end
651
645
  end
652
646
 
653
- def compile_arg(token)
654
- name = token[1..-1]
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)
655
650
  case token
656
- when WILDCARD
657
- access_unify(name) || fail_due_to('invalid in arglist: ' + token)
651
+ when WILDCARD then access_unify(token[1..-1]) # could be nil
658
652
  when LITERAL then token
659
- when KEYWORD then get_keyword(name)
660
- when CONST then get_const(name)
661
- when PARAM then get_param(name)
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])
662
656
  when CLOSING then fail_due_to("#{token} in invalid position")
663
657
  when nil then fail_due_to('pattern ended prematurely')
664
- else fail_due_to("invalid token in arglist: #{token.inspect}")
665
658
  end
666
659
  end
667
660
 
661
+ def compile_arg
662
+ token = tokens.shift
663
+ compile_atom(token) || expr_to_atom(compile_expr(token))
664
+ end
665
+
668
666
  def next_capture
669
667
  index = @captures
670
668
  @captures += 1
@@ -716,10 +714,10 @@ module RuboCop
716
714
  @keywords.map { |k| format(pattern, keyword: k) }.join(',')
717
715
  end
718
716
 
719
- def emit_trailing_params(forwarding: false)
717
+ def emit_params(*first, forwarding: false)
720
718
  params = emit_param_list
721
719
  keywords = emit_keyword_list(forwarding: forwarding)
722
- [params, keywords].reject(&:empty?).map { |p| ", #{p}" }.join
720
+ [*first, params, keywords].reject(&:empty?).join(',')
723
721
  end
724
722
 
725
723
  def emit_method_code
@@ -754,7 +752,7 @@ module RuboCop
754
752
  end
755
753
 
756
754
  def auto_use_temp_node?(code)
757
- code.scan(CUR_PLACEHOLDER).count > 1
755
+ code.match?(MULTIPLE_CUR_PLACEHOLDER)
758
756
  end
759
757
 
760
758
  # with_<...>_context methods are used whenever the context,
@@ -793,16 +791,22 @@ module RuboCop
793
791
  end
794
792
 
795
793
  def self.tokens(pattern)
796
- pattern.scan(TOKEN).reject { |token| token =~ /\A#{SEPARATORS}\Z/ }
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
797
802
  end
798
803
 
799
804
  def def_helper(base, method_name, **defaults)
800
805
  location = caller_locations(3, 1).first
801
806
  unless defaults.empty?
802
- base.send :define_method, method_name do |*args, **values|
803
- send method_name, *args, **defaults, **values
804
- end
805
- method_name = :"without_defaults_#{method_name}"
807
+ call = :"without_defaults_#{method_name}"
808
+ base.send :define_method, method_name, &wrapping_block(call, **defaults)
809
+ method_name = call
806
810
  end
807
811
  src = yield method_name
808
812
  base.class_eval(src, location.path, location.lineno)
@@ -811,7 +815,7 @@ module RuboCop
811
815
  def def_node_matcher(base, method_name, **defaults)
812
816
  def_helper(base, method_name, **defaults) do |name|
813
817
  <<~RUBY
814
- def #{name}(node = self#{emit_trailing_params})
818
+ def #{name}(#{emit_params('node = self')})
815
819
  #{emit_method_code}
816
820
  end
817
821
  RUBY
@@ -828,20 +832,18 @@ module RuboCop
828
832
  if method_name.to_s.end_with?('?')
829
833
  on_match = 'return true'
830
834
  else
831
- prelude = <<~RUBY
832
- return enum_for(:#{method_name},
833
- node0#{emit_trailing_params(forwarding: true)}) unless block_given?
834
- RUBY
835
- on_match = emit_yield_capture('node')
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)
836
838
  end
837
839
  emit_node_search_body(method_name, prelude: prelude, on_match: on_match)
838
840
  end
839
841
 
840
842
  def emit_node_search_body(method_name, prelude:, on_match:)
841
843
  <<~RUBY
842
- def #{method_name}(node0#{emit_trailing_params})
844
+ def #{method_name}(#{emit_params(@root)})
843
845
  #{prelude}
844
- node0.each_node do |node|
846
+ #{@root}.each_node do |#{@node_var}|
845
847
  if #{match_code}
846
848
  #{on_match}
847
849
  end
@@ -874,7 +876,7 @@ module RuboCop
874
876
  # as soon as it finds a descendant which matches. Otherwise, it will
875
877
  # yield all descendants which match.
876
878
  def def_node_search(method_name, pattern_str, **keyword_defaults)
877
- Compiler.new(pattern_str, 'node')
879
+ Compiler.new(pattern_str, 'node0', 'node')
878
880
  .def_node_search(self, method_name, **keyword_defaults)
879
881
  end
880
882
  end
@@ -883,8 +885,8 @@ module RuboCop
883
885
 
884
886
  def initialize(str)
885
887
  @pattern = str
886
- compiler = Compiler.new(str)
887
- src = "def match(node0#{compiler.emit_trailing_params});" \
888
+ compiler = Compiler.new(str, 'node0')
889
+ src = "def match(#{compiler.emit_params('node0')});" \
888
890
  "#{compiler.emit_method_code}end"
889
891
  instance_eval(src, __FILE__, __LINE__ + 1)
890
892
  end
@@ -933,6 +935,17 @@ module RuboCop
933
935
 
934
936
  nil
935
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
936
949
  end
937
950
  end
938
951
  end