rubocop 1.91.0 → 1.92.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 (236) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +3 -1
  4. data/lib/rubocop/cached_data.rb +47 -7
  5. data/lib/rubocop/changed_files.rb +35 -13
  6. data/lib/rubocop/cli/command/cop_names.rb +57 -0
  7. data/lib/rubocop/cli/command/execute_runner.rb +2 -2
  8. data/lib/rubocop/cli/command/explain.rb +34 -0
  9. data/lib/rubocop/cli/command/show_cops.rb +6 -0
  10. data/lib/rubocop/cli/command/show_docs_url.rb +6 -4
  11. data/lib/rubocop/cli.rb +3 -2
  12. data/lib/rubocop/comment_config.rb +17 -8
  13. data/lib/rubocop/config_loader.rb +6 -3
  14. data/lib/rubocop/config_loader_resolver.rb +37 -12
  15. data/lib/rubocop/config_obsoletion/removed_cop.rb +1 -1
  16. data/lib/rubocop/config_validator.rb +11 -0
  17. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  18. data/lib/rubocop/cop/base.rb +42 -21
  19. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  20. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  21. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  22. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  23. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  24. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +22 -7
  25. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  26. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +23 -2
  27. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  28. data/lib/rubocop/cop/documentation.rb +1 -1
  29. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  30. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  31. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  32. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  33. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  34. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  35. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  36. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  37. data/lib/rubocop/cop/layout/class_structure.rb +9 -1
  38. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  39. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +14 -5
  40. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  41. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  42. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +1 -1
  43. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  44. data/lib/rubocop/cop/layout/end_alignment.rb +16 -10
  45. data/lib/rubocop/cop/layout/extra_spacing.rb +4 -0
  46. data/lib/rubocop/cop/layout/first_argument_indentation.rb +1 -0
  47. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +5 -3
  48. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  49. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  50. data/lib/rubocop/cop/layout/hash_alignment.rb +16 -12
  51. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +2 -0
  52. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  53. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  54. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  55. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  56. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  57. data/lib/rubocop/cop/layout/line_length.rb +52 -18
  58. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  59. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  60. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +7 -0
  61. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  62. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +14 -0
  63. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  64. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  65. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +15 -2
  66. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  67. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  68. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +6 -11
  69. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  70. data/lib/rubocop/cop/layout/space_before_block_braces.rb +4 -1
  71. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  72. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +31 -8
  73. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  74. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  75. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  76. data/lib/rubocop/cop/lint/literal_as_condition.rb +15 -0
  77. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  78. data/lib/rubocop/cop/lint/loop.rb +24 -2
  79. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +71 -11
  80. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +18 -3
  81. data/lib/rubocop/cop/lint/number_conversion.rb +1 -1
  82. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +12 -1
  83. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  84. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  85. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  86. data/lib/rubocop/cop/lint/void.rb +8 -1
  87. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  88. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  89. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  90. data/lib/rubocop/cop/mixin/alignment.rb +4 -0
  91. data/lib/rubocop/cop/mixin/check_line_breakable.rb +17 -14
  92. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +5 -1
  93. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  94. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  95. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +69 -0
  96. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  97. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +15 -1
  98. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +26 -2
  99. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  100. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  101. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  102. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  103. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  104. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +17 -2
  105. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  106. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  107. data/lib/rubocop/cop/mixin/statement_modifier.rb +22 -2
  108. data/lib/rubocop/cop/mixin.rb +2 -0
  109. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  110. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  111. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  112. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  113. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  114. data/lib/rubocop/cop/offense.rb +59 -3
  115. data/lib/rubocop/cop/registry.rb +1 -1
  116. data/lib/rubocop/cop/security/json_load.rb +16 -4
  117. data/lib/rubocop/cop/style/access_modifier_declarations.rb +7 -0
  118. data/lib/rubocop/cop/style/alias.rb +18 -5
  119. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  120. data/lib/rubocop/cop/style/and_or.rb +4 -0
  121. data/lib/rubocop/cop/style/arguments_forwarding.rb +4 -1
  122. data/lib/rubocop/cop/style/attr.rb +3 -4
  123. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  124. data/lib/rubocop/cop/style/block_delimiters.rb +29 -3
  125. data/lib/rubocop/cop/style/case_like_if.rb +6 -3
  126. data/lib/rubocop/cop/style/class_and_module_children.rb +54 -16
  127. data/lib/rubocop/cop/style/class_methods_definitions.rb +8 -0
  128. data/lib/rubocop/cop/style/combinable_loops.rb +85 -11
  129. data/lib/rubocop/cop/style/concat_array_literals.rb +46 -7
  130. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  131. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +65 -9
  132. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  133. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  134. data/lib/rubocop/cop/style/empty_method.rb +3 -1
  135. data/lib/rubocop/cop/style/endless_method.rb +67 -6
  136. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  137. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  138. data/lib/rubocop/cop/style/float_division.rb +22 -2
  139. data/lib/rubocop/cop/style/for.rb +40 -2
  140. data/lib/rubocop/cop/style/format_string.rb +28 -6
  141. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  142. data/lib/rubocop/cop/style/guard_clause.rb +9 -1
  143. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  144. data/lib/rubocop/cop/style/hash_lookup_method.rb +32 -9
  145. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  146. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  147. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  148. data/lib/rubocop/cop/style/if_unless_modifier.rb +19 -20
  149. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  150. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  151. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  152. data/lib/rubocop/cop/style/it_block_parameter.rb +32 -3
  153. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  154. data/lib/rubocop/cop/style/lambda.rb +18 -0
  155. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  156. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  157. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +45 -10
  158. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +25 -2
  159. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  160. data/lib/rubocop/cop/style/method_def_parentheses.rb +29 -3
  161. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  162. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  163. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  164. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  165. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  166. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  167. data/lib/rubocop/cop/style/mutable_constant.rb +28 -1
  168. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  169. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  170. data/lib/rubocop/cop/style/nested_modifier.rb +27 -8
  171. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  172. data/lib/rubocop/cop/style/next.rb +24 -10
  173. data/lib/rubocop/cop/style/nil_comparison.rb +16 -4
  174. data/lib/rubocop/cop/style/non_nil_check.rb +1 -1
  175. data/lib/rubocop/cop/style/not.rb +10 -5
  176. data/lib/rubocop/cop/style/numeric_predicate.rb +21 -5
  177. data/lib/rubocop/cop/style/one_line_conditional.rb +13 -2
  178. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  179. data/lib/rubocop/cop/style/reduce_to_hash.rb +17 -1
  180. data/lib/rubocop/cop/style/redundant_begin.rb +20 -3
  181. data/lib/rubocop/cop/style/redundant_condition.rb +30 -13
  182. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +6 -0
  183. data/lib/rubocop/cop/style/redundant_exception.rb +15 -3
  184. data/lib/rubocop/cop/style/redundant_fetch_block.rb +9 -1
  185. data/lib/rubocop/cop/style/redundant_freeze.rb +10 -1
  186. data/lib/rubocop/cop/style/redundant_parentheses.rb +12 -1
  187. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  188. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +20 -0
  189. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  190. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  191. data/lib/rubocop/cop/style/require_order.rb +9 -0
  192. data/lib/rubocop/cop/style/rescue_modifier.rb +5 -9
  193. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  194. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  195. data/lib/rubocop/cop/style/sample.rb +9 -8
  196. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  197. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  198. data/lib/rubocop/cop/style/single_line_methods.rb +16 -4
  199. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  200. data/lib/rubocop/cop/style/string_concatenation.rb +9 -3
  201. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  202. data/lib/rubocop/cop/style/symbol_proc.rb +23 -5
  203. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  204. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  205. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  206. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  207. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  208. data/lib/rubocop/cop/team.rb +10 -5
  209. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  210. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  211. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  212. data/lib/rubocop/formatter/formatter_set.rb +4 -0
  213. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  214. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  215. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  216. data/lib/rubocop/formatter/sarif_formatter.rb +12 -7
  217. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  218. data/lib/rubocop/lsp/runtime.rb +47 -11
  219. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  220. data/lib/rubocop/lsp.rb +5 -2
  221. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  222. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  223. data/lib/rubocop/mcp/scope.rb +65 -0
  224. data/lib/rubocop/mcp/server.rb +184 -76
  225. data/lib/rubocop/mcp/source_files.rb +35 -0
  226. data/lib/rubocop/offense_limit.rb +30 -0
  227. data/lib/rubocop/options.rb +45 -10
  228. data/lib/rubocop/plugin/configuration_integrator.rb +12 -5
  229. data/lib/rubocop/remote_config.rb +4 -1
  230. data/lib/rubocop/result_cache.rb +20 -13
  231. data/lib/rubocop/runner.rb +109 -41
  232. data/lib/rubocop/server/cache.rb +2 -2
  233. data/lib/rubocop/util.rb +69 -0
  234. data/lib/rubocop/version.rb +1 -1
  235. data/lib/rubocop.rb +5 -0
  236. metadata +16 -5
