rubocop 0.70.0 → 0.72.0

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -10
  3. data/config/default.yml +50 -491
  4. data/lib/rubocop.rb +5 -53
  5. data/lib/rubocop/ast/builder.rb +2 -0
  6. data/lib/rubocop/ast/node.rb +1 -1
  7. data/lib/rubocop/ast/node/float_node.rb +12 -0
  8. data/lib/rubocop/ast/node/int_node.rb +12 -0
  9. data/lib/rubocop/ast/node/mixin/numeric_node.rb +21 -0
  10. data/lib/rubocop/ast/node/resbody_node.rb +1 -6
  11. data/lib/rubocop/cached_data.rb +1 -1
  12. data/lib/rubocop/config.rb +35 -6
  13. data/lib/rubocop/config_loader.rb +2 -2
  14. data/lib/rubocop/config_loader_resolver.rb +0 -6
  15. data/lib/rubocop/cop/cop.rb +0 -4
  16. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +55 -0
  17. data/lib/rubocop/cop/layout/class_structure.rb +1 -1
  18. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +3 -1
  19. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +4 -0
  20. data/lib/rubocop/cop/layout/indent_first_argument.rb +6 -2
  21. data/lib/rubocop/cop/layout/indent_first_parameter.rb +7 -3
  22. data/lib/rubocop/cop/layout/indent_heredoc.rb +0 -1
  23. data/lib/rubocop/cop/layout/indentation_consistency.rb +13 -12
  24. data/lib/rubocop/cop/layout/indentation_width.rb +8 -4
  25. data/lib/rubocop/cop/layout/multiline_method_argument_line_breaks.rb +2 -0
  26. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  27. data/lib/rubocop/cop/mixin/hash_alignment.rb +4 -0
  28. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +20 -22
  29. data/lib/rubocop/cop/style/commented_keyword.rb +1 -1
  30. data/lib/rubocop/cop/style/conditional_assignment.rb +2 -1
  31. data/lib/rubocop/cop/style/float_division.rb +94 -0
  32. data/lib/rubocop/cop/style/format_string.rb +7 -3
  33. data/lib/rubocop/cop/style/if_inside_else.rb +42 -0
  34. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +7 -1
  35. data/lib/rubocop/cop/style/safe_navigation.rb +1 -1
  36. data/lib/rubocop/cop/style/ternary_parentheses.rb +12 -2
  37. data/lib/rubocop/cop/style/trailing_comma_in_arguments.rb +4 -0
  38. data/lib/rubocop/cop/style/word_array.rb +2 -2
  39. data/lib/rubocop/cop/style/zero_length_predicate.rb +1 -1
  40. data/lib/rubocop/node_pattern.rb +84 -5
  41. data/lib/rubocop/options.rb +0 -2
  42. data/lib/rubocop/processed_source.rb +5 -1
  43. data/lib/rubocop/rspec/cop_helper.rb +0 -1
  44. data/lib/rubocop/rspec/shared_contexts.rb +0 -17
  45. data/lib/rubocop/rspec/support.rb +0 -1
  46. data/lib/rubocop/runner.rb +6 -7
  47. data/lib/rubocop/version.rb +1 -1
  48. data/lib/rubocop/yaml_duplication_checker.rb +8 -2
  49. metadata +7 -69
  50. data/lib/rubocop/cop/mixin/target_rails_version.rb +0 -16
  51. data/lib/rubocop/cop/rails/action_filter.rb +0 -117
  52. data/lib/rubocop/cop/rails/active_record_aliases.rb +0 -48
  53. data/lib/rubocop/cop/rails/active_record_override.rb +0 -82
  54. data/lib/rubocop/cop/rails/active_support_aliases.rb +0 -69
  55. data/lib/rubocop/cop/rails/application_job.rb +0 -40
  56. data/lib/rubocop/cop/rails/application_record.rb +0 -40
  57. data/lib/rubocop/cop/rails/assert_not.rb +0 -44
  58. data/lib/rubocop/cop/rails/belongs_to.rb +0 -102
  59. data/lib/rubocop/cop/rails/blank.rb +0 -164
  60. data/lib/rubocop/cop/rails/bulk_change_table.rb +0 -289
  61. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +0 -91
  62. data/lib/rubocop/cop/rails/date.rb +0 -161
  63. data/lib/rubocop/cop/rails/delegate.rb +0 -132
  64. data/lib/rubocop/cop/rails/delegate_allow_blank.rb +0 -37
  65. data/lib/rubocop/cop/rails/dynamic_find_by.rb +0 -91
  66. data/lib/rubocop/cop/rails/enum_uniqueness.rb +0 -45
  67. data/lib/rubocop/cop/rails/environment_comparison.rb +0 -68
  68. data/lib/rubocop/cop/rails/exit.rb +0 -67
  69. data/lib/rubocop/cop/rails/file_path.rb +0 -108
  70. data/lib/rubocop/cop/rails/find_by.rb +0 -55
  71. data/lib/rubocop/cop/rails/find_each.rb +0 -51
  72. data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +0 -25
  73. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +0 -106
  74. data/lib/rubocop/cop/rails/http_positional_arguments.rb +0 -117
  75. data/lib/rubocop/cop/rails/http_status.rb +0 -179
  76. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +0 -94
  77. data/lib/rubocop/cop/rails/inverse_of.rb +0 -246
  78. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +0 -175
  79. data/lib/rubocop/cop/rails/link_to_blank.rb +0 -98
  80. data/lib/rubocop/cop/rails/not_null_column.rb +0 -67
  81. data/lib/rubocop/cop/rails/output.rb +0 -49
  82. data/lib/rubocop/cop/rails/output_safety.rb +0 -99
  83. data/lib/rubocop/cop/rails/pluralization_grammar.rb +0 -107
  84. data/lib/rubocop/cop/rails/presence.rb +0 -124
  85. data/lib/rubocop/cop/rails/present.rb +0 -153
  86. data/lib/rubocop/cop/rails/read_write_attribute.rb +0 -74
  87. data/lib/rubocop/cop/rails/redundant_allow_nil.rb +0 -111
  88. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +0 -136
  89. data/lib/rubocop/cop/rails/reflection_class_name.rb +0 -37
  90. data/lib/rubocop/cop/rails/refute_methods.rb +0 -76
  91. data/lib/rubocop/cop/rails/relative_date_constant.rb +0 -93
  92. data/lib/rubocop/cop/rails/request_referer.rb +0 -56
  93. data/lib/rubocop/cop/rails/reversible_migration.rb +0 -286
  94. data/lib/rubocop/cop/rails/safe_navigation.rb +0 -87
  95. data/lib/rubocop/cop/rails/save_bang.rb +0 -316
  96. data/lib/rubocop/cop/rails/scope_args.rb +0 -29
  97. data/lib/rubocop/cop/rails/skips_model_validations.rb +0 -87
  98. data/lib/rubocop/cop/rails/time_zone.rb +0 -238
  99. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +0 -105
  100. data/lib/rubocop/cop/rails/unknown_env.rb +0 -63
  101. data/lib/rubocop/cop/rails/validation.rb +0 -109
  102. data/lib/rubocop/rspec/shared_examples.rb +0 -59
