rubocop 0.67.2 → 0.68.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +86 -233
  4. data/exe/rubocop +0 -12
  5. data/lib/rubocop.rb +13 -30
  6. data/lib/rubocop/ast/builder.rb +4 -0
  7. data/lib/rubocop/ast/node/alias_node.rb +24 -0
  8. data/lib/rubocop/ast/node/class_node.rb +31 -0
  9. data/lib/rubocop/ast/node/module_node.rb +24 -0
  10. data/lib/rubocop/ast/node/range_node.rb +7 -0
  11. data/lib/rubocop/ast/node/resbody_node.rb +12 -0
  12. data/lib/rubocop/ast/node/self_class_node.rb +24 -0
  13. data/lib/rubocop/cli.rb +40 -4
  14. data/lib/rubocop/config.rb +9 -7
  15. data/lib/rubocop/config_loader.rb +48 -7
  16. data/lib/rubocop/config_loader_resolver.rb +5 -4
  17. data/lib/rubocop/cop/commissioner.rb +24 -0
  18. data/lib/rubocop/cop/correctors/unused_arg_corrector.rb +18 -6
  19. data/lib/rubocop/cop/internal_affairs/node_destructuring.rb +12 -14
  20. data/lib/rubocop/cop/layout/access_modifier_indentation.rb +9 -20
  21. data/lib/rubocop/cop/layout/align_arguments.rb +93 -0
  22. data/lib/rubocop/cop/layout/align_parameters.rb +57 -33
  23. data/lib/rubocop/cop/layout/class_structure.rb +5 -5
  24. data/lib/rubocop/cop/layout/empty_lines_around_access_modifier.rb +6 -8
  25. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +3 -6
  26. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +1 -2
  27. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +1 -0
  28. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +292 -0
  29. data/lib/rubocop/cop/layout/{first_parameter_indentation.rb → indent_first_argument.rb} +11 -12
  30. data/lib/rubocop/cop/layout/{indent_array.rb → indent_first_array_element.rb} +2 -2
  31. data/lib/rubocop/cop/layout/{indent_hash.rb → indent_first_hash_element.rb} +2 -2
  32. data/lib/rubocop/cop/layout/indent_first_parameter.rb +96 -0
  33. data/lib/rubocop/cop/layout/indentation_width.rb +4 -16
  34. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +2 -4
  35. data/lib/rubocop/cop/layout/space_in_lambda_literal.rb +1 -16
  36. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +1 -2
  37. data/lib/rubocop/cop/lint/duplicate_methods.rb +6 -8
  38. data/lib/rubocop/cop/lint/format_parameter_mismatch.rb +4 -8
  39. data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +157 -0
  40. data/lib/rubocop/cop/lint/inherit_exception.rb +3 -4
  41. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +18 -1
  42. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +3 -5
  43. data/lib/rubocop/cop/lint/underscore_prefixed_variable_name.rb +25 -5
  44. data/lib/rubocop/cop/lint/useless_assignment.rb +2 -6
  45. data/lib/rubocop/cop/lint/useless_setter_call.rb +1 -2
  46. data/lib/rubocop/cop/message_annotator.rb +1 -0
  47. data/lib/rubocop/cop/metrics/line_length.rb +139 -28
  48. data/lib/rubocop/cop/metrics/perceived_complexity.rb +3 -4
  49. data/lib/rubocop/cop/mixin/check_line_breakable.rb +190 -0
  50. data/lib/rubocop/cop/mixin/{array_hash_indentation.rb → multiline_element_indentation.rb} +3 -2
  51. data/lib/rubocop/cop/mixin/too_many_lines.rb +3 -7
  52. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +33 -4
  53. data/lib/rubocop/cop/rails/active_record_override.rb +23 -8
  54. data/lib/rubocop/cop/rails/delegate.rb +5 -8
  55. data/lib/rubocop/cop/rails/environment_comparison.rb +5 -3
  56. data/lib/rubocop/cop/rails/find_each.rb +1 -1
  57. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +3 -3
  58. data/lib/rubocop/cop/rails/reflection_class_name.rb +1 -1
  59. data/lib/rubocop/cop/rails/skips_model_validations.rb +6 -7
  60. data/lib/rubocop/cop/rails/time_zone.rb +3 -10
  61. data/lib/rubocop/cop/rails/validation.rb +3 -0
  62. data/lib/rubocop/cop/registry.rb +3 -3
  63. data/lib/rubocop/cop/style/alias.rb +13 -7
  64. data/lib/rubocop/cop/style/block_delimiters.rb +20 -0
  65. data/lib/rubocop/cop/style/class_and_module_children.rb +19 -21
  66. data/lib/rubocop/cop/style/class_methods.rb +16 -24
  67. data/lib/rubocop/cop/style/conditional_assignment.rb +20 -49
  68. data/lib/rubocop/cop/style/documentation.rb +3 -7
  69. data/lib/rubocop/cop/style/format_string.rb +18 -21
  70. data/lib/rubocop/cop/style/hash_syntax.rb +1 -1
  71. data/lib/rubocop/cop/style/inverse_methods.rb +4 -0
  72. data/lib/rubocop/cop/style/lambda.rb +12 -8
  73. data/lib/rubocop/cop/style/mixin_grouping.rb +8 -10
  74. data/lib/rubocop/cop/style/module_function.rb +2 -3
  75. data/lib/rubocop/cop/style/next.rb +10 -14
  76. data/lib/rubocop/cop/style/one_line_conditional.rb +5 -3
  77. data/lib/rubocop/cop/style/optional_arguments.rb +1 -4
  78. data/lib/rubocop/cop/style/random_with_offset.rb +44 -47
  79. data/lib/rubocop/cop/style/redundant_return.rb +6 -14
  80. data/lib/rubocop/cop/style/redundant_sort_by.rb +1 -1
  81. data/lib/rubocop/cop/style/safe_navigation.rb +3 -0
  82. data/lib/rubocop/cop/style/struct_inheritance.rb +2 -3
  83. data/lib/rubocop/cop/style/symbol_proc.rb +20 -40
  84. data/lib/rubocop/cop/style/unless_else.rb +1 -2
  85. data/lib/rubocop/cop/style/yoda_condition.rb +8 -7
  86. data/lib/rubocop/cop/util.rb +2 -4
  87. data/lib/rubocop/file_finder.rb +5 -10
  88. data/lib/rubocop/formatter/disabled_config_formatter.rb +5 -0
  89. data/lib/rubocop/node_pattern.rb +304 -170
  90. data/lib/rubocop/options.rb +4 -1
  91. data/lib/rubocop/rspec/shared_contexts.rb +3 -0
  92. data/lib/rubocop/version.rb +1 -1
  93. data/lib/rubocop/yaml_duplication_checker.rb +1 -1
  94. metadata +26 -50
  95. data/lib/rubocop/cop/performance/caller.rb +0 -69
  96. data/lib/rubocop/cop/performance/case_when_splat.rb +0 -177
  97. data/lib/rubocop/cop/performance/casecmp.rb +0 -108
  98. data/lib/rubocop/cop/performance/chain_array_allocation.rb +0 -78
  99. data/lib/rubocop/cop/performance/compare_with_block.rb +0 -122
  100. data/lib/rubocop/cop/performance/count.rb +0 -102
  101. data/lib/rubocop/cop/performance/detect.rb +0 -110
  102. data/lib/rubocop/cop/performance/double_start_end_with.rb +0 -94
  103. data/lib/rubocop/cop/performance/end_with.rb +0 -56
  104. data/lib/rubocop/cop/performance/fixed_size.rb +0 -97
  105. data/lib/rubocop/cop/performance/flat_map.rb +0 -78
  106. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +0 -99
  107. data/lib/rubocop/cop/performance/open_struct.rb +0 -46
  108. data/lib/rubocop/cop/performance/range_include.rb +0 -50
  109. data/lib/rubocop/cop/performance/redundant_block_call.rb +0 -93
  110. data/lib/rubocop/cop/performance/redundant_match.rb +0 -56
  111. data/lib/rubocop/cop/performance/redundant_merge.rb +0 -183
  112. data/lib/rubocop/cop/performance/regexp_match.rb +0 -265
  113. data/lib/rubocop/cop/performance/reverse_each.rb +0 -42
  114. data/lib/rubocop/cop/performance/size.rb +0 -77
  115. data/lib/rubocop/cop/performance/start_with.rb +0 -59
  116. data/lib/rubocop/cop/performance/string_replacement.rb +0 -173
  117. data/lib/rubocop/cop/performance/times_map.rb +0 -71
  118. data/lib/rubocop/cop/performance/unfreeze_string.rb +0 -50
  119. data/lib/rubocop/cop/performance/uri_default_parser.rb +0 -47
