rubocop 1.87.0 → 1.88.1

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 (162) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +78 -72
  3. data/config/obsoletion.yml +21 -1
  4. data/lib/rubocop/cli/command/auto_generate_config.rb +6 -0
  5. data/lib/rubocop/cop/base.rb +17 -2
  6. data/lib/rubocop/cop/bundler/gem_comment.rb +5 -3
  7. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  8. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  9. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  10. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  11. data/lib/rubocop/cop/gemspec/require_mfa.rb +4 -1
  12. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  13. data/lib/rubocop/cop/layout/block_alignment.rb +58 -4
  14. data/lib/rubocop/cop/layout/class_structure.rb +7 -3
  15. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  16. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  17. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +14 -1
  18. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +13 -14
  19. data/lib/rubocop/cop/layout/indentation_width.rb +28 -0
  20. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  21. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  22. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  23. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  24. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  25. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  26. data/lib/rubocop/cop/lint/deprecated_constants.rb +1 -7
  27. data/lib/rubocop/cop/lint/empty_block.rb +3 -3
  28. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  29. data/lib/rubocop/cop/lint/erb_new_arguments.rb +3 -1
  30. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  31. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  32. data/lib/rubocop/cop/lint/interpolation_check.rb +18 -3
  33. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  34. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  35. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  36. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +4 -4
  37. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  38. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  39. data/lib/rubocop/cop/lint/number_conversion.rb +13 -4
  40. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  41. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  42. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  43. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  44. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  45. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +4 -1
  46. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  47. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +14 -7
  48. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  49. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +7 -0
  50. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  51. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  52. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  53. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  54. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  55. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  56. data/lib/rubocop/cop/lint/require_relative_self_path.rb +5 -5
  57. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  58. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +1 -0
  59. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  60. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  61. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  62. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  63. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  64. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  65. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  66. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  67. data/lib/rubocop/cop/lint/to_enum_arguments.rb +35 -2
  68. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  69. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +4 -1
  70. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +35 -9
  71. data/lib/rubocop/cop/lint/useless_assignment.rb +10 -5
  72. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +7 -3
  73. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  74. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  75. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  76. data/lib/rubocop/cop/metrics/method_length.rb +1 -1
  77. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  78. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  79. data/lib/rubocop/cop/mixin/hash_transform_method.rb +4 -0
  80. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  81. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  82. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +9 -0
  83. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +9 -3
  84. data/lib/rubocop/cop/security/io_methods.rb +1 -1
  85. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  86. data/lib/rubocop/cop/style/accessor_grouping.rb +11 -1
  87. data/lib/rubocop/cop/style/alias.rb +1 -1
  88. data/lib/rubocop/cop/style/and_or.rb +1 -1
  89. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  90. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  91. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  92. data/lib/rubocop/cop/style/block_delimiters.rb +16 -2
  93. data/lib/rubocop/cop/style/case_equality.rb +14 -2
  94. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  95. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  96. data/lib/rubocop/cop/style/colon_method_call.rb +13 -6
  97. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  98. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  99. data/lib/rubocop/cop/style/concat_array_literals.rb +5 -1
  100. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -1
  101. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  102. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  103. data/lib/rubocop/cop/style/date_time.rb +2 -2
  104. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  105. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  106. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  107. data/lib/rubocop/cop/style/empty_class_definition.rb +8 -1
  108. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  109. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  110. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  111. data/lib/rubocop/cop/style/env_home.rb +4 -0
  112. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  113. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  114. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  115. data/lib/rubocop/cop/style/file_null.rb +4 -2
  116. data/lib/rubocop/cop/style/file_write.rb +17 -14
  117. data/lib/rubocop/cop/style/format_string.rb +13 -1
  118. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  119. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  120. data/lib/rubocop/cop/style/if_unless_modifier.rb +1 -1
  121. data/lib/rubocop/cop/style/if_with_semicolon.rb +9 -1
  122. data/lib/rubocop/cop/style/inline_comment.rb +1 -1
  123. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  124. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  125. data/lib/rubocop/cop/style/lambda.rb +7 -1
  126. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  127. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  128. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +6 -2
  129. data/lib/rubocop/cop/style/method_def_parentheses.rb +1 -1
  130. data/lib/rubocop/cop/style/min_max_comparison.rb +3 -0
  131. data/lib/rubocop/cop/style/multiline_if_then.rb +1 -1
  132. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  133. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  134. data/lib/rubocop/cop/style/mutable_constant.rb +105 -11
  135. data/lib/rubocop/cop/style/nil_lambda.rb +8 -0
  136. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  137. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  138. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  139. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  140. data/lib/rubocop/cop/style/parallel_assignment.rb +19 -3
  141. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +2 -0
  142. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  143. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  144. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  145. data/lib/rubocop/cop/style/redundant_format.rb +29 -0
  146. data/lib/rubocop/cop/style/redundant_line_continuation.rb +11 -3
  147. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  148. data/lib/rubocop/cop/style/redundant_self.rb +9 -0
  149. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +23 -4
  150. data/lib/rubocop/cop/style/semicolon.rb +20 -5
  151. data/lib/rubocop/cop/style/single_line_do_end_block.rb +17 -4
  152. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  153. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  154. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  155. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  156. data/lib/rubocop/cop/style/word_array.rb +1 -0
  157. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  158. data/lib/rubocop/formatter/disabled_config_formatter.rb +14 -7
  159. data/lib/rubocop/runner.rb +5 -3
  160. data/lib/rubocop/server/core.rb +6 -0
  161. data/lib/rubocop/version.rb +1 -1
  162. metadata +3 -3
