rubocop-ast 0.1.0 → 0.4.2

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