@@ -83,7 +83,7 @@ module RuboCop
83
83
 
84
84
  check_indentation(end_loc, node.body)
85
85
 
86
- return unless indentation_consistency_style == 'rails'
86
+ return unless indented_internal_methods_style?
87
87
 
88
88
  check_members(end_loc, [node.body])
89
89
  end
@@ -155,8 +155,8 @@ module RuboCop
155
155
 
156
156
  return unless members.any? && members.first.begin_type?
157
157
 
158
- if indentation_consistency_style == 'rails'
159
- check_members_for_rails_style(members)
158
+ if indentation_consistency_style == 'indented_internal_methods'
159
+ check_members_for_indented_internal_methods_style(members)
160
160
  else
161
161
  members.first.children.each do |member|
162
162
  next if member.send_type? && member.access_modifier?
@@ -176,7 +176,7 @@ module RuboCop
176
176
  end
177
177
  end
178
178
 
179
- def check_members_for_rails_style(members)
179
+ def check_members_for_indented_internal_methods_style(members)
180
180
  each_member(members) do |member, previous_modifier|
181
181
  check_indentation(previous_modifier, member,
182
182
  indentation_consistency_style)
@@ -195,6 +195,10 @@ module RuboCop
195
195
  end
196
196
  end
197
197
 