@@ -128,8 +128,8 @@ module RuboCop
128
128
  end
129
129
 
130
130
  def ignored_gem?(node)
131
- ignored_gems = Array(cop_config['IgnoredGems'])
132
- ignored_gems.include?(node.first_argument.value)
131
+ allowed_gems = Array(cop_config['AllowedGems'])
132
+ allowed_gems.include?(node.first_argument.value)
133
133
  end
134
134
 
135
135
  def checked_options_present?(node)
@@ -163,7 +163,9 @@ module RuboCop
163
163
  def gem_options(node)
164
164
  return [] unless node.last_argument&.hash_type?
165
165
 
166
- node.last_argument.keys.map(&:value)
166
+ # Only literal keys carry an option name to check; a non-literal key
167
+ # (e.g. a variable or method call) has no `value` and must be skipped.
168
+ node.last_argument.keys.filter_map { |key| key.value if key.type?(:sym, :str) }
167
169
  end
168
170
  end
169
171
  end
@@ -27,7 +27,7 @@ module RuboCop
27
27
  if block_node.arguments?
28
28
  format(CORRECTION_WITH_ARGUMENTS,
29
29
  collection: collection_node.source,
30
- variables: argument_node.children.first.source)
30
+ variables: argument_node.children.map(&:source).join(', '))
31
31
  else
32
32
  format(CORRECTION_WITHOUT_ARGUMENTS, enumerable: collection_node.source)
33
33
  end
@@ -86,7 +86,13 @@ module RuboCop
86
86
  end
87
87
 
88
88
  def lambda_arg_string
89
- arguments.children.map(&:source).join(', ')
89
+ # Block-local (shadow) arguments are separated from regular arguments by a
90
+ # `;`; joining everything with `,` would turn them into extra parameters
91
+ # and change the lambda's arity.
92
+ regular, shadow = arguments.children.partition { |arg| !arg.shadowarg_type? }
93
+ arg_string = regular.map(&:source).join(', ')
94
+ arg_string += "; #{shadow.map(&:source).join(', ')}" unless shadow.empty?
95
+ arg_string
90
96
  end
91
97
 
92
98
  def needs_separating_space?
@@ -18,7 +18,14 @@ module RuboCop
18
18
  current_range = declaration_with_comment(node)
19
19
  previous_range = declaration_with_comment(previous_declaration)
20
20
 
21
- ->(corrector) { corrector.swap(current_range, previous_range) }
21
+ lambda do |corrector|
22
+ if current_range.source.end_with?("\n")
23
+ corrector.swap(current_range, previous_range)
24
+ else
25
+ corrector.insert_before(previous_range, "#{current_range.source}\n")
26
+ corrector.remove(current_range)
27
+ end
28
+ end
22
29
  end