@@ -90,6 +90,8 @@ module RuboCop
90
90
 
91
91
  # rubocop:disable Metrics/CyclomaticComplexity
92
92
  def check_branch(node)
93
+ return unless node
94
+
93
95
  case node.type
94
96
  when :return then check_return_node(node)
95
97
  when :case then check_case_node(node)
@@ -111,25 +113,15 @@ module RuboCop
111
113
  end
112
114
 
113
115
  def check_case_node(node)
114
- _cond, *when_nodes, else_node = *node
115
- when_nodes.each { |when_node| check_when_node(when_node) }
116
- check_branch(else_node) if else_node
117
- end
118
-
119
- def check_when_node(node)
120
- return unless node
121
-
122
- _cond, body = *node
123
- check_branch(body) if body
116
+ node.when_branches.each { |when_node| check_branch(when_node.body) }
117
+ check_branch(node.else_branch)
124
118
  end
125
119
 
126
120
  def check_if_node(node)
127
121
  return if node.modifier_form? || node.ternary?
128
122
 
129
- _cond, if_node, else_node = *node.node_parts
130
-
131
- check_branch(if_node) if if_node
132
- check_branch(else_node) if else_node
123
+ check_branch(node.if_branch)
124
+ check_branch(node.else_branch)
133
125
  end
134
126
 
135
127
  def check_rescue_node(node)
@@ -35,7 +35,7 @@ module RuboCop
35
35
  end
36
36
 
37
37
  def autocorrect(node)
38
- send, = *node
38
+ send = node.send_node
39
39
  ->(corrector) { corrector.replace(sort_by_range(send, node), 'sort') }
40
40
  end
41
41
 
@@ -65,6 +65,8 @@ module RuboCop
65
65
 
66
66
  MSG = 'Use safe navigation (`&.`) instead of checking if an object ' \
67
67
  'exists before calling the method.'.freeze
68
+ LOGIC_JUMP_KEYWORDS = %i[break fail next raise
69
+ return throw yield].freeze
68
70
 
69
71
  minimum_target_ruby_version 2.3
70
72
 
@@ -149,6 +151,7 @@ module RuboCop
149
151
 
150
152
  checked_variable, matching_receiver, method =
151
153
  extract_common_parts(receiver, variable)
154
+ matching_receiver = nil if LOGIC_JUMP_KEYWORDS.include?(receiver.type)
152
155
  [checked_variable, matching_receiver, receiver, method]
153
156
  end
154
157
 
@@ -24,10 +24,9 @@ module RuboCop
24
24
  'Use a block to customize the struct.'.freeze
25
25
 
26
26
  def on_class(node)
27
- _name, superclass, _body = *node
28
- return unless struct_constructor?(superclass)
27
+ return unless struct_constructor?(node.parent_class)
29
28
 
30
- add_offense(node, location: superclass.source_range)
29
+ add_offense(node, location: node.parent_class.source_range)
31
30
  end
32
31
 
33
32
  def_node_matcher :struct_constructor?, <<-PATTERN
@@ -32,46 +32,38 @@ module RuboCop
32
32
  end
33
33
 
34
34
  def on_block(node)
35
- symbol_proc?(node) do |send_or_super, block_args, method|
36
- block_method_name = resolve_block_method_name(send_or_super)
37
-
35
+ symbol_proc?(node) do |dispatch_node, arguments_node, method_name|
38
36
  # TODO: Rails-specific handling that we should probably make
39
37
  # configurable - https://github.com/rubocop-hq/rubocop/issues/1485