198
+ def indented_internal_methods_style?
199
+ indentation_consistency_style == 'indented_internal_methods'
200
+ end
201
+
198
202
  def indentation_consistency_style
199
203
  config.for_cop('Layout/IndentationConsistency')['EnforcedStyle']
200
204
  end
@@ -26,6 +26,8 @@ module RuboCop
26
26
  'on a separate line.'
27
27
 
28
28
  def on_send(node)
29
+ return if node.method_name == :[]=
30
+
29
31
  args = node.arguments
30
32
 
31
33
  # If there is a trailing hash arg without explicit braces, like this:
@@ -64,7 +64,7 @@ module RuboCop
64
64
 
65
65
  def date_time_object?(node)
66
66
  child = node
67
- while child.send_type?
67
+ while child&.send_type?
68
68
  return true if datetime? child
69
69
 
70
70
  child = child.children[0]
@@ -83,6 +83,10 @@ module RuboCop
83
83
  class TableAlignment
84
84
  include ValueAlignment
85
85
 
86
+ def initialize
87
+ self.max_key_width = 0
88
+ end
89
+
86
90
  def deltas_for_first_pair(first_pair, node)
87
91
  self.max_key_width = node.keys.map { |key| key.source.length }.max
88
92
 
@@ -57,24 +57,20 @@ module RuboCop
57
57
  MSG = 'Use `%<preferred>s` instead of `%<bad>s`.'
58
58
 
59
59
  def on_resbody(node)
60
- exception_type, @exception_name = *node
61
- return unless exception_type || @exception_name
62
-
63
- @exception_name ||= exception_type.children.first
64
- return if @exception_name.const_type? ||
65
- variable_name == preferred_name
60
+ name = variable_name(node)
61
+ return unless name
62
+ return if preferred_name(name).to_sym == name
66
63
 
67
64
  add_offense(node, location: offense_range(node))
68
65
  end
69
66
 
70
67
  def autocorrect(node)
71
68
  lambda do |corrector|
72
- offending_name = node.exception_variable.children.first
69
+ offending_name = variable_name(node)
70
+ preferred_name = preferred_name(offending_name)
73
71
  corrector.replace(offense_range(node), preferred_name)
74
72
 
75
- return unless node.body
76
-
77
- node.body.each_descendant(:lvar) do |var|
73
+ node.body&.each_descendant(:lvar) do |var|
78
74
  next unless var.children.first == offending_name
79
75
 
80
76
  corrector.replace(var.loc.expression, preferred_name)
@@ -89,24 +85,26 @@ module RuboCop
89
85
  variable.loc.expression
90
86
  end
91
87
 
92
- def preferred_name
93
- @preferred_name ||= begin
94
- name = cop_config.fetch('PreferredName', 'e')
95
- name = "_#{name}" if variable_name.to_s.start_with?('_')
96
- name
88
+ def preferred_name(variable_name)
89
+ preferred_name = cop_config.fetch('PreferredName', 'e')
90
+ if variable_name.to_s.start_with?('_')
91
+ "_#{preferred_name}"
92
+ else
93
+ preferred_name
97
94
  end
98
95
  end
99
96
 
100
- def variable_name
101
- location.source
102
- end
97
+ def variable_name(node)
98
+ asgn_node = node.exception_variable
99
+ return unless asgn_node
103
100
 
104
- def location
105
- @exception_name.loc.expression
101
+ asgn_node.children.last
106
102
  end
107
103
 
108
- def message(_node = nil)
109
- format(MSG, preferred: preferred_name, bad: variable_name)
104
+ def message(node)
105
+ offending_name = variable_name(node)
106
+ preferred_name = preferred_name(offending_name)
107
+ format(MSG, preferred: preferred_name, bad: offending_name)
110
108
  end
111
109
  end
112
110
  end
@@ -6,7 +6,7 @@ module RuboCop
6
6
  # This cop checks for comments put on the same line as some keywords.