23
30
 
24
31
  private
@@ -95,7 +95,7 @@ module RuboCop
95
95
  private
96
96
 
97
97
  def forbidden_gem?(gem_name)
98
- !cop_config['AllowedGems'].include?(gem_name)
98
+ !Array(cop_config['AllowedGems']).include?(gem_name)
99
99
  end
100
100
 
101
101
  def message(_range)
@@ -143,7 +143,10 @@ module RuboCop
143
143
  #{block_var}.metadata['rubygems_mfa_required'] = 'true'
144
144
  RUBY
145
145
 
146
- if (last_assignment = metadata_assignment(processed_source.ast).to_a.last)
146
+ # Scope the search to the current spec block. Searching the whole file
147
+ # would, for a second `Gem::Specification.new` block, insert the directive
148
+ # into the first block, leaving this block uncorrected and looping forever.
149
+ if (last_assignment = metadata_assignment(node).to_a.last)
147
150
  corrector.insert_after(last_assignment, "\n#{require_mfa_directive}")
148
151
  else
149
152
  corrector.insert_before(node.loc.end, "#{require_mfa_directive}\n")
@@ -46,8 +46,7 @@ module RuboCop
46
46
 
47
47
  def on_block(node)
48
48
  return unless let_rubocop_config_new?(node)
49
-
50
- describe = find_describe_method_node(node)
49
+ return unless (describe = find_describe_method_node(node))
51
50
 
52
51
  unless (exist_config = describe.last_argument.source == ':config')
53
52
  additional_message = ' and specify `:config` in `describe`'
@@ -65,7 +64,10 @@ module RuboCop
65
64
  private
66
65
 
67
66
  def find_describe_method_node(block_node)
68
- block_node.ancestors.find { |node| node.block_type? && node.method?(:describe) }.send_node
67
+ describe = block_node.ancestors.find do |ancestor|
68
+ ancestor.block_type? && ancestor.method?(:describe)
69
+ end
70
+ describe&.send_node
69
71
  end
70
72
  end
71
73
  end
@@ -18,6 +18,10 @@ module RuboCop
18
18
  # `either` (which is the default) : the `end` is allowed to be in either
19
19
  # location. The autocorrect will default to `start_of_line`.
20
20
  #
21
+ # When the `do` or `{` appears on a continuation line of multiline
22
+ # method arguments, the start of the line where the method is called
23
+ # is used as the alignment target instead of that continuation line.
24
+ #
21
25
  # @example EnforcedStyleAlignWith: either (default)
22
26
  # # bad
23
27
  #
@@ -116,6 +120,7 @@ module RuboCop
116
120
  end_loc = block_node.loc.end
117
121
  return unless begins_its_line?(end_loc)
118
122
 
123
+ start_node = start_for_line_node(block_node) if style == :start_of_line
119
124
  start_loc = start_node.source_range
120
125
  return unless start_loc.column != end_loc.column || style == :start_of_block
121
126
 
@@ -196,23 +201,34 @@ module RuboCop
196
201
 
197
202
  def compute_do_source_line_column(node, end_loc)
198
203
  do_loc = node.loc.begin # Actually it's either do or {.
204
+ anchor_loc = do_line_anchor_loc(node, do_loc)
199
205
 
200
206
  # We've found that "end" is not aligned with the start node (which
201
207
  # can be a block, a variable assignment, etc). But we also allow
202
208
  # the "end" to be aligned with the start of the line where the "do"
203
209
  # is, which is a style some people use in multi-line chains of
204
210
  # blocks.
205
- match = /\S.*/.match(do_loc.source_line)
211
+ match = /\S.*/.match(anchor_loc.source_line)
206
212
  indentation_of_do_line = match.begin(0)
207
- return unless end_loc.column != indentation_of_do_line || style == :start_of_line
213
+ permitted_columns = permitted_do_line_columns(do_loc, indentation_of_do_line)
214
+ return if permitted_columns.include?(end_loc.column) && style != :start_of_line
208
215
 