@@ -98,6 +98,8 @@ module RuboCop
98
98
  end
99
99
  elsif node.condition.falsey_literal?
100
100
  add_offense(node.condition) do |corrector|
101
+ next if orphans_loop_control_keyword?(node)
102
+
101
103
  corrector.replace(node, node.body.child_nodes.map(&:source).join("\n"))
102
104
  end
103
105
  end
@@ -127,6 +129,8 @@ module RuboCop
127
129
  end
128
130
  elsif node.condition.truthy_literal?
129
131
  add_offense(node.condition) do |corrector|
132
+ next if orphans_loop_control_keyword?(node)
133
+
130
134
  corrector.replace(node, node.body.child_nodes.map(&:source).join("\n"))
131
135
  end
132
136
  end
@@ -175,6 +179,15 @@ module RuboCop
175
179
 
176
180
  private
177
181
 
182
+ # Unwrapping the body of a loop that runs once would leave a `break`,
183
+ # `next` or `redo` that belongs to it outside of any loop.
184
+ def orphans_loop_control_keyword?(node)
185
+ node.body.each_node(:next, :break, :redo).any? do |control|
186
+ inner = control.each_ancestor.take_while { |ancestor| !ancestor.equal?(node) }
187
+ inner.none? { |ancestor| ancestor.any_block_type? || ancestor.loop_keyword? }
188
+ end
189
+ end
190
+
178
191
  def void_value_expression?(node)