7
7
  # These keywords are: `begin`, `class`, `def`, `end`, `module`.
8
8
  #
9
- # Note that some comments (`:nodoc:`, `:yields:, and `rubocop:disable`)
9
+ # Note that some comments (`:nodoc:`, `:yields:`, and `rubocop:disable`)
10
10
  # are allowed.
11
11
  #
12
12
  # @example
@@ -596,7 +596,8 @@ module RuboCop
596
596
 
597
597
  remove_whitespace_in_branches(corrector, branch, condition, column)
598
598
 
599
- branch_else = branch.parent.loc.else
599
+ return unless (branch_else = branch.parent.loc.else)
600
+
600
601
  corrector.remove_preceding(branch_else, branch_else.column - column)
601
602
  end
602
603
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # This cop checks for division with integers coerced to floats.
7
+ # It is recommended to either always use `fdiv` or coerce one side only.
8
+ # This cop also provides other options for code consistency.
9
+ #
10
+ # @example EnforcedStyle: single_coerce (default)
11
+ # # bad
12
+ # a.to_f / b.to_f
13
+ #
14
+ # # good
15
+ # a.to_f / b
16
+ # a / b.to_f
17
+ #
18
+ # @example EnforcedStyle: left_coerce
19
+ # # bad
20
+ # a / b.to_f
21
+ # a.to_f / b.to_f
22
+ #
23
+ # # good
24
+ # a.to_f / b
25
+ #
26
+ # @example EnforcedStyle: right_coerce
27
+ # # bad
28
+ # a.to_f / b
29
+ # a.to_f / b.to_f
30
+ #
31
+ # # good
32
+ # a / b.to_f
33
+ #
34
+ # @example EnforcedStyle: fdiv
35
+ # # bad
36
+ # a / b.to_f
37
+ # a.to_f / b
38
+ # a.to_f / b.to_f
39
+ #
40
+ # # good
41
+ # a.fdiv(b)
42
+ class FloatDivision < Cop
43
+ include ConfigurableEnforcedStyle
44
+
45
+ def_node_matcher :right_coerce?, <<-PATTERN
46
+ (send _ :/ (send _ :to_f))
47
+ PATTERN
48
+ def_node_matcher :left_coerce?, <<-PATTERN
49
+ (send (send _ :to_f) :/ _)
50
+ PATTERN
51
+ def_node_matcher :both_coerce?, <<-PATTERN
52
+ (send (send _ :to_f) :/ (send _ :to_f))
53
+ PATTERN
54
+ def_node_matcher :any_coerce?, <<-PATTERN
55
+ {(send _ :/ (send _ :to_f)) (send (send _ :to_f) :/ _)}
56
+ PATTERN
57
+
58
+ def on_send(node)
59
+ add_offense(node) if offense_condition?(node)
60
+ end
61
+
62
+ private
63
+
64
+ def offense_condition?(node)
65
+ case style
66
+ when :left_coerce
67
+ right_coerce?(node)
68
+ when :right_coerce
69
+ left_coerce?(node)
70
+ when :single_coerce
71
+ both_coerce?(node)
72
+ when :fdiv
73
+ any_coerce?(node)
74
+ else
75
+ false
76
+ end
77
+ end
78
+
79
+ def message(_node)
80
+ case style
81
+ when :left_coerce
82
+ 'Prefer using `.to_f` on the left side.'
83
+ when :right_coerce
84
+ 'Prefer using `.to_f` on the right side.'
85
+ when :single_coerce
86
+ 'Prefer using `.to_f` on one side only.'
87
+ when :fdiv
88
+ 'Prefer using `fdiv` for float divisions.'
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -48,6 +48,10 @@ module RuboCop
48
48
  }
49
49
  PATTERN
50
50
 
51
+ def_node_matcher :variable_argument?, <<-PATTERN
52
+ (send {str dstr} :% {send_type? lvar_type?})
53
+ PATTERN
54
+
51
55
  def on_send(node)
52
56
  formatter(node) do |selector|
53
57
  detected_style = selector == :% ? :percent : selector
@@ -70,10 +74,10 @@ module RuboCop
70
74
  end
71
75
 