209
216
  {
210
217
  source: match[0],
211
- line: do_loc.line,
218
+ line: anchor_loc.line,
212
219
  column: indentation_of_do_line
213
220
  }
214
221
  end
215
222
 
223
+ # `end` aligned with an argument continuation line that holds the `do`
224
+ # was accepted before the anchor moved to the method dispatch line;
225
+ # keep accepting it so that such code does not become an offense.
226
+ def permitted_do_line_columns(do_loc, indentation_of_do_line)
227
+ columns = [indentation_of_do_line]
228
+ columns << (do_loc.source_line =~ /\S/) if style == :either
229
+ columns
230
+ end
231
+
216
232
  def loc_to_source_line_column(loc)
217
233
  {
218
234
  source: loc.source.lines.to_a.first.chomp,
@@ -239,7 +255,7 @@ module RuboCop
239
255
  def compute_start_col(ancestor_node, node)
240
256
  if style == :start_of_block
241
257
  do_loc = node.loc.begin
242
- return do_loc.source_line =~ /\S/
258
+ return do_line_anchor_loc(node, do_loc).source_line =~ /\S/
243
259
  end
244
260
  (ancestor_node || node).source_range.column
245
261
  end
@@ -253,6 +269,44 @@ module RuboCop
253
269
 
254
270
  corrector.remove(range)
255
271
  end
272
+
273
+ # When the `do` or `{` is on a continuation line of multiline method
274
+ # arguments, the indentation of that line is not a meaningful
275
+ # alignment target; anchor on the method dispatch position instead.
276
+ def do_line_anchor_loc(node, do_loc)
277
+ if do_line_begins_inside_argument?(node, do_loc)
278
+ node.send_node.selector || node.send_node.source_range
279
+ else
280
+ do_loc
281
+ end
282
+ end
283
+
284
+ # rubocop:disable Metrics/AbcSize
285
+ def do_line_begins_inside_argument?(node, do_loc)
286
+ line_begin_pos = do_loc.begin_pos - do_loc.column
287
+ first_char_pos = line_begin_pos + (do_loc.source_line =~ /\S/)
288
+ return false unless inside_parentheses?(node, first_char_pos)
289
+
290
+ (node.send_node.arguments + node.arguments).any? do |argument|
291
+ argument.source_range.begin_pos <= first_char_pos &&
292
+ first_char_pos < argument.source_range.end_pos
293
+ end
294
+ end
295
+ # rubocop:enable Metrics/AbcSize
296
+
297
+ # The continuation line indentation is only an unmeaningful alignment target when
298
+ # it is dictated by an opening delimiter, i.e. the line begins inside `(` or `[`.
299
+ # For a bare argument list without parentheses the indentation is the author's
300
+ # deliberate alignment, so the anchor must not move to the method dispatch line.
301
+ def inside_parentheses?(node, pos)
302
+ preceding_tokens = processed_source.tokens.select do |token|
303
+ token.begin_pos >= node.source_range.begin_pos && token.begin_pos < pos
304
+ end
305
+ opens = preceding_tokens.count { |token| token.left_parens? || token.left_bracket? }
306
+ closes = preceding_tokens.count { |token| token.right_parens? || token.right_bracket? }
307
+
308
+ opens > closes
309
+ end
256
310
  end
257
311
  end
258
312
  end
@@ -278,10 +278,14 @@ module RuboCop
278
278
 
279
279
  return [] unless class_def
280
280
 
281
- if class_def.type?(:def, :send)
282
- [class_def]
283
- else
281
+ # Only a multi-statement body (`begin`/`kwbegin`) wraps several elements; any
282
+ # single statement (`def`, `send`, `csend`, `if`, ...) is itself the sole element.
283
+ # Exploding such a node into its children would yield non-node values (e.g. a
284
+ # method-name `Symbol` from a `csend`) and crash later checks.
285
+ if class_def.type?(:begin, :kwbegin)
284
286
  class_def.children.compact
287
+ else
288
+ [class_def]
285
289
  end
286
290
  end
287
291
 
@@ -44,16 +44,26 @@ module RuboCop
44
44
  message = message(condition)
45
45
 
46
46
  add_offense(condition, message: message) do |corrector|
47
- range = range_by_whole_lines(condition.source_range, include_final_newline: true)
48
-
49
47
  corrector.insert_after(condition.parent.loc.keyword, " #{condition.source}")
50
- corrector.remove(range)
48
+ corrector.remove(removal_range(node, condition))
51
49
  end
52
50
  end
53
51
 
54
52
  def message(condition)
55
53
  format(MSG, keyword: condition.parent.keyword)
56
54
  end
55
+
56
+ # When a body statement shares the condition's line (e.g. `while\n cond; body\nend`),
57
+ # removing the whole line would delete the body too. In that case only remove the
58
+ # condition and its trailing separator, preserving the body statement.
59
+ def removal_range(node, condition)
60
+ body = node.body
61
+ if body && body.source_range.line == condition.source_range.last_line
62
+ range_between(condition.source_range.begin_pos, body.source_range.begin_pos)
63
+ else
64
+ range_by_whole_lines(condition.source_range, include_final_newline: true)
65
+ end
66
+ end
57
67
  end
58
68
  end
59
69
  end
@@ -95,8 +95,7 @@ module RuboCop
95
95
  end
96
96
 
97
97
  def autocorrect(corrector, node)
98
- previous_token = previous_token(node)
99
- range = if previous_token && same_line?(node, previous_token)
98
+ range = if inline_comment?(node)
100
99
  range_with_surrounding_space(node.source_range, newlines: false)
101
100
  else
102
101
  range_by_whole_lines(node.source_range, include_final_newline: true)
@@ -138,14 +137,13 @@ module RuboCop
138
137
  cop_config['AllowMarginComment']
139
138
  end
140
139
 
141
- def current_token(comment)
142
- processed_source.tokens.find { |token| token.pos == comment.source_range }
143
- end
144
-
145
- def previous_token(node)
146
- current_token = current_token(node)
147
- index = processed_source.tokens.index(current_token)
148
- index.zero? ? nil : processed_source.tokens[index - 1]
140
+ # A comment is inline when code precedes it on the same line. Detecting this
141
+ # from the source (rather than the token stream) is required because, for a
142
+ # comment trailing a heredoc opener, the preceding token is the heredoc end on
143
+ # a later line, which would wrongly trigger whole-line removal of the opener.
144
+ def inline_comment?(comment)
145
+ preceding_source = processed_source.lines[comment.loc.line - 1][0...comment.loc.column]
146
+ !preceding_source.strip.empty?
149
147
  end
150
148
  end
151
149
  end
@@ -274,7 +274,20 @@ module RuboCop
274
274
  end
275
275
 
276
276
  def end_loc(node)
277
- node.source_range.end
277
+ end_location = node.source_range.end
278
+ trailing_heredoc_end(node, end_location) || end_location
279
+ end
280
+
281
+ # For an endless method whose body is a heredoc (e.g. `def a = <<~TEXT`), the
282
+ # node's source range ends at the heredoc opening line, before the heredoc body.
283
+ # Use the heredoc's closing delimiter so the def's real end is located after the
284
+ # heredoc and blank-line insertion does not land inside the heredoc body.
285
+ def trailing_heredoc_end(node, end_location)
286
+ heredocs = node.each_descendant(:any_str).select(&:heredoc?)
287
+ return if heredocs.empty?
288
+
289
+ heredoc_end = heredocs.map { |heredoc| heredoc.loc.heredoc_end }.max_by(&:end_pos)
290
+ heredoc_end if heredoc_end.end_pos > end_location.end_pos
278
291
  end
279
292
 
280
293
  def autocorrect_remove_lines(corrector, newline_pos, count)
@@ -28,10 +28,10 @@ module RuboCop
28
28
  # # bad
29
29
  # hash = {
30
30
  # key: :value
31
- # }
32
- # and_in_a_method_call({
33
- # no: :difference
34
- # })
31
+ # }
32
+ # in_a_method_call({
33
+ # foo: :bar
34
+ # })
35
35
  # takes_multi_pairs_hash(x: {
36
36
  # a: 1,
37
37
  # b: 2
@@ -42,13 +42,12 @@ module RuboCop
42
42
  # })
43
43
  #
44
44
  # # good
45
- # special_inside_parentheses
46
45
  # hash = {
47
46
  # key: :value
48
47
  # }
49
- # but_in_a_method_call({
50
- # its_like: :this
51
- # })
48
+ # in_a_method_call({
49
+ # foo: :bar
50
+ # })
52
51
  # takes_multi_pairs_hash(x: {
53
52
  # a: 1,
54
53
  # b: 2
@@ -67,17 +66,17 @@ module RuboCop
67
66
  # # bad
68
67
  # hash = {
69
68
  # key: :value
70
- # }
71
- # but_in_a_method_call({
72
- # its_like: :this
73
- # })
69
+ # }
70
+ # in_a_method_call({
71
+ # foo: :bar
72
+ # })
74
73
  #
75
74
  # # good
76
75
  # hash = {
77
76
  # key: :value
78
77
  # }
79
- # and_in_a_method_call({
80
- # no: :difference
78
+ # in_a_method_call({
79
+ # foo: :bar
81
80
  # })
82
81
  #
83
82
  #
@@ -25,6 +25,17 @@ module RuboCop
25
25
  # end
26
26
  # end
27
27
  #
28
+ # @example
29
+ # # bad
30
+ # value = (
31
+ # foo - bar
32
+ # )
33
+ #
34
+ # # good
35
+ # value = (
36
+ # foo - bar
37
+ # )
38
+ #
28
39
  # @example AllowedPatterns: ['^\s*module']
29
40
  # # bad
30
41
  # module A
@@ -95,6 +106,16 @@ module RuboCop
95
106
  check_indentation(node.loc.end, node.children.first)
96
107
  end
97
108
 
109
+ def on_begin(node)
110
+ # Only a parenthesized grouping expression (e.g. `(\n foo\n)`) has
111
+ # explicit delimiters. Indent the body one step from the line
112
+ # the opening parenthesis is on, but only when the closing parenthesis is
113
+ # first on its line (a body on the opening line is skipped downstream).
114
+ return unless parentheses?(node) && begins_its_line?(node.loc.end)
115
+
116
+ check_indentation(opening_line_start(node.loc.begin), node.children.first)
117
+ end
118
+
98
119
  def on_block(node)
99
120
  end_loc = node.loc.end
100
121
 
@@ -188,6 +209,13 @@ module RuboCop
188
209
  AlignmentCorrector.correct(corrector, processed_source, node, @column_delta)
189
210
  end
190
211
 
212
+ # Returns a range at the first non-space column of the line the opening parenthesis is on,
213
+ # used as the base to indent the body from.
214
+ def opening_line_start(begin_loc)
215
+ column = begin_loc.source_line =~ /\S/
216
+ source_range(processed_source.buffer, begin_loc.line, column)
217
+ end
218
+
191
219
  def check_members(base, members)
192
220
  check_indentation(base, select_check_member(members.first))
193
221
 
@@ -204,10 +204,14 @@ module RuboCop
204
204
 
205
205
  def autocorrect(corrector, range, right_operand)
206
206
  range_source = range.source
207
+ # Match the operator exactly, not by substring, so compound assignments
208
+ # like `**=` and `/=` are not mistaken for `**` and `/` (which would drop
209
+ # the `=` and silently change the program's behavior).
210
+ operator = range_source.strip
207
211
 
208
- if range_source.include?('**') && !space_around_exponent_operator?
212
+ if operator == '**' && !space_around_exponent_operator?
209
213
  corrector.replace(range, '**')
210
- elsif range_source.include?('/') && !space_around_slash_operator?(right_operand)
214
+ elsif operator == '/' && !space_around_slash_operator?(right_operand)
211
215
  corrector.replace(range, '/')
212
216
  elsif range_source.end_with?("\n")
213
217
  corrector.replace(range, " #{range_source.strip}\n")
@@ -28,7 +28,7 @@ module RuboCop
28
28
  MISTAKES = { '=-' => '-=', '=+' => '+=', '=*' => '*=', '=!' => '!=' }.freeze
29
29
 
30
30
  def on_asgn(node)
31
- return unless (rhs = rhs(node))
31
+ return unless (rhs = node.expression)
32
32
 
33
33
  range = range_between(node.loc.operator.end_pos - 1, rhs.source_range.begin_pos + 1)
34
34
  source = range.source
@@ -38,16 +38,6 @@ module RuboCop
38
38
  end
39
39
 
40
40
  SIMPLE_ASSIGNMENT_TYPES.each { |asgn_type| alias_method :"on_#{asgn_type}", :on_asgn }
41
-
42
- private
43
-
44
- def rhs(node)
45
- if node.casgn_type?
46
- node.children[2]
47
- else
48
- node.children[1]
49
- end
50
- end
51
41
  end
52
42
  end
53
43
  end
@@ -44,13 +44,6 @@ module RuboCop
44
44
  RESTRICT_ON_SEND = PRECEDENCE.flatten.freeze
45
45
  MSG = 'Wrap expressions with varying precedence with parentheses to avoid ambiguity.'
46
46
 
47
- def on_new_investigation
48
- # Cache the precedence of each node being investigated
49
- # so that we only need to calculate it once
50
- @node_precedences = {}
51
- super
52
- end
53
-
54
47
  def on_and(node)
55
48
  return unless (parent = node.parent)
56
49
 
@@ -77,9 +70,7 @@ module RuboCop
77
70
  private
78
71
 
79
72
  def precedence(node)
80
- @node_precedences.fetch(node) do
81
- PRECEDENCE.index { |operators| operators.include?(operator_name(node)) }
82
- end
73
+ PRECEDENCE.index { |operators| operators.include?(operator_name(node)) }
83
74
  end
84
75
 
85
76
  def operator?(node)
@@ -78,13 +78,25 @@ module RuboCop
78
78
  end
79
79
 
80
80
  def allowed_construct?(asgn_node)
81
- asgn_node.begin_type? || conditional_assignment?(asgn_node)
81
+ return true if asgn_node.begin_type?
82
+
83
+ conditional_assignment?(asgn_node) || discarded_assignment?(asgn_node)
82
84
  end
83
85
 
84
86
  def conditional_assignment?(asgn_node)
85
87
  !asgn_node.loc.operator
86
88
  end
87
89
 
90
+ # An assignment that is a statement of a multi-statement `begin`
91
+ # (e.g. `(foo = bar; baz)`) has its value discarded, so it is not used
92
+ # as the condition. Wrapping it in parentheses would only conflict with
93
+ # `Style/RedundantParentheses`, so it is left alone.
94
+ def discarded_assignment?(asgn_node)
95
+ parent = asgn_node.parent
96
+
97
+ parent&.begin_type? && parent.children.size > 1
98
+ end
99
+
88
100
  def skip_children?(asgn_node)
89
101
  (asgn_node.call_type? && !asgn_node.assignment_method?) ||
90
102
  empty_condition?(asgn_node) ||
@@ -80,7 +80,6 @@ module RuboCop
80
80
  check_assignment_chain(arg_name, arg_value)
81
81
  end
82
82
 
83
- # rubocop:disable Metrics/AbcSize
84
83
  def check_assignment_chain(arg_name, node)
85
84
  return unless node.lvasgn_type?
86
85
 
@@ -88,7 +87,7 @@ module RuboCop
88
87
  current_node = node
89
88
 
90
89
  while current_node.lvasgn_type?
91
- seen_variables << current_node.children.first if current_node.lvasgn_type?
90
+ seen_variables << current_node.children.first
92
91
  current_node = current_node.children.last
93
92
  end
94
93
 
@@ -99,7 +98,6 @@ module RuboCop
99
98
 
100
99
  add_offense(current_node, message: format(MSG, arg_name: arg_name))
101
100
  end
102
- # rubocop:enable Metrics/AbcSize
103
101
  end
104
102
  end
105
103
  end
@@ -73,7 +73,6 @@ module RuboCop
73
73
  # require 'my_debugger/start'
74
74
  class Debugger < Base
75
75
  MSG = 'Remove debugger entry point `%<source>s`.'
76
- BLOCK_TYPES = %i[block numblock itblock kwbegin].freeze
77
76
 
78
77
  def on_send(node)
79
78
  return if assumed_usage_context?(node)
@@ -49,7 +49,7 @@ module RuboCop
49
49
  # Maybe further investigation of RuboCop AST will lead to an essential solution.
50
50
  return unless node.loc
51
51
 
52
- constant = node.absolute? ? constant_name(node, node.short_name) : node.source
52
+ constant = node.source.delete_prefix('::')
53
53
  return unless (deprecated_constant = deprecated_constants[constant])
54
54
 
55
55
  alternative = deprecated_constant['Alternative']
@@ -63,12 +63,6 @@ module RuboCop
63
63
 
64
64
  private
65
65
 
66
- def constant_name(node, nested_constant_name)
67
- return nested_constant_name.to_s unless node.namespace.const_type?
68
-
69
- constant_name(node.namespace, "#{node.namespace.short_name}::#{nested_constant_name}")
70
- end
71
-
72
66
  def message(good, bad, deprecated_version)
73
67
  deprecated_message = ", deprecated since Ruby #{deprecated_version}" if deprecated_version
74
68
 
@@ -77,7 +77,7 @@ module RuboCop
77
77
  return false unless processed_source.contains_comment?(node.source_range)
78
78
 
79
79
  line_comment = processed_source.comment_at_line(node.source_range.line)
80
- !line_comment || !comment_disables_cop?(line_comment.source)
80
+ !line_comment || !comment_disables_cop?(line_comment)
81
81
  end
82
82
 
83
83
  def allow_empty_lambdas?
@@ -85,8 +85,8 @@ module RuboCop
85
85
  end
86
86
 
87
87
  def comment_disables_cop?(comment)
88
- regexp_pattern = "# rubocop : (disable|todo) ([^,],)* (all|#{cop_name})"
89
- Regexp.new(regexp_pattern.gsub(' ', '\s*')).match?(comment)
88
+ directive = DirectiveComment.new(comment)
89
+ directive.disabled? && directive.cop_names.include?(cop_name)
90
90
  end
91
91
  end
92
92
  end
@@ -43,7 +43,25 @@ module RuboCop
43
43
  MSG = 'Do not return from an `ensure` block.'
44
44
 
45
45
  def on_ensure(node)
46
- node.branch&.each_node(:return) { |return_node| add_offense(return_node) }
46
+ node.branch&.each_node(:return) do |return_node|
47
+ next if return_from_inner_scope?(return_node, node)
48
+
49
+ add_offense(return_node)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # A `return` inside a nested method definition or lambda within the
56
+ # `ensure` returns from that inner scope, not from the method whose
57
+ # `ensure` this is, so it is not an offense. A `return` inside a plain
58
+ # block (or `proc`) does propagate out, so it remains an offense.
59
+ def return_from_inner_scope?(return_node, ensure_node)
60
+ return_node.each_ancestor do |ancestor|
61
+ break if ancestor == ensure_node
62
+ return true if ancestor.any_def_type? || (ancestor.any_block_type? && ancestor.lambda?)
63
+ end
64
+ false
47
65
  end
48
66
  end
49
67
  end
@@ -148,7 +148,9 @@ module RuboCop
148
148
  arguments = node.arguments
149
149
  overridden_kwargs = kwargs.dup
150
150
 
151
- overridden_kwargs[0] = "trim_mode: #{arguments[2].source}" if arguments[2]
151
+ if arguments[2] && !arguments[2].hash_type?
152
+ overridden_kwargs[0] = "trim_mode: #{arguments[2].source}"
153
+ end
152
154
 
153
155
  if arguments[3] && !arguments[3].hash_type?
154
156
  overridden_kwargs[1] = "eoutvar: #{arguments[3].source}"
@@ -104,6 +104,7 @@ module RuboCop
104
104
 
105
105
  def literal_safe?(node)
106
106
  return false unless node
107
+ return literal_safe?(node.children.first) if node.begin_type?
107
108
 
108
109
  (node.numeric_type? && node.value.zero?) || node.nil_type?
109
110
  end