40
38
  # we should ignore lambdas & procs
41
- return if proc_node?(send_or_super)
42
- return if %i[lambda proc].include?(block_method_name)
43
- return if ignored_method?(block_method_name)
44
- return if block_args.children.size == 1 &&
45
- block_args.source.include?(',')
39
+ return if proc_node?(dispatch_node)
40
+ return if %i[lambda proc].include?(dispatch_node.method_name)
41
+ return if ignored_method?(dispatch_node.method_name)
42
+ return if destructuring_block_argument?(arguments_node)
46
43
 
47
- offense(node, method, block_method_name)
44
+ register_offense(node, method_name, dispatch_node.method_name)
48
45
  end
49
46
  end
50
47
 
48
+ def destructuring_block_argument?(argument_node)
49
+ argument_node.one? && argument_node.source.include?(',')
50
+ end
51
+
51
52
  def autocorrect(node)
52
53
  lambda do |corrector|
53
- block_send_or_super, _block_args, block_body = *node
54
- _receiver, method_name, _args = *block_body
55
-
56
- if super?(block_send_or_super)
57
- args = *block_send_or_super
54
+ if node.send_node.arguments?
55
+ autocorrect_with_args(corrector, node,
56
+ node.send_node.arguments,
57
+ node.body.method_name)
58
58
  else
59
- _breceiver, _bmethod_name, *args = *block_send_or_super
59
+ autocorrect_without_args(corrector, node)
60
60
  end
61
- autocorrect_method(corrector, node, args, method_name)
62
61
  end
63
62
  end
64
63
 
65
64
  private
66
65
 
67
- def resolve_block_method_name(block_send_or_super)
68
- return :super if super?(block_send_or_super)
69
-
70
- _receiver, method_name, _args = *block_send_or_super
71
- method_name
72
- end
73
-
74
- def offense(node, method_name, block_method_name)
66
+ def register_offense(node, method_name, block_method_name)
75
67
  block_start = node.loc.begin.begin_pos
76
68
  block_end = node.loc.end.end_pos
77
69
  range = range_between(block_start, block_end)
@@ -83,16 +75,9 @@ module RuboCop
83
75
  block_method: block_method_name))
84
76
  end
85
77
 
86
- def autocorrect_method(corrector, node, args, method_name)
87
- if args.empty?
88
- autocorrect_no_args(corrector, node, method_name)
89
- else
90
- autocorrect_with_args(corrector, node, args, method_name)
91
- end
92
- end
93
-
94
- def autocorrect_no_args(corrector, node, method_name)
95
- corrector.replace(block_range_with_space(node), "(&:#{method_name})")
78
+ def autocorrect_without_args(corrector, node)
79
+ corrector.replace(block_range_with_space(node),
80
+ "(&:#{node.body.method_name})")
96
81
  end
97
82
 
98
83
  def autocorrect_with_args(corrector, node, args, method_name)
@@ -111,8 +96,7 @@ module RuboCop
111
96
  end
112
97
 
113
98
  def begin_pos_for_replacement(node)
114
- block_send_or_super, _block_args, _block_body = *node
115
- expr = block_send_or_super.source_range
99
+ expr = node.send_node.source_range
116
100
 
117
101
  if (paren_pos = (expr.source =~ /\(\s*\)$/))
118
102
  expr.begin_pos + paren_pos
@@ -120,10 +104,6 @@ module RuboCop
120
104
  node.loc.begin.begin_pos
121
105
  end
122
106
  end
123
-
124
- def super?(node)
125
- SUPER_TYPES.include?(node.type)
126
- end
127
107
  end
128
108
  end
129
109
  end
@@ -32,8 +32,7 @@ module RuboCop
32
32
  end
33
33
 
34
34
  def autocorrect(node)
35
- condition, = *node
36
- body_range = range_between_condition_and_else(node, condition)
35
+ body_range = range_between_condition_and_else(node, node.condition)
37
36
  else_range = range_between_else_and_end(node)
38
37
 
39
38
  lambda do |corrector|
@@ -98,7 +98,8 @@ module RuboCop
98
98
  end
99
99
 
100
100
  def valid_yoda?(node)
101
- lhs, _operator, rhs = *node
101
+ lhs = node.receiver
102
+ rhs = node.first_argument
102
103
 
103
104
  return true if lhs.literal? && rhs.literal? ||
104
105
  !lhs.literal? && !rhs.literal?
@@ -111,8 +112,10 @@ module RuboCop
111
112
  end
112
113
 
113
114
  def corrected_code(node)
114
- lhs, operator, rhs = *node
115
- "#{rhs.source} #{reverse_comparison(operator)} #{lhs.source}"
115
+ lhs = node.receiver
116
+ rhs = node.first_argument
117
+
118
+ "#{rhs.source} #{reverse_comparison(node.method_name)} #{lhs.source}"
116
119
  end
117
120
 
118
121
  def actual_code_range(node)
@@ -126,13 +129,11 @@ module RuboCop
126
129
  end
127
130
 
128
131
  def non_equality_operator?(node)
129
- _, operator, = *node
130
- !EQUALITY_OPERATORS.include?(operator)
132
+ !EQUALITY_OPERATORS.include?(node.method_name)
131
133
  end
132
134
 
133
135
  def noncommutative_operator?(node)
134
- _, operator, = *node
135
- NONCOMMUTATIVE_OPERATORS.include?(operator)
136
+ NONCOMMUTATIVE_OPERATORS.include?(node.method_name)
136
137
  end
137
138
  end
138
139
  end
@@ -45,11 +45,9 @@ module RuboCop
45
45
  while node
46
46
  case node.type
47
47
  when :send
48
- receiver, _method_name, _args = *node
49
- node = receiver
48
+ node = node.receiver
50
49
  when :block
51
- method, _args, _body = *node
52
- node = method
50
+ node = node.send_node
53
51
  else
54
52
  break
55
53
  end
@@ -13,16 +13,16 @@ module RuboCop
13
13
  @root_level == path.to_s
14
14
  end
15
15
 