72
76
  def autocorrect(node)
73
- lambda do |corrector|
74
- detected_method = node.method_name
77
+ return if variable_argument?(node)
75
78
 
76
- case detected_method
79
+ lambda do |corrector|
80
+ case node.method_name
77
81
  when :%
78
82
  autocorrect_from_percent(corrector, node)
79
83
  when :format, :sprintf
@@ -27,6 +27,37 @@ module RuboCop
27
27
  # else
28
28
  # action_c
29
29
  # end
30
+ #
31
+ # @example AllowIfModifier: false (default)
32
+ # # bad
33
+ # if condition_a
34
+ # action_a
35
+ # else
36
+ # action_b if condition_b
37
+ # end
38
+ #
39
+ # # good
40
+ # if condition_a
41
+ # action_a
42
+ # elsif condition_b
43
+ # action_b
44
+ # end
45
+ #
46
+ # @example AllowIfModifier: true
47
+ # # good
48
+ # if condition_a
49
+ # action_a
50
+ # else
51
+ # action_b if condition_b
52
+ # end
53
+ #
54
+ # # good
55
+ # if condition_a
56
+ # action_a
57
+ # elsif condition_b
58
+ # action_b
59
+ # end
60
+ #
30
61
  class IfInsideElse < Cop
31
62
  MSG = 'Convert `if` nested inside `else` to `elsif`.'
32
63
 
@@ -36,9 +67,20 @@ module RuboCop
36
67
  else_branch = node.else_branch
37
68
 
38
69
  return unless else_branch&.if_type? && else_branch&.if?
70
+ return if allow_if_modifier_in_else_branch?(else_branch)
39
71
 
40
72
  add_offense(else_branch, location: :keyword)
41
73
  end
74
+
75
+ private
76
+
77
+ def allow_if_modifier_in_else_branch?(else_branch)
78
+ allow_if_modifier? && else_branch&.modifier_form?
79
+ end
80
+
81
+ def allow_if_modifier?
82
+ cop_config['AllowIfModifier']
83
+ end
42
84
  end
43
85
  end
44
86
  end
@@ -348,7 +348,8 @@ module RuboCop
348
348
  end
349
349
 
350
350
  def ambigious_literal?(node)
351
- splat?(node) || ternary_if?(node) || regexp_slash_literal?(node)
351
+ splat?(node) || ternary_if?(node) || regexp_slash_literal?(node) ||
352
+ unary_literal?(node)
352
353
  end
353
354
 
354
355
  def splat?(node)
@@ -371,6 +372,11 @@ module RuboCop
371
372
  node.regexp_type? && node.loc.begin.source == '/'
372
373
  end
373
374
 
375
+ def unary_literal?(node)
376
+ node.numeric_type? && node.sign? ||
377
+ node.parent&.send_type? && node.parent&.unary_operation?
378
+ end
379
+
374
380
  def assigned_before?(node, target)
375
381
  node.assignment? &&
376
382
  node.loc.operator.begin < target.loc.begin
@@ -224,7 +224,7 @@ module RuboCop
224
224
  end
225
225
 
226
226
  def method_called?(send_node)
227
- send_node.parent&.send_type?
227
+ send_node&.parent&.send_type?
228
228
  end
229
229
 
230
230
  def begin_range(node, method_call)
@@ -108,7 +108,7 @@ module RuboCop
108
108
  end
109
109
 
110
110
  def non_complex_send?(node)
111
- return false unless node.send_type?
111
+ return false unless node.call_type?
112
112
 
113
113
  !node.operator_method? || node.method?(:[])
114
114
  end
@@ -149,7 +149,8 @@ module RuboCop
149
149
 
150
150
  def unsafe_autocorrect?(condition)
151
151
  condition.children.any? do |child|
152
- unparenthesized_method_call?(child)
152
+ unparenthesized_method_call?(child) ||
153
+ below_ternary_precedence?(child)
153
154
  end
154
155
  end
155
156
 
@@ -157,6 +158,15 @@ module RuboCop
157
158
  method_name(child) =~ /^[a-z]/i && !child.parenthesized?
158
159
  end
159
160
 