179
192
  node = node.children.last while node&.begin_type?
180
193
 
@@ -273,6 +286,8 @@ module RuboCop
273
286
  "#{node.else_branch.source.sub('elsif', 'if')}\nend"
274
287
  elsif node.else? || node.ternary?
275
288
  node.else_branch.source
289
+ elsif node.value_used?
290
+ 'nil'
276
291
  else
277
292
  '' # Equivalent to removing the node
278
293
  end
@@ -24,30 +24,16 @@ module RuboCop
24
24
  MSG = 'Literal interpolation detected.'
25
25
  COMPOSITE = %i[array hash pair irange erange].freeze
26
26
 
27
- # rubocop:disable-next Metrics/AbcSize
28
27
  def on_interpolation(begin_node)
29
28
  final_node = begin_node.children.last
30
29
  return unless offending?(final_node)
31
-
32
- # %W and %I split the content into words before expansion
33
- # treating each interpolation as a word component, so
34
- # interpolation should not be removed if the expanded value
35
- # contains a space character.
36
- expanded_value = autocorrected_value(final_node)
37
- expanded_value = handle_special_regexp_chars(begin_node, expanded_value)
38
-
39
- return if in_array_percent_literal?(begin_node) && /\s|\A\z/.match?(expanded_value)
30
+ return unless (expanded_value = correctable_value(begin_node, final_node))
40
31
 
41
32
  add_offense(final_node) do |corrector|
42
33
  next if final_node.dstr_type? # nested, fixed in next iteration
34
+ next if follows_literal_interpolation?(begin_node)
43
35
 
44
- replacement = if final_node.str_type? && !final_node.value.valid_encoding?
45
- final_node.source.delete_prefix('"').delete_suffix('"')
46
- else
47
- expanded_value
48
- end
49
-
50
- corrector.replace(final_node.parent, replacement)
36
+ corrector.replace(final_node.parent, replacement(final_node, expanded_value))
51
37
  end
52
38
  end
53
39
 
@@ -73,6 +59,22 @@ module RuboCop
73
59
  node.array_type? && grandparent.regexp_type?
74
60
  end
75
61
 
62
+ def correctable_value(begin_node, final_node)
63
+ value = autocorrected_value(final_node)
64
+ value = handle_special_regexp_chars(begin_node, value)
65
+ return if starts_interpolation_after_hash_sign?(begin_node, value)
66
+
67
+ value = escape_trailing_hash_sign(begin_node, value)
68
+
69
+ # %W and %I split the content into words before expansion
70
+ # treating each interpolation as a word component, so
71
+ # interpolation should not be removed if the expanded value
72
+ # contains a space character.
73
+ return if in_array_percent_literal?(begin_node) && /\s|\A\z/.match?(value)
74
+
75
+ value
76
+ end
77
+
76
78
  # rubocop:disable-next Metrics/MethodLength, Metrics/CyclomaticComplexity