16
- def find_file_upwards(filename, start_dir, use_home: false)
17
- traverse_files_upwards(filename, start_dir, use_home) do |file|
16
+ def find_file_upwards(filename, start_dir)
17
+ traverse_files_upwards(filename, start_dir) do |file|
18
18
  # minimize iteration for performance
19
19
  return file if file
20
20
  end
21
21
  end
22
22
 
23
- def find_files_upwards(filename, start_dir, use_home: false)
23
+ def find_files_upwards(filename, start_dir)
24
24
  files = []
25
- traverse_files_upwards(filename, start_dir, use_home) do |file|
25
+ traverse_files_upwards(filename, start_dir) do |file|
26
26
  files << file
27
27
  end
28
28
  files
@@ -30,18 +30,13 @@ module RuboCop
30
30
 
31
31
  private
32
32
 
33
- def traverse_files_upwards(filename, start_dir, use_home)
33
+ def traverse_files_upwards(filename, start_dir)
34
34
  Pathname.new(start_dir).expand_path.ascend do |dir|
35
35
  break if FileFinder.root_level?(dir)
36
36
 
37
37
  file = dir + filename
38
38
  yield(file.to_s) if file.exist?
39
39
  end
40
-
41
- return unless use_home && ENV.key?('HOME')
42
-
43
- file = File.join(Dir.home, filename)
44
- yield(file) if File.exist?(file)
45
40
  end
46
41
  end
47
42
  end
@@ -58,6 +58,11 @@ module RuboCop
58
58
 
59
59
  def command
60
60
  command = 'rubocop --auto-gen-config'
61
+
62
+ if @options[:auto_gen_only_exclude]
63
+ command += ' --auto-gen-only-exclude'
64
+ end
65
+
61
66
  if @exclude_limit_option
62
67
  command +=