161
+ def below_ternary_precedence?(child)
162
+ # Handle English "or", e.g. 'foo or bar ? a : b'
163
+ (child.or_type? && child.semantic_operator?) ||
164
+ # Handle English "and", e.g. 'foo and bar ? a : b'
165
+ (child.and_type? && child.semantic_operator?) ||
166
+ # Handle English "not", e.g. 'not foo ? a : b'
167
+ (child.send_type? && child.prefix_not?)
168
+ end
169
+
160
170
  def_node_matcher :method_name, <<-PATTERN
161
171
  {($:defined? (send nil? _) ...)
162
172
  (send {_ nil?} $_ _ ...)}
@@ -65,6 +65,10 @@ module RuboCop
65
65
  PunctuationCorrector.swap_comma(range)
66
66
  end
67
67
 
68
+ def self.autocorrect_incompatible_with
69
+ [Layout::HeredocArgumentClosingParenthesis]
70
+ end
71
+
68
72
  private
69
73
 
70
74
  def avoid_autocorrect?(args)
@@ -71,8 +71,8 @@ module RuboCop
71
71
 
72
72
  def complex_content?(strings)
73
73
  strings.any? do |s|
74
- string = s.str_content
75
- !string.dup.force_encoding(::Encoding::UTF_8).valid_encoding? ||
74
+ string = s.str_content.dup.force_encoding(::Encoding::UTF_8)
75
+ !string.valid_encoding? ||
76
76
  string !~ word_regex || string =~ / /
77
77
  end
78
78
  end
@@ -7,7 +7,7 @@ module RuboCop
7
7
  # by a predicate method, such as receiver.length == 0,
8
8
  # receiver.length > 0, receiver.length != 0,
9
9
  # receiver.length < 1 and receiver.size == 0 that can be
10
- # replaced by receiver.empty? and !receiver.empty.
10
+ # replaced by receiver.empty? and !receiver.empty?.
11
11
  #
12
12
  # @example
13
13
  # # bad
@@ -47,6 +47,11 @@ module RuboCop
47
47
  # # a `sym` node, but can have more.
48
48
  # '(array <$str $_>)' # captures are in the order of the pattern,
49
49
  # # irrespective of the actual order of the children
50
+ # '(array int*)' # will match an array of 0 or more integers
51
+ # '(array int ?)' # will match 0 or 1 integer.
52
+ # # Note: Space needed to distinguish from int?
53
+ # '(array int+)' # will match an array of 1 or more integers
54
+ # '(array (int $_)+)' # as above and will capture the numbers in an array
50
55
  # '(send $...)' # capture all the children as an array
51
56
  # '(send $... int)' # capture all children but the last as an array
52
57
  # '(send _x :+ _x)' # unification is performed on named wildcards
@@ -106,7 +111,9 @@ module RuboCop
106
111
  class Compiler
107
112
  SYMBOL = %r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}.freeze
108
113
  IDENTIFIER = /[a-zA-Z_][a-zA-Z0-9_-]*/.freeze
109
- META = Regexp.union(%w"( ) { } [ ] $< < > $... $ ! ^ ...").freeze
114
+ META = Regexp.union(
115
+ %w"( ) { } [ ] $< < > $... $ ! ^ ... + * ?"
116
+ ).freeze
110
117
  NUMBER = /-?\d+(?:\.\d+)?/.freeze
111
118
  STRING = /".+?"/.freeze
112
119
  METHOD_NAME = /\#?#{IDENTIFIER}[\!\?]?\(?/.freeze
@@ -160,6 +167,21 @@ module RuboCop
160
167
  RUBY
161
168
  ANY_ORDER_TEMPLATE.location = [__FILE__, line + 1]
162
169
 
170
+ line = __LINE__
171
+ REPEATED_TEMPLATE = ERB.new <<~RUBY.gsub("-%>\n", '%>')
172
+ <% if captured %>(<%= accumulate %> = Array.new) && <% end %>
173
+ <%= CUR_NODE %>.children[<%= range %>].all? do |<%= child %>|
174
+ <%= with_context(expr, child, use_temp_node: false) %><% if captured %>&&
175
+ <%= accumulate %>.push(<%= captured %>)<% end %>
176
+ end <% if captured %>&&
177
+ (<%= captured %> = if <%= accumulate %>.empty?
178
+ <%= captured %>.map{[]} # Transpose hack won't work for empty case
179
+ else
180
+ <%= accumulate %>.transpose
181
+ end) <% end -%>
182
+ RUBY
183
+ REPEATED_TEMPLATE.location = [__FILE__, line + 1]
184
+
163
185
  def initialize(str, node_var = 'node0')