77
79
  def autocorrected_value(node)
78
80
  case node.type
@@ -116,6 +118,17 @@ module RuboCop
116
118
  end
117
119
  end
118
120
 
121
+ def starts_interpolation_after_hash_sign?(begin_node, value)
122
+ source = processed_source.buffer.source
123
+ range = begin_node.source_range
124
+ # Removing an empty value puts the `#` right before what follows the interpolation.
125
+ following = "#{value}#{source[range.end_pos]}"
126
+ return false unless following.start_with?('{', '@', '$')
127
+ return false unless source[range.begin_pos - 1] == '#'
128
+
129
+ unescaped_hash_sign?(source[0...range.begin_pos])
130
+ end
131
+
119
132
  def autocorrected_value_for_string(node)
120
133
  return node.source.delete_prefix('"').delete_suffix('"') unless node.value.valid_encoding?
121
134
 
@@ -180,7 +193,9 @@ module RuboCop
180
193
  end
181
194
 
182
195
  def space_literal?(node)
183
- node.str_type? && node.value.valid_encoding? && node.value.blank?
196
+ # `String#blank?` misses Unicode spaces such as U+3000 that
197
+ # `Layout/TrailingWhitespace` treats as trailing whitespace.
198
+ node.str_type? && node.value.valid_encoding? && node.value.match?(/\A[[:space:]]*\z/)
184
199
  end
185
200
 
186
201
  def ends_heredoc_line?(node)
@@ -188,7 +203,8 @@ module RuboCop
188
203
  return false unless grandparent&.dstr_type? && grandparent.heredoc?
189
204
 
190
205
  line = processed_source.lines[node.last_line - 1]
191
- line.size == node.loc.last_column + 1
206
+
207
+ line.size == node.parent.source_range.last_column
192
208
  end
193
209
 
194
210
  def in_array_percent_literal?(node)
@@ -198,6 +214,34 @@ module RuboCop
198
214
  grandparent = parent.parent
199
215
  grandparent&.array_type? && grandparent.percent_literal?
200
216
  end