63
68
  format(' --exclude-limit %<limit>d',
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'delegate'
4
+ require 'erb'
5
+
3
6
  # rubocop:disable Metrics/ClassLength, Metrics/CyclomaticComplexity
4
7
  module RuboCop
5
8
  # This class performs a pattern-matching operation on an AST node.
@@ -38,6 +41,12 @@ module RuboCop
38
41
  # '$(send const ...)' # arbitrary matching can be performed on a capture
39
42
  # '(send _recv _msg)' # wildcards can be named (for readability)
40
43
  # '(send ... :new)' # you can match against the last children
44
+ # '(array <str sym>)' # you can match children in any order. This
45
+ # # would match `['x', :y]` as well as `[:y, 'x']
46
+ # '(_ <str sym ...>)' # will match if arguments have at least a `str` and
47
+ # # a `sym` node, but can have more.
48
+ # '(array <$str $_>)' # captures are in the order of the pattern,
49
+ # # irrespective of the actual order of the children
41
50
  # '(send $...)' # capture all the children as an array
42
51
  # '(send $... int)' # capture all children but the last as an array
43
52
  # '(send _x :+ _x)' # unification is performed on named wildcards
@@ -97,7 +106,7 @@ module RuboCop
97
106
  class Compiler
98
107
  SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze
99
108
  IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze
100
- META = /\(|\)|\{|\}|\[|\]|\$\.\.\.|\$|!|\^|\.\.\./.freeze
109
+ META = Regexp.union(%w"( ) { } [ ] $< < > $... $ ! ^ ...").freeze
101
110
  NUMBER = /-?\d+(?:\.\d+)?/.freeze
102
111
  STRING = /".+?"/.freeze
103
112
  METHOD_NAME = /\#?#{IDENTIFIER}[\!\?]?\(?/.freeze
@@ -121,10 +130,36 @@ module RuboCop
121
130
  REST = '...'.freeze
122
131
  CAPTURED_REST = '$...'.freeze
123
132
 
124
- attr_reader :match_code
133
+ attr_reader :match_code, :tokens, :captures
125
134
 
126
135
  SEQ_HEAD_INDEX = -1
127
136
 
137
+ # Placeholders while compiling, see with_..._context methods
138
+ CUR_PLACEHOLDER = '@@@cur'.freeze
139
+ CUR_NODE = "#{CUR_PLACEHOLDER} node@@@".freeze
140
+ CUR_ELEMENT = "#{CUR_PLACEHOLDER} element@@@".freeze
141
+ SEQ_HEAD_GUARD = '@@@seq guard head@@@'.freeze
142
+
143
+ line = __LINE__
144
+ ANY_ORDER_TEMPLATE = ERB.new <<-RUBY.strip_indent.gsub("-%>\n", '%>')
145
+ <% if capture_rest %>(<%= capture_rest %> = []) && <% end -%>
146
+ <% if capture_all %>(<%= capture_all %> = <% end -%>
147
+ <%= CUR_NODE %>.children[<%= range %>]<% if capture_all %>)<% end -%>
148
+ .each_with_object({}) { |<%= child %>, <%= matched %>|
149
+ case
150
+ <% patterns.each_with_index do |pattern, i| -%>
151
+ when !<%= matched %>[<%= i %>] && <%=
152
+ with_context(pattern, child, use_temp_node: false)
153
+ %> then <%= matched %>[<%= i %>] = true
154
+ <% end -%>
155
+ <% if !rest %> else break({})
156
+ <% elsif capture_rest %> else <%= capture_rest %> << <%= child %>
157
+ <% end -%>
158
+ end
159
+ }.size == <%= patterns.size -%>
160
+ RUBY
161
+ ANY_ORDER_TEMPLATE.location = [__FILE__, line + 1]
162
+
128
163
  def initialize(str, node_var = 'node0')
129
164
  @string = str
130
165
  @root = node_var
@@ -133,40 +168,45 @@ module RuboCop
133
168
  @captures = 0 # number of captures seen
134
169
  @unify = {} # named wildcard -> temp variable number
135
170
  @params = 0 # highest % (param) number seen
136
-
137
171
  run(node_var)
138
172
  end
139
173
 
140
174
  def run(node_var)
141
- tokens = Compiler.tokens(@string)
175
+ @tokens = Compiler.tokens(@string)
142
176
 
143
- @match_code = compile_expr(tokens, node_var, false)
177
+ @match_code = with_context(compile_expr, node_var, use_temp_node: false)
178
+ @match_code.prepend("(captures = Array.new(#{@captures})) && ") \
179
+ if @captures > 0
144
180
 
145
181
  fail_due_to('unbalanced pattern') unless tokens.empty?
146
182
  end
147
183
 
148
184
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
149
- def compile_expr(tokens, cur_node, seq_head)
185
+ def compile_expr(token = tokens.shift)
150
186
  # read a single pattern-matching expression from the token stream,
151
187
  # return Ruby code which performs the corresponding matching operation
152
- # on 'cur_node' (which is Ruby code which evaluates to an AST node)
153
188
  #
154
189
  # the 'pattern-matching' expression may be a composite which
155
- # contains an arbitrary number of sub-expressions
156
- token = tokens.shift
190
+ # contains an arbitrary number of sub-expressions, but that composite
191
+ # must all have precedence higher or equal to that of `&&`
192
+ #
193
+ # Expressions may use placeholders like:
194
+ # CUR_NODE: Ruby code that evaluates to an AST node
195
+ # CUR_ELEMENT: Either the node or the type if in first element of
196
+ # a sequence (aka seq_head, e.g. "(seq_head first_node_arg ...")
157
197
  case token
158
- when '(' then compile_seq(tokens, cur_node, seq_head)
159
- when '{' then compile_union(tokens, cur_node, seq_head)
160
- when '[' then compile_intersect(tokens, cur_node, seq_head)
161
- when '!' then compile_negation(tokens, cur_node, seq_head)
162
- when '$' then compile_capture(tokens, cur_node, seq_head)
163
- when '^' then compile_ascend(tokens, cur_node, seq_head)
164
- when WILDCARD then compile_wildcard(cur_node, token[1..-1], seq_head)
165
- when FUNCALL then compile_funcall(tokens, cur_node, token, seq_head)
166
- when LITERAL then compile_literal(cur_node, token, seq_head)
167
- when PREDICATE then compile_predicate(tokens, cur_node, token, seq_head)
168
- when NODE then compile_nodetype(cur_node, token)
169
- when PARAM then compile_param(cur_node, token[1..-1], seq_head)
198
+ when '(' then compile_seq
199
+ when '{' then compile_union
200
+ when '[' then compile_intersect
201
+ when '!' then compile_negation
202
+ when '$' then compile_capture
203
+ when '^' then compile_ascend
204
+ when WILDCARD then compile_wildcard(token[1..-1])
205
+ when FUNCALL then compile_funcall(token)
206
+ when LITERAL then compile_literal(token)
207
+ when PREDICATE then compile_predicate(token)
208
+ when NODE then compile_nodetype(token)
209
+ when PARAM then compile_param(token[1..-1])
170
210
  when CLOSING then fail_due_to("#{token} in invalid position")
171
211
  when nil then fail_due_to('pattern ended prematurely')
172
212
  else fail_due_to("invalid token #{token.inspect}")
@@ -174,211 +214,260 @@ module RuboCop
174
214
  end
175
215
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
176
216
 
177
- def compile_seq(tokens, cur_node, seq_head)
178
- fail_due_to('empty parentheses') if tokens.first == ')'
179
- fail_due_to('parentheses at sequence head') if seq_head
217
+ def tokens_until(stop, what)
218
+ return to_enum __method__, stop, what unless block_given?
180
219
 
181
- # 'cur_node' is a Ruby expression which evaluates to an AST node,
182
- # but we don't know how expensive it is
183
- # to be safe, cache the node in a temp variable and then use the
184
- # temp variable as 'cur_node'
185
- with_temp_node(cur_node) do |init, temp_node|
186
- terms = compile_seq_terms(tokens, temp_node)
187
- terms.unshift(compile_guard_clause(temp_node))
220
+ fail_due_to("empty #{what}") if tokens.first == stop && what
221
+ yield until tokens.first == stop
222
+ tokens.shift
223
+ end
188
224
 
189
- join_terms(init, terms, " &&\n")
190
- end
225
+ def compile_seq
226
+ terms = tokens_until(')', 'sequence').map { variadic_seq_term }
227
+ Sequence.new(self, *terms).compile
191
228
  end
192
229
 
193
- def compile_guard_clause(cur_node)
194
- "#{cur_node}.is_a?(RuboCop::AST::Node)"
230
+ def compile_guard_clause
231
+ "#{CUR_NODE}.is_a?(RuboCop::AST::Node)"
195
232
  end
196
233
 
197
- def compile_seq_terms(tokens, cur_node)
198
- ret =
199
- compile_seq_terms_with_size(tokens, cur_node) do |token, terms, index|
200
- capture = next_capture if token == CAPTURED_REST
201
- if capture || token == REST
202
- index = 0 if index == SEQ_HEAD_INDEX # Consider ($...) as (_ $...)
203
- return compile_ellipsis(tokens, cur_node, terms, index, capture)
204
- end
205
- end
206
- ret << "(#{cur_node}.children.size == #{ret.size - 1})"
234
+ def variadic_seq_term
235
+ token = tokens.shift
236
+ case token
237
+ when CAPTURED_REST then compile_captured_ellipsis
238
+ when REST then compile_ellipsis
239
+ when '$<' then compile_any_order(next_capture)
240
+ when '<' then compile_any_order
241
+ else [1, compile_expr(token)]
242
+ end
207
243
  end
208
244
 
209
- def compile_seq_terms_with_size(tokens, cur_node)
210
- index = SEQ_HEAD_INDEX
211
- terms = []
212
- until tokens.first == ')'
213
- yield tokens.first, terms, index
214
- term = compile_expr_with_index(tokens, cur_node, index)
215
- index += 1
216
- terms << term
245
+ # @private
246
+ # Builds Ruby code for a sequence
247
+ # (head *first_terms variadic_term *last_terms)
248
+ class Sequence < SimpleDelegator
249
+ def initialize(compiler, *arity_term_list)
250
+ @arities, @terms = arity_term_list.transpose
251
+
252
+ super(compiler)
253
+ @variadic_index = @arities.find_index { |a| a.is_a?(Range) }
254
+ fail_due_to 'multiple variable patterns in same sequence' \
255
+ if @variadic_index && !@arities.one? { |a| a.is_a?(Range) }
217
256
  end
218
257
 
219
- tokens.shift # drop concluding )
220
- terms
221
- end
258
+ def compile
259
+ [
260
+ compile_guard_clause,
261
+ compile_child_nb_guard,
262
+ compile_seq_head,
263
+ *compile_first_terms,
264
+ compile_variadic_term,
265
+ *compile_last_terms
266
+ ].compact.join(" &&\n") << SEQ_HEAD_GUARD
267
+ end
222
268
 
223
- def compile_expr_with_index(tokens, cur_node, index)
224
- if index == SEQ_HEAD_INDEX
225
- # in 'sequence head' position; some expressions are compiled
226
- # differently at 'sequence head' (notably 'node type' expressions)
227
- # grep for seq_head to see where it makes a difference
228
- compile_expr(tokens, cur_node, true)
229
- else
230
- child_node = "#{cur_node}.children[#{index}]"
231
- compile_expr(tokens, child_node, false)
269
+ private
270
+
271
+ def first_terms_arity
272
+ first_terms_range { |r| @arities[r].inject(0, :+) } || 0
232
273
  end
233
- end
234
274
 
235
- def compile_ellipsis(tokens, cur_node, terms, index, capture = nil)
236
- tokens.shift # drop ellipsis
237
- tail = compile_seq_tail(tokens, cur_node)
238
- terms << "(#{cur_node}.children.size >= #{index + tail.size})"
239
- terms.concat tail
240
- if capture
241
- range = index..-tail.size - 1
242
- terms << "(#{capture} = #{cur_node}.children[#{range}])"
275
+ def last_terms_arity
276
+ last_terms_range { |r| @arities[r].inject(0, :+) } || 0
243
277
  end
244
- terms
245
- end
246
278
 
247
- def compile_seq_tail(tokens, cur_node)
248
- child_node = "#{cur_node}.children[%<revindex>i]"
249
- terms = []
250
- until tokens.first == ')'
251
- terms << compile_expr(tokens, child_node, false)
279
+ def first_terms_range
280
+ yield 1..(@variadic_index || @terms.size) - 1 if seq_head?
252
281
  end
253
- tokens.shift # drop ')'
254
- # E.g. for terms.size == 3, we want to replace the three [%<revindex>i]
255
- # with [-3], [-2] and [-1]
256
- terms.map.with_index { |term, i| format term, revindex: i - terms.size }
257
- end
258
282
 
259
- def compile_union(tokens, cur_node, seq_head)
260
- fail_due_to('empty union') if tokens.first == '}'
283
+ def last_terms_range
284
+ yield @variadic_index + 1...@terms.size if @variadic_index
285
+ end
261
286
 
262
- with_temp_node(cur_node) do |init, temp_node|
263
- terms = union_terms(tokens, temp_node, seq_head)
264
- join_terms(init, terms, ' || ')
287
+ def seq_head?
288
+ @variadic_index != 0
265
289
  end
266
- end
267
290
 
268
- def union_terms(tokens, temp_node, seq_head)
269
- # we need to ensure that each branch of the {} contains the same
270
- # number of captures (since only one branch of the {} can actually
271
- # match, the same variables are used to hold the captures for each
272
- # branch)
273
- compile_expr_with_captures(tokens,
274
- temp_node, seq_head) do |term, before, after|
275
- terms = [term]
276
- until tokens.first == '}'
277
- terms << compile_expr_with_capture_check(tokens, temp_node,
278
- seq_head, before, after)
291
+ def compile_child_nb_guard
292
+ min = first_terms_arity + last_terms_arity
293
+ "#{CUR_NODE}.children.size #{@variadic_index ? '>' : '='}= #{min}"
294
+ end
295
+
296
+ def term(index, range)
297
+ t = @terms[index]
298
+ if t.respond_to? :call
299
+ t.call(range)
300
+ else
301
+ with_child_context(t, range.begin)
279
302
  end
280
- tokens.shift
303
+ end
304
+
305
+ def compile_seq_head
306
+ return unless seq_head?
307
+
308
+ fail_due_to 'sequences can not start with <' \
309
+ if @terms[0].respond_to? :call
281
310
 
282
- terms
311
+ with_seq_head_context(@terms[0])
283
312
  end
284
- end
285
313
 
286
- def compile_expr_with_captures(tokens, temp_node, seq_head)
287
- captures_before = @captures
288
- expr = compile_expr(tokens, temp_node, seq_head)
314
+ def compile_first_terms
315
+ first_terms_range { |range| compile_terms(range, 0) }
316
+ end
289
317
 
290
- yield expr, captures_before, @captures
291
- end
318
+ def compile_last_terms
319
+ last_terms_range { |r| compile_terms(r, -last_terms_arity) }
320
+ end
292
321
 
293
- def compile_expr_with_capture_check(tokens, temp_node, seq_head, before,
294
- after)
295
- @captures = before
296
- expr = compile_expr(tokens, temp_node, seq_head)
297
- if @captures != after
298
- fail_due_to('each branch of {} must have same # of captures')
322
+ def compile_terms(index_range, start)
323
+ index_range.map do |i|
324
+ current = start
325
+ start += @arities.fetch(i)
326
+ term(i, current..start - 1)
327
+ end
299
328
  end
300
329
 
301
- expr
302
- end
330
+ def compile_variadic_term
331
+ variadic_arity { |arity| term(@variadic_index, arity) }
332
+ end
303
333
 
304
- def compile_intersect(tokens, cur_node, seq_head)
305
- fail_due_to('empty intersection') if tokens.first == ']'
334
+ def variadic_arity
335
+ return unless @variadic_index
306
336
 
307
- with_temp_node(cur_node) do |init, temp_node|
308
- terms = []
309
- until tokens.first == ']'
310
- terms << compile_expr(tokens, temp_node, seq_head)
337
+ first = @variadic_index > 0 ? first_terms_arity : SEQ_HEAD_INDEX
338
+ yield first..-last_terms_arity - 1
339
+ end
340
+ end
341
+ private_constant :Sequence
342
+
343
+ def compile_captured_ellipsis
344
+ capture = next_capture
345
+ block = lambda { |range|
346
+ # Consider ($...) like (_ $...):
347
+ range = 0..range.end if range.begin == SEQ_HEAD_INDEX
348
+ "(#{capture} = #{CUR_NODE}.children[#{range}])"
349
+ }
350
+ [0..Float::INFINITY, block]
351
+ end
352
+
353
+ def compile_ellipsis
354
+ [0..Float::INFINITY, 'true']
355
+ end
356
+
357
+ # rubocop:disable Metrics/MethodLength
358
+ def compile_any_order(capture_all = nil)
359
+ rest = capture_rest = nil
360
+ patterns = []
361
+ with_temp_variables do |child, matched|
362
+ tokens_until('>', 'any child').each do
363
+ fail_due_to 'ellipsis must be at the end of <>' if rest
364
+ token = tokens.shift
365
+ case token
366
+ when CAPTURED_REST then rest = capture_rest = next_capture
367
+ when REST then rest = true
368
+ else patterns << compile_expr(token)
369
+ end
311
370
  end
312
- tokens.shift
371
+ [rest ? patterns.size..Float::INFINITY : patterns.size,
372
+ ->(range) { ANY_ORDER_TEMPLATE.result(binding) }]
373
+ end
374
+ end
375
+ # rubocop:enable Metrics/MethodLength
376
+
377
+ def insure_same_captures(enum, what)
378
+ return to_enum __method__, enum, what unless block_given?
313
379
 
314
- join_terms(init, terms, ' && ')
380
+ captures_before = captures_after = nil
381
+ enum.each do
382
+ captures_before ||= @captures
383
+ @captures = captures_before
384
+ yield
385
+ captures_after ||= @captures
386
+ if captures_after != @captures
387
+ fail_due_to("each #{what} must have same # of captures")
388
+ end
315
389
  end
316
390
  end
317
391
 
318
- def compile_capture(tokens, cur_node, seq_head)
319
- "(#{next_capture} = #{cur_node}#{'.type' if seq_head}; " \
320
- "#{compile_expr(tokens, cur_node, seq_head)})"
392
+ def compile_union
393
+ # we need to ensure that each branch of the {} contains the same
394
+ # number of captures (since only one branch of the {} can actually
395
+ # match, the same variables are used to hold the captures for each
396
+ # branch)
397
+ enum = tokens_until('}', 'union')
398
+ terms = insure_same_captures(enum, 'branch of {}')
399
+ .map { compile_expr }
400
+
401
+ "(#{terms.join(' || ')})"
402
+ end
403
+
404
+ def compile_intersect
405
+ tokens_until(']', 'intersection')
406
+ .map { compile_expr }
407
+ .join(' && ')
321
408
  end
322
409
 
323
- def compile_negation(tokens, cur_node, seq_head)
324
- "(!#{compile_expr(tokens, cur_node, seq_head)})"
410
+ def compile_capture
411
+ "(#{next_capture} = #{CUR_ELEMENT}; #{compile_expr})"
325
412
  end
326
413
 
327
- def compile_ascend(tokens, cur_node, seq_head)
328
- "(#{cur_node}.parent && " \
329
- "#{compile_expr(tokens, "#{cur_node}.parent", seq_head)})"
414
+ def compile_negation
415
+ "!(#{compile_expr})"
330
416
  end
331
417
 
332
- def compile_wildcard(cur_node, name, seq_head)
418
+ def compile_ascend
419
+ with_context("#{CUR_NODE} && #{compile_expr}", "#{CUR_NODE}.parent")
420
+ end
421
+
422
+ def compile_wildcard(name)
333
423
  if name.empty?
334
424
  'true'
335
425
  elsif @unify.key?(name)
336
426
  # we have already seen a wildcard with this name before
337
427
  # so the value it matched the first time will already be stored
338
428
  # in a temp. check if this value matches the one stored in the temp
339
- "(#{cur_node}#{'.type' if seq_head} == temp#{@unify[name]})"
429
+ "#{CUR_ELEMENT} == temp#{@unify[name]}"
340
430
  else
341
431
  n = @unify[name] = next_temp_value
342
432
  # double assign to temp#{n} to avoid "assigned but unused variable"
343
- "(temp#{n} = #{cur_node}#{'.type' if seq_head}; " \
433
+ "(temp#{n} = #{CUR_ELEMENT}; " \
344
434
  "temp#{n} = temp#{n}; true)"
345
435
  end
346
436
  end
347
437
 
348
- def compile_literal(cur_node, literal, seq_head)
349
- "(#{cur_node}#{'.type' if seq_head} == #{literal})"
438
+ def compile_literal(literal)
439
+ "#{CUR_ELEMENT} == #{literal}"
350
440
  end
351
441
 
352
- def compile_predicate(tokens, cur_node, predicate, seq_head)
442
+ def compile_predicate(predicate)
353
443
  if predicate.end_with?('(') # is there an arglist?
354
444
  args = compile_args(tokens)
355
445
  predicate = predicate[0..-2] # drop the trailing (
356
- "(#{cur_node}#{'.type' if seq_head}.#{predicate}(#{args.join(',')}))"
446
+ "#{CUR_ELEMENT}.#{predicate}(#{args.join(',')})"
357
447
  else
358
- "(#{cur_node}#{'.type' if seq_head}.#{predicate})"
448
+ "#{CUR_ELEMENT}.#{predicate}"
359
449
  end
360
450
  end
361
451
 
362
- def compile_funcall(tokens, cur_node, method, seq_head)
452
+ def compile_funcall(method)
363
453
  # call a method in the context which this pattern-matching
364
454
  # code is used in. pass target value as an argument
365
455
  method = method[1..-1] # drop the leading #
366
456
  if method.end_with?('(') # is there an arglist?
367
457
  args = compile_args(tokens)
368
458
  method = method[0..-2] # drop the trailing (
369
- "(#{method}(#{cur_node}#{'.type' if seq_head},#{args.join(',')}))"
459
+ "#{method}(#{CUR_ELEMENT},#{args.join(',')})"
370
460
  else
371
- "(#{method}(#{cur_node}#{'.type' if seq_head}))"
461
+ "#{method}(#{CUR_ELEMENT})"
372
462
  end
373
463
  end
374
464
 
375
- def compile_nodetype(cur_node, type)
376
- "(#{cur_node}.is_a?(RuboCop::AST::Node) && " \
377
- "#{cur_node}.#{type.tr('-', '_')}_type?)"
465
+ def compile_nodetype(type)
466
+ "#{compile_guard_clause} && #{CUR_NODE}.#{type.tr('-', '_')}_type?"
378
467
  end
379
468
 
380
- def compile_param(cur_node, number, seq_head)
381
- "(#{cur_node}#{'.type' if seq_head} == #{get_param(number)})"
469
+ def compile_param(number)
470
+ "#{CUR_ELEMENT} == #{get_param(number)}"
382
471
  end
383
472
 
384
473
  def compile_args(tokens)
@@ -406,7 +495,9 @@ module RuboCop
406
495
  end
407
496
 
408
497
  def next_capture
409
- "capture#{@captures += 1}"
498
+ index = @captures
499
+ @captures += 1
500
+ "captures[#{index}]"
410
501
  end
411
502
 
412
503
  def get_param(number)
@@ -415,21 +506,24 @@ module RuboCop
415
506
  number.zero? ? @root : "param#{number}"
416
507
  end
417
508
 
418
- def join_terms(init, terms, operator)
419
- "(#{init};#{terms.join(operator)})"
420
- end
421
-
422
- def emit_capture_list
423
- (1..@captures).map { |n| "capture#{n}" }.join(',')
509
+ def emit_yield_capture(when_no_capture = '')
510
+ yield_val = if @captures.zero?
511
+ when_no_capture
512
+ elsif @captures == 1
513
+ 'captures[0]' # Circumvent https://github.com/jruby/jruby/issues/5710
514
+ else
515
+ '*captures'
516
+ end
517
+ "yield(#{yield_val})"
424
518
  end
425
519
 
426
520
  def emit_retval
427
521
  if @captures.zero?
428
522
  'true'
429
523
  elsif @captures == 1
430
- 'capture1'
524
+ 'captures[0]'
431
525
  else
432
- "[#{emit_capture_list}]"
526
+ 'captures'
433
527
  end
434
528
  end
435
529
 
@@ -445,7 +539,7 @@ module RuboCop
445
539
  def emit_method_code
446
540
  <<-RUBY
447
541
  return unless #{@match_code}
448
- block_given? ? yield(#{emit_capture_list}) : (return #{emit_retval})
542
+ block_given? ? #{emit_yield_capture} : (return #{emit_retval})
449
543
  RUBY
450
544
  end
451
545
 
@@ -454,20 +548,62 @@ module RuboCop
454
548
  end
455
549
 
456
550
  def with_temp_node(cur_node)
457
- with_temp_variable do |temp_var|
458
- # double assign to temp#{n} to avoid "assigned but unused variable"
459
- yield "#{temp_var} = #{cur_node}; #{temp_var} = #{temp_var}", temp_var
551
+ with_temp_variables do |node|
552
+ yield "(#{node} = #{cur_node})", node
460
553
  end
554
+ .gsub("\n", "\n ") # Nicer indent for debugging
461
555
  end
462
556
 
463
- def with_temp_variable
464
- yield "temp#{next_temp_value}"
557
+ def with_temp_variables(&block)
558
+ names = block.parameters.map { |_, name| "#{name}#{next_temp_value}" }
559
+ yield(*names)
465
560
  end
466
561
 
467
562
  def next_temp_value
468
563
  @temps += 1
469
564
  end
470
565
 
566
+ def auto_use_temp_node?(code)
567
+ code.scan(CUR_PLACEHOLDER).count > 1
568
+ end
569
+
570
+ # with_<...>_context methods are used whenever the context,
571
+ # i.e the current node or the current element can be determined.
572
+
573
+ def with_child_context(code, child_index)
574
+ with_context(code, "#{CUR_NODE}.children[#{child_index}]")
575
+ end
576
+
577
+ def with_context(code, cur_node,
578
+ use_temp_node: auto_use_temp_node?(code))
579
+ if use_temp_node
580
+ with_temp_node(cur_node) do |init, temp_var|
581
+ substitute_cur_node(code, temp_var, first_cur_node: init)
582
+ end
583
+ else
584
+ substitute_cur_node(code, cur_node)
585
+ end
586
+ end
587
+
588
+ def with_seq_head_context(code)
589
+ if code.include?(SEQ_HEAD_GUARD)
590
+ fail_due_to('parentheses at sequence head')
591
+ end
592
+
593
+ code.gsub CUR_ELEMENT, "#{CUR_NODE}.type"
594
+ end
595
+
596
+ def substitute_cur_node(code, cur_node, first_cur_node: cur_node)
597
+ iter = 0
598
+ code
599
+ .gsub(CUR_ELEMENT, CUR_NODE)
600
+ .gsub(CUR_NODE) do
601
+ iter += 1
602
+ iter == 1 ? first_cur_node : cur_node
603
+ end
604
+ .gsub(SEQ_HEAD_GUARD, '')
605
+ end
606
+
471
607
  def self.tokens(pattern)
472
608
  pattern.scan(TOKEN).reject { |token| token =~ /\A#{SEPARATORS}\Z/ }
473
609
  end
@@ -515,13 +651,11 @@ module RuboCop
515
651
  end
516
652
 
517
653
  def node_search_all(method_name, compiler, called_from)
518
- yieldval = compiler.emit_capture_list
519
- yieldval = 'node' if yieldval.empty?
654
+ yield_code = compiler.emit_yield_capture('node')
520
655
  prelude = "return enum_for(:#{method_name}, node0" \
521
656
  "#{compiler.emit_trailing_params}) unless block_given?"
522
657
 
523
- node_search(method_name, compiler, "yield(#{yieldval})", prelude,
524
- called_from)
658
+ node_search(method_name, compiler, yield_code, prelude, called_from)
525
659
  end
526
660
 
527
661
  def node_search(method_name, compiler, on_match, prelude, called_from)