164
186
  @string = str
165
187
  @root = node_var
@@ -238,10 +260,48 @@ module RuboCop
238
260
  when REST then compile_ellipsis
239
261
  when '$<' then compile_any_order(next_capture)
240
262
  when '<' then compile_any_order
241
- else [1, compile_expr(token)]
263
+ else compile_repeated_expr(token)
264
+ end
265
+ end
266
+
267
+ def compile_repeated_expr(token)
268
+ before = @captures
269
+ expr = compile_expr(token)
270
+ min, max = parse_repetition_token
271
+ return [1, expr] if min.nil?
272
+
273
+ if @captures != before
274
+ captured = "captures[#{before}...#{@captures}]"
275
+ accumulate = next_temp_variable(:accumulate)
276
+ end
277
+ arity = min..max || Float::INFINITY
278
+
279
+ [arity, repeated_generator(expr, captured, accumulate)]
280
+ end
281
+
282
+ def repeated_generator(expr, captured, accumulate)
283
+ with_temp_variables do |child|
284
+ lambda do |range|
285
+ if range.begin == SEQ_HEAD_INDEX
286
+ fail_due_to 'repeated pattern at beginning of sequence'
287
+ end
288
+ REPEATED_TEMPLATE.result(binding)
289
+ end
242
290
  end
243
291
  end
244
292
 
293
+ def parse_repetition_token
294
+ case tokens.first
295
+ when '*' then min = 0
296
+ when '+' then min = 1
297
+ when '?' then min = 0
298
+ max = 1
299
+ else return
300
+ end
301
+ tokens.shift
302
+ [min, max]
303
+ end
304
+
245
305
  # @private
246
306
  # Builds Ruby code for a sequence
247
307
  # (head *first_terms variadic_term *last_terms)
@@ -276,6 +336,10 @@ module RuboCop
276
336
  last_terms_range { |r| @arities[r].inject(0, :+) } || 0
277
337
  end
278
338
 
339
+ def variadic_term_min_arity
340
+ @variadic_index ? @arities[@variadic_index].begin : 0
341
+ end
342
+
279
343
  def first_terms_range
280
344
  yield 1..(@variadic_index || @terms.size) - 1 if seq_head?
281
345
  end
@@ -289,8 +353,19 @@ module RuboCop
289
353
  end
290
354
 
291
355
  def compile_child_nb_guard
292
- min = first_terms_arity + last_terms_arity
293
- "#{CUR_NODE}.children.size #{@variadic_index ? '>' : '='}= #{min}"
356
+ fixed = first_terms_arity + last_terms_arity
357
+ min = fixed + variadic_term_min_arity
358
+ op = if @variadic_index
359
+ max_variadic = @arities[@variadic_index].end
360
+ if max_variadic != Float::INFINITY
361
+ range = min..fixed + max_variadic
362
+ return "(#{range}).cover?(#{CUR_NODE}.children.size)"
363
+ end
364
+ '>='
365
+ else
366
+ '=='
367
+ end
368
+ "#{CUR_NODE}.children.size #{op} #{min}"
294
369
  end
295
370
 
296
371
  def term(index, range)
@@ -555,10 +630,14 @@ module RuboCop
555
630
  end
556
631
 
557
632
  def with_temp_variables(&block)
558
- names = block.parameters.map { |_, name| "#{name}#{next_temp_value}" }
633
+ names = block.parameters.map { |_, name| next_temp_variable(name) }
559
634
  yield(*names)
560
635
  end
561
636
 
637
+ def next_temp_variable(name)
638
+ "#{name}#{next_temp_value}"
639
+ end
640
+
562
641
  def next_temp_value
563
642
  @temps += 1
564
643
  end