217
+
218
+ def escape_trailing_hash_sign(begin_node, value)
219
+ following = processed_source.buffer.source[begin_node.source_range.end_pos]
220
+ return value unless %w[{ @ $].include?(following) && unescaped_hash_sign?(value)
221
+
222
+ "#{value.delete_suffix('#')}\\#"
223
+ end
224
+
225
+ def unescaped_hash_sign?(text)
226
+ text.end_with?('#') && text.delete_suffix('#')[/\\*\z/].length.even?
227
+ end
228
+
229
+ # Each interpolation is checked against the original source, so correcting it in the
230
+ # same pass as a preceding one that leaves a `#` (or nothing) could join that `#` and
231
+ # a `{` from this one. It's left for the next iteration instead.
232
+ def follows_literal_interpolation?(begin_node)
233
+ previous = begin_node.left_sibling
234
+ return false unless previous&.begin_type? && offending?(previous.children.last)
235
+
236
+ value = correctable_value(previous, previous.children.last)
237
+ value && (value.empty? || value.end_with?('#'))
238
+ end
239
+
240
+ def replacement(node, expanded_value)
241
+ return expanded_value unless node.str_type? && !node.value.valid_encoding?
242
+
243
+ node.source.delete_prefix('"').delete_suffix('"')
244
+ end
201
245
  end
202
246
  end
203
247
  end
@@ -42,6 +42,8 @@ module RuboCop
42
42
  # break if some_condition
43
43
  # end
44
44
  class Loop < Base
45
+ include Alignment
46
+ include RangeHelp
45
47
  extend AutoCorrector
46
48
 
47
49
  MSG = 'Use `Kernel#loop` with `break` rather than `begin/end/until`(or `while`).'
@@ -62,7 +64,8 @@ module RuboCop
62
64
  add_offense(node.loc.keyword) do |corrector|
63
65
  corrector.replace(body.loc.begin, 'loop do')
64
66
  corrector.remove(keyword_and_condition_range(node))
65
- corrector.insert_before(body.loc.end, build_break_line(node))
67
+
68
+ insert_break_line(corrector, node, body)
66
69
  end
67
70
  end
68
71
 
@@ -70,9 +73,28 @@ module RuboCop
70
73
  node.body.loc.end.end.join(node.source_range.end)
71
74
  end
72
75
 
76
+ def insert_break_line(corrector, node, body)
77
+ if body.single_line?
78
+ corrector.replace(space_before_end(body), build_break_line(node))
79
+ else
80
+ corrector.insert_before(body.loc.end, build_break_line(node))
81
+ end
82
+ end
83
+
84
+ def space_before_end(body)
85
+ range = range_with_surrounding_space(range: body.loc.end, side: :left, newlines: false)
86
+
87
+ range.with(end_pos: body.loc.end.begin_pos)
88
+ end
89
+
73
90
  def build_break_line(node)
74
91
  conditional_keyword = node.while_post_type? ? 'unless' : 'if'
75
- "break #{conditional_keyword} #{node.condition.source}\n#{indent(node)}"
92
+ break_line = "break #{conditional_keyword} #{node.condition.source}\n#{indent(node)}"
93
+ return break_line unless node.body.single_line?
94
+
95
+ # A single-line `begin ... end` keeps its body and `end` on one line,
96
+ # so the `break` has to start a line of its own.
97
+ "\n#{indent(node, offset: configured_indentation_width)}#{break_line}"
76
98
  end
77
99
  end
78
100
  end
@@ -116,31 +116,88 @@ module RuboCop
116
116
  end
117
117
 
118
118
  def first_code_token
119
- @first_code_token ||= processed_source.sorted_tokens.find { |token| !token.comment? }
119
+ processed_source.sorted_tokens.find { |token| !token.comment? }
120
120
  end
121
121
 
122
122
  def check_encoding_comment(comment)
123
+ return unless misplaced_encoding_position?(comment)
124
+
125
+ add_offense(comment, message: MSG_ENCODING) do |corrector|
126
+ correct_encoding_comment(corrector, comment)
127
+ end
128
+ end
129
+
130
+ def misplaced_encoding_position?(comment)
123
131
  line = comment.source_range.line
124
- return if line == effective_encoding_line && comment_starts_line?(comment)
132
+ return false if line == effective_encoding_line && comment_starts_line?(comment)
133
+
125
134
  # Leave `# frozen_string_literal: true` + `# encoding: x` runs at the
126
135
  # very top to Lint/OrderedMagicComments, which already reorders them.
127
- return if preceded_only_by_magic_comments?(comment)
136
+ !preceded_only_by_magic_comments?(comment)
137
+ end
128
138
 
129
- add_offense(comment, message: MSG_ENCODING) do |corrector|
139
+ # With an effective encoding comment already in place, moving another
140
+ # one in would override it. Drop an identical duplicate and leave a
141
+ # conflicting one for a human to resolve.
142
+ def correct_encoding_comment(corrector, comment)
143
+ effective_comment = effective_encoding_comment
144
+
145
+ if effective_comment.nil?
130
146
  move_comment(corrector, comment, effective_encoding_line)
147
+ elsif comment_encoding(effective_comment) == comment_encoding(comment)
148
+ corrector.remove(comment_removal_range(comment))
131
149
  end
132
150
  end
133
151
 
152
+ def comment_encoding(comment)
153
+ Encoding.find(MagicComment.parse(comment.text).encoding)
154
+ rescue ArgumentError
155
+ nil
156
+ end
157
+
134
158
  def check_top_block_comment(comment, directive)
135
159
  return unless first_code_token
136
160
  return unless comment.source_range.begin_pos > first_code_token.begin_pos
137
161
 
138
162
  message = format(MSG_AFTER_CODE, directive: directive)
139
163
  add_offense(comment, message: message) do |corrector|
140
- move_comment(corrector, comment, effective_encoding_line)
164
+ # Both would be inserted at the top, in no reliable order. Leave
165
+ # this one to the next pass, once the encoding comment is in place.
166
+ next if encoding_comment_moving_in?
167
+
168
+ move_comment(corrector, comment, top_block_insertion_line)
141
169
  end
142
170
  end
143
171
 
172
+ def encoding_comment_moving_in?
173
+ return false if effective_encoding_comment
174
+
175
+ processed_source.comments.any? do |comment|
176
+ known_encoding_comment?(comment) && misplaced_encoding_position?(comment)
177
+ end
178
+ end
179
+
180
+ def effective_encoding_comment
181
+ processed_source.comments.find do |comment|
182
+ comment.source_range.line == effective_encoding_line &&
183
+ comment_starts_line?(comment) &&
184
+ MagicComment.parse(comment.text).encoding_specified?
185
+ end
186
+ end
187
+
188
+ def known_encoding_comment?(comment)
189
+ return false unless magic_comment_shaped?(comment.text)
190
+
191
+ magic_comment = MagicComment.parse(comment.text)
192
+ magic_comment.encoding_specified? && known_encoding?(magic_comment.encoding)
193
+ end
194
+
195
+ # Insert below an effective encoding comment rather than above it,
196
+ # which would push it off the only lines where Ruby honors it.
197
+ def top_block_insertion_line
198
+ effective_encoding_comment ? effective_encoding_line + 1 : effective_encoding_line
199
+ end
200
+
144
201
  def check_magic_comment_above_shebang
145
202
  shebang_comment = processed_source.comments.find do |comment|
146
203
  comment.source_range.line > 1 && comment.text.start_with?('#!') &&
@@ -166,12 +223,7 @@ module RuboCop
166
223
  end
167
224
 
168
225
  def move_comment(corrector, comment, target_line)
169
- removal_range = if comment_starts_line?(comment)
170
- range_by_whole_lines(comment.source_range, include_final_newline: true)
171
- else
172
- range_with_surrounding_space(comment.source_range, side: :left)
173
- end
174
- corrector.remove(removal_range)
226
+ corrector.remove(comment_removal_range(comment))
175
227
  target_range = processed_source.buffer.line_range(target_line)
176
228
  if comment.source_range.line < target_line
177
229
  corrector.insert_after(target_range, "\n#{comment.text}")
@@ -179,6 +231,14 @@ module RuboCop
179
231
  corrector.insert_before(target_range, "#{comment.text}\n")
180
232
  end
181
233
  end
234
+
235
+ def comment_removal_range(comment)
236
+ if comment_starts_line?(comment)
237
+ range_by_whole_lines(comment.source_range, include_final_newline: true)
238
+ else
239
+ range_with_surrounding_space(comment.source_range, side: :left)
240
+ end
241
+ end
182
242
  end
183
243
  end
184
244
  end
@@ -62,7 +62,7 @@ module RuboCop
62
62
  each_missing_enable do |cop, line_range|
63
63
  next if acceptable_range?(cop, line_range)
64
64
 
65
- comment = processed_source.comment_at_line(line_range.begin)
65
+ comment = opening_comment(line_range)
66
66
 
67
67
  add_offense(comment, message: message(cop, comment))
68
68
  end
@@ -73,14 +73,29 @@ module RuboCop
73
73
  def each_missing_enable
74
74
  processed_source.disabled_line_ranges.each do |cop, line_ranges|
75
75
  line_ranges.each do |line_range|
76
- # A `disable-next` scope closes itself with its statement.
77
- next if line_range.respond_to?(:directive) && line_range.directive.disable_next?
76
+ # A `disable-next` or `next` scope closes itself with its statement.
77
+ next if statement_scoped?(line_range)
78
78
 
79
79
  yield cop, line_range
80
80
  end
81
81
  end
82
82
  end
83
83
 
84
+ def statement_scoped?(line_range)
85
+ directive = line_range.respond_to?(:directive) && line_range.directive
86
+ directive && (directive.disable_next? || directive.next?)
87
+ end
88
+
89
+ # A disable suspended by `enable-next` or `next +Cop` reopens below the
90
+ # statement, on a code line, so the comment that opened it has to come
91
+ # from the range rather than from its first line.
92
+ def opening_comment(line_range)
93
+ directive = line_range.respond_to?(:directive) && line_range.directive
94
+ return directive.comment if directive
95
+
96
+ processed_source.comment_at_line(line_range.begin)
97
+ end
98
+
84
99
  def acceptable_range?(cop, line_range)
85
100
  # This has to remain a strict inequality to handle
86
101
  # the case when max_range is Float::INFINITY
@@ -132,7 +132,7 @@ module RuboCop
132
132
 
133
133
  def handle_as_symbol(node)
134
134
  to_method_symbol(node) do |receiver, sym_node, to_method|
135
- next if receiver.nil? || !node.arguments.one?
135
+ next if receiver.nil? || !node.arguments.one? || node.operator_method?
136
136
 
137
137
  message = format(
138
138
  MSG,
@@ -110,7 +110,7 @@ module RuboCop
110
110
  # A pending cop that is not enabled in this run produces no
111
111
  # offenses, so its directives cannot be judged - they typically
112
112
  # prepare the code for the moment the cop gets enabled.
113
- next if pending_cop_not_run?(cop)
113
+ next if pending_cop_not_run?(cop) || disabled_by_preview?(cop)
114
114
 
115
115
  each_already_disabled(cop, line_ranges, &block)
116
116
  each_line_range(cop, line_ranges, &block)
@@ -128,6 +128,17 @@ module RuboCop
128
128
  !processed_source.registry.enabled_pending_cop?(cop_cfg, config, cop)
129
129
  end
130
130
 
131
+ # A cop that only the `Preview` defaults disable runs again as soon as
132
+ # preview is turned off, so its directives are still needed.
133
+ def disabled_by_preview?(cop)
134
+ return false unless preview? && all_cop_names.include?(cop)
135
+
136
+ preview_defaults = ConfigLoader.default_configuration[cop]&.fetch('Preview', nil)
137
+ return false unless preview_defaults.is_a?(Hash) && preview_defaults['Enabled'] == false
138
+
139
+ !config.cop_enabled?(cop)
140
+ end
141
+
131
142
  def each_line_range(cop, line_ranges)
132
143
  line_ranges.each_with_index do |line_range, line_range_index|
133
144
  next if should_skip_line_range?(cop, line_range)
@@ -28,6 +28,7 @@ module RuboCop
28
28
  # end
29
29
  #
30
30
  class RedundantDirGlobSort < Base
31
+ include RangeHelp
31
32
  extend AutoCorrector
32
33
  extend TargetRubyVersion
33
34
 
@@ -46,8 +47,7 @@ module RuboCop
46
47
  selector = node.loc.selector
47
48
 
48
49
  add_offense(selector) do |corrector|
49
- corrector.remove(selector)
50
- corrector.remove(node.loc.dot)
50
+ autocorrect(corrector, node.loc.dot, selector)
51
51
  end
52
52
  end
53
53
 
@@ -67,6 +67,17 @@ module RuboCop
67
67
  def sort_with_comparator?(node)
68
68
  node.parent&.any_block_type? || node.last_argument&.block_pass_type?
69
69
  end
70
+
71
+ def autocorrect(corrector, dot, selector)
72
+ range = dot.join(selector)
73
+
74
+ if processed_source.contains_comment?(range)
75
+ corrector.remove(dot)
76
+ corrector.remove(selector)
77
+ else
78
+ corrector.remove(range_with_surrounding_space(range, side: :left))
79
+ end
80
+ end
70
81
  end
71
82
  end
72
83
  end
@@ -53,12 +53,22 @@ module RuboCop
53
53
  return if node.each_descendant(:dstr).any?
54
54
 
55
55
  regexp_constructor(node) do |text|
56
+ next unless verbatim_string?(text)
57
+
56
58
  detect_offenses_in_tree(text, parse_regexp(text.value))
57
59
  end
58
60
  end
59
61
 
60
62
  private
61
63
 
64
+ # Offsets into the string's value only point at the right place in the source
65
+ # when the string has no escape sequences, so anything else is left alone.
66
+ def verbatim_string?(node)
67
+ return false unless node.loc?(:begin) && node.loc?(:end)
68
+
69
+ node.loc.begin.end.join(node.loc.end.begin).source == node.value
70
+ end
71
+
62
72
  # When a character class opens with a bare `]` (e.g. `[^]]`), `regexp_parser` parses
63
73
  # `[^]` / `[]` as an empty set and reports the closing `]` as a separate literal.
64
74
  # Ruby treats that `]` as the end of the class, not as an unescaped bracket,
@@ -97,7 +107,7 @@ module RuboCop
97
107
  location = range_at_index(node, expr.ts, pos)
98
108
 
99
109
  add_offense(location) do |corrector|
100
- corrector.replace(location, '\]')
110
+ corrector.replace(location, escaped_bracket(node))
101
111
  end
102
112
  end
103
113
 
@@ -108,6 +118,13 @@ module RuboCop
108
118
  adjustment = index + offset
109
119
  node.loc.begin.end.adjust(begin_pos: adjustment, end_pos: adjustment + 1)
110
120
  end
121
+
122
+ # A double-quoted string turns `\]` into `]`, so its backslash needs escaping too.
123
+ def escaped_bracket(node)
124
+ return '\]' if node.regexp_type? || node.single_quoted? || node.percent_literal?(:q)
125
+
126
+ '\\\\]'
127
+ end
111
128
  end
112
129
  end
113
130
  end
@@ -212,16 +212,23 @@ module RuboCop
212
212
  end
213
213
 
214
214
  def remove_local_variable_assignment_part(corrector, node)
215
- corrector.remove(node.loc.name.begin.join(node.expression.source_range.begin))
216
- end
217
-
218
- def variable_in_loop_condition?(assignment_node, variable)
219
- return false if assignment_node.each_ancestor(:any_def).any?
215
+ expression = node.expression
216
+ assignment_part = node.loc.name.begin.join(expression.source_range.begin)
220
217
 
221
- loop_node = assignment_node.each_ancestor.find do |ancestor|
222
- ancestor.type?(*VariableForce::LOOP_TYPES)
218
+ # `x = 1, 2` cannot become `1, 2`, so the values become an array literal.
219
+ if expression.array_type? && !expression.bracketed?
220
+ corrector.replace(assignment_part, '[')
221
+ corrector.insert_after(expression, ']')
222
+ else
223
+ corrector.remove(assignment_part)
223
224
  end
225
+ end
224
226
 
227
+ def variable_in_loop_condition?(assignment_node, variable)
228
+ # A method defined inside the loop has its own scope, so the loop condition
229
+ # can only read the variable if the loop is nearer than any method definition.
230
+ loop_node = assignment_node.each_ancestor(*VariableForce::LOOP_TYPES, :any_def).first
231
+ return false if !loop_node || loop_node.any_def_type?
225
232
  return false unless loop_node.respond_to?(:condition)
226
233
 
227
234
  condition_node = loop_node.condition
@@ -271,7 +271,14 @@ module RuboCop
271
271
  return if node.parent.type?(:if, :case, :when, :case_match, :in_pattern)
272
272
  return if (def_node = node.each_ancestor(:any_def).first) && def_node.assignment_method?
273
273
 
274
- corrector.remove(range_with_surrounding_space(range: node.source_range, side: :left))
274
+ range = range_with_surrounding_space(range: void_expression_range(node), side: :left)
275
+ corrector.remove(range)
276
+ end
277
+
278
+ def void_expression_range(node)
279
+ return node.source_range unless node.respond_to?(:heredoc?) && node.heredoc?
280
+
281
+ node.source_range.join(node.loc.heredoc_end)
275
282
  end
276
283
 
277
284
  def autocorrect_nonmutating_send(corrector, node, suggestion)
@@ -40,6 +40,8 @@ module RuboCop
40
40
  class ClassLength < Base
41
41
  include CodeLength
42
42
 
43
+ LABEL = 'Class'
44
+
43
45
  def on_class(node)
44
46
  check_code_length(node)
45
47
  end
@@ -60,8 +62,8 @@ module RuboCop
60
62
 
61
63
  private
62
64
 
63
- def message(length, max_length)
64
- format('Class has too many lines. [%<length>d/%<max>d]', length: length, max: max_length)
65
+ def cop_label
66
+ LABEL
65
67
  end
66
68
 
67
69
  def find_expression_within_parent(parent)
@@ -48,10 +48,6 @@ module RuboCop
48
48
 
49
49
  1
50
50
  end
51
-
52
- def count_block?(block)
53
- KNOWN_ITERATING_METHODS.include? block.method_name
54
- end
55
51
  end
56
52
  end
57
53
  end
@@ -38,6 +38,8 @@ module RuboCop
38
38
  class ModuleLength < Base
39
39
  include CodeLength
40
40
 
41
+ LABEL = 'Module'
42
+
41
43
  def on_module(node)
42
44
  check_code_length(node)
43
45
  end
@@ -53,8 +55,8 @@ module RuboCop
53
55
  (casgn nil? _ (any_block (send (const {nil? cbase} :Module) :new) ...))
54
56
  PATTERN
55
57
 
56
- def message(length, max_length)
57
- format('Module has too many lines. [%<length>d/%<max>d]', length: length, max: max_length)
58
+ def cop_label
59
+ LABEL
58
60
  end
59
61
  end
60
62
  end
@@ -15,6 +15,10 @@ module RuboCop
15
15
  cop_config['IndentationWidth'] || config.for_cop('Layout/IndentationWidth')['Width'] || 2
16
16
  end
17
17
 
18
+ def using_tabs?
19
+ config.for_cop('Layout/IndentationStyle')['EnforcedStyle'] == 'tabs'
20
+ end
21
+
18
22
  def indentation(node)
19
23
  offset(node) + (SPACE * configured_indentation_width)
20
24
  end