rubocop 1.84.2 → 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 (357) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +183 -87
  3. data/config/obsoletion.yml +26 -1
  4. data/lib/rubocop/cache_config.rb +1 -1
  5. data/lib/rubocop/cli/command/auto_generate_config.rb +34 -2
  6. data/lib/rubocop/cli/command/list_enabled_cops_for.rb +40 -0
  7. data/lib/rubocop/cli/command/mcp.rb +19 -0
  8. data/lib/rubocop/cli/command/show_cops.rb +2 -2
  9. data/lib/rubocop/cli/command/show_docs_url.rb +4 -8
  10. data/lib/rubocop/cli/command/suggest_extensions.rb +1 -1
  11. data/lib/rubocop/cli.rb +9 -7
  12. data/lib/rubocop/comment_config.rb +12 -15
  13. data/lib/rubocop/config.rb +14 -10
  14. data/lib/rubocop/config_finder.rb +1 -1
  15. data/lib/rubocop/config_loader.rb +17 -2
  16. data/lib/rubocop/config_loader_resolver.rb +13 -4
  17. data/lib/rubocop/config_obsoletion/extracted_cop.rb +4 -2
  18. data/lib/rubocop/config_store.rb +2 -2
  19. data/lib/rubocop/config_validator.rb +1 -1
  20. data/lib/rubocop/cop/autocorrect_logic.rb +2 -1
  21. data/lib/rubocop/cop/base.rb +25 -4
  22. data/lib/rubocop/cop/bundler/gem_comment.rb +5 -3
  23. data/lib/rubocop/cop/correctors/condition_corrector.rb +1 -1
  24. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +1 -1
  25. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +7 -1
  26. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +1 -5
  27. data/lib/rubocop/cop/correctors/ordered_gem_corrector.rb +8 -1
  28. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +33 -2
  29. data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +2 -2
  30. data/lib/rubocop/cop/correctors.rb +28 -0
  31. data/lib/rubocop/cop/documentation.rb +2 -3
  32. data/lib/rubocop/cop/exclude_limit.rb +31 -5
  33. data/lib/rubocop/cop/gemspec/development_dependencies.rb +1 -1
  34. data/lib/rubocop/cop/gemspec/duplicated_assignment.rb +2 -2
  35. data/lib/rubocop/cop/gemspec/require_mfa.rb +9 -6
  36. data/lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb +3 -3
  37. data/lib/rubocop/cop/internal_affairs/itblock_handler.rb +69 -0
  38. data/lib/rubocop/cop/internal_affairs/location_line_equality_comparison.rb +1 -0
  39. data/lib/rubocop/cop/internal_affairs/redundant_let_rubocop_config_new.rb +5 -3
  40. data/lib/rubocop/cop/internal_affairs.rb +1 -0
  41. data/lib/rubocop/cop/layout/argument_alignment.rb +2 -2
  42. data/lib/rubocop/cop/layout/array_alignment.rb +1 -1
  43. data/lib/rubocop/cop/layout/begin_end_alignment.rb +1 -1
  44. data/lib/rubocop/cop/layout/block_alignment.rb +58 -4
  45. data/lib/rubocop/cop/layout/class_structure.rb +8 -4
  46. data/lib/rubocop/cop/layout/condition_position.rb +13 -3
  47. data/lib/rubocop/cop/layout/dot_position.rb +1 -1
  48. data/lib/rubocop/cop/layout/empty_comment.rb +8 -10
  49. data/lib/rubocop/cop/layout/empty_line_after_guard_clause.rb +23 -7
  50. data/lib/rubocop/cop/layout/empty_line_between_defs.rb +15 -2
  51. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  52. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +12 -2
  53. data/lib/rubocop/cop/layout/empty_lines_around_class_body.rb +16 -2
  54. data/lib/rubocop/cop/layout/empty_lines_around_module_body.rb +16 -2
  55. data/lib/rubocop/cop/layout/end_alignment.rb +8 -5
  56. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +20 -15
  57. data/lib/rubocop/cop/layout/hash_alignment.rb +1 -1
  58. data/lib/rubocop/cop/layout/indentation_width.rb +40 -0
  59. data/lib/rubocop/cop/layout/line_length.rb +5 -3
  60. data/lib/rubocop/cop/layout/multiline_assignment_layout.rb +9 -2
  61. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +1 -1
  62. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +53 -3
  63. data/lib/rubocop/cop/layout/parameter_alignment.rb +1 -1
  64. data/lib/rubocop/cop/layout/redundant_line_break.rb +3 -1
  65. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +1 -1
  66. data/lib/rubocop/cop/layout/space_around_keyword.rb +3 -1
  67. data/lib/rubocop/cop/layout/space_around_operators.rb +6 -2
  68. data/lib/rubocop/cop/layout/space_before_brackets.rb +1 -1
  69. data/lib/rubocop/cop/layout/space_in_lambda_literal.rb +1 -0
  70. data/lib/rubocop/cop/lint/ambiguous_assignment.rb +1 -11
  71. data/lib/rubocop/cop/lint/ambiguous_block_association.rb +1 -1
  72. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +1 -10
  73. data/lib/rubocop/cop/lint/assignment_in_condition.rb +13 -1
  74. data/lib/rubocop/cop/lint/circular_argument_reference.rb +1 -3
  75. data/lib/rubocop/cop/lint/constant_overwritten_in_rescue.rb +1 -1
  76. data/lib/rubocop/cop/lint/constant_reassignment.rb +93 -11
  77. data/lib/rubocop/cop/lint/constant_resolution.rb +6 -6
  78. data/lib/rubocop/cop/lint/data_define_override.rb +63 -0
  79. data/lib/rubocop/cop/lint/debugger.rb +0 -1
  80. data/lib/rubocop/cop/lint/deprecated_constants.rb +2 -8
  81. data/lib/rubocop/cop/lint/duplicate_methods.rb +55 -8
  82. data/lib/rubocop/cop/lint/empty_block.rb +4 -4
  83. data/lib/rubocop/cop/lint/empty_conditional_body.rb +6 -1
  84. data/lib/rubocop/cop/lint/empty_in_pattern.rb +8 -1
  85. data/lib/rubocop/cop/lint/empty_when.rb +8 -1
  86. data/lib/rubocop/cop/lint/ensure_return.rb +19 -1
  87. data/lib/rubocop/cop/lint/erb_new_arguments.rb +4 -2
  88. data/lib/rubocop/cop/lint/float_comparison.rb +1 -0
  89. data/lib/rubocop/cop/lint/incompatible_io_select_with_fiber_scheduler.rb +5 -1
  90. data/lib/rubocop/cop/lint/interpolation_check.rb +25 -5
  91. data/lib/rubocop/cop/lint/lambda_without_literal_block.rb +1 -1
  92. data/lib/rubocop/cop/lint/literal_assignment_in_condition.rb +11 -1
  93. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +8 -11
  94. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +5 -5
  95. data/lib/rubocop/cop/lint/multiple_comparison.rb +2 -2
  96. data/lib/rubocop/cop/lint/next_without_accumulator.rb +2 -0
  97. data/lib/rubocop/cop/lint/no_return_in_begin_end_blocks.rb +16 -0
  98. data/lib/rubocop/cop/lint/non_deterministic_require_order.rb +4 -2
  99. data/lib/rubocop/cop/lint/non_local_exit_from_iterator.rb +1 -1
  100. data/lib/rubocop/cop/lint/number_conversion.rb +19 -10
  101. data/lib/rubocop/cop/lint/numbered_parameter_assignment.rb +1 -1
  102. data/lib/rubocop/cop/lint/numeric_operation_with_constant_result.rb +3 -0
  103. data/lib/rubocop/cop/lint/ordered_magic_comments.rb +7 -7
  104. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +3 -13
  105. data/lib/rubocop/cop/lint/raise_exception.rb +1 -1
  106. data/lib/rubocop/cop/lint/rand_one.rb +1 -1
  107. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +4 -1
  108. data/lib/rubocop/cop/lint/redundant_cop_enable_directive.rb +6 -12
  109. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +15 -4
  110. data/lib/rubocop/cop/lint/redundant_safe_navigation.rb +36 -12
  111. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +4 -0
  112. data/lib/rubocop/cop/lint/redundant_type_conversion.rb +10 -3
  113. data/lib/rubocop/cop/lint/redundant_with_index.rb +1 -1
  114. data/lib/rubocop/cop/lint/redundant_with_object.rb +5 -0
  115. data/lib/rubocop/cop/lint/refinement_import_methods.rb +8 -1
  116. data/lib/rubocop/cop/lint/regexp_as_condition.rb +9 -1
  117. data/lib/rubocop/cop/lint/require_parentheses.rb +13 -4
  118. data/lib/rubocop/cop/lint/require_range_parentheses.rb +2 -1
  119. data/lib/rubocop/cop/lint/require_relative_self_path.rb +7 -5
  120. data/lib/rubocop/cop/lint/rescue_type.rb +1 -1
  121. data/lib/rubocop/cop/lint/safe_navigation_chain.rb +18 -0
  122. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +7 -1
  123. data/lib/rubocop/cop/lint/safe_navigation_with_empty.rb +1 -1
  124. data/lib/rubocop/cop/lint/script_permission.rb +5 -1
  125. data/lib/rubocop/cop/lint/self_assignment.rb +24 -1
  126. data/lib/rubocop/cop/lint/send_with_mixin_argument.rb +1 -1
  127. data/lib/rubocop/cop/lint/shadowed_exception.rb +1 -1
  128. data/lib/rubocop/cop/lint/shadowing_outer_local_variable.rb +14 -0
  129. data/lib/rubocop/cop/lint/shared_mutable_default.rb +3 -1
  130. data/lib/rubocop/cop/lint/suppressed_exception_in_number_conversion.rb +12 -0
  131. data/lib/rubocop/cop/lint/symbol_conversion.rb +21 -4
  132. data/lib/rubocop/cop/lint/syntax.rb +25 -1
  133. data/lib/rubocop/cop/lint/to_enum_arguments.rb +35 -2
  134. data/lib/rubocop/cop/lint/top_level_return_with_argument.rb +1 -1
  135. data/lib/rubocop/cop/lint/trailing_comma_in_attribute_declaration.rb +5 -1
  136. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +36 -10
  137. data/lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb +1 -0
  138. data/lib/rubocop/cop/lint/unreachable_code.rb +2 -2
  139. data/lib/rubocop/cop/lint/unreachable_pattern_branch.rb +113 -0
  140. data/lib/rubocop/cop/lint/unused_method_argument.rb +10 -0
  141. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -14
  142. data/lib/rubocop/cop/lint/useless_constant_scoping.rb +4 -4
  143. data/lib/rubocop/cop/lint/useless_default_value_argument.rb +2 -0
  144. data/lib/rubocop/cop/lint/useless_ruby2_keywords.rb +8 -4
  145. data/lib/rubocop/cop/lint/useless_setter_call.rb +4 -1
  146. data/lib/rubocop/cop/lint/useless_times.rb +22 -1
  147. data/lib/rubocop/cop/lint/utils/nil_receiver_checker.rb +35 -9
  148. data/lib/rubocop/cop/lint/void.rb +32 -12
  149. data/lib/rubocop/cop/metrics/block_length.rb +1 -1
  150. data/lib/rubocop/cop/metrics/block_nesting.rb +23 -0
  151. data/lib/rubocop/cop/metrics/collection_literal_length.rb +1 -1
  152. data/lib/rubocop/cop/metrics/method_length.rb +2 -2
  153. data/lib/rubocop/cop/metrics/perceived_complexity.rb +38 -7
  154. data/lib/rubocop/cop/metrics/utils/iterating_block.rb +1 -1
  155. data/lib/rubocop/cop/migration/department_name.rb +12 -1
  156. data/lib/rubocop/cop/mixin/check_line_breakable.rb +1 -1
  157. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +2 -2
  158. data/lib/rubocop/cop/mixin/configurable_max.rb +6 -5
  159. data/lib/rubocop/cop/mixin/hash_subset.rb +8 -0
  160. data/lib/rubocop/cop/mixin/hash_transform_method/autocorrection.rb +63 -0
  161. data/lib/rubocop/cop/mixin/hash_transform_method.rb +14 -60
  162. data/lib/rubocop/cop/mixin/project_index_help.rb +48 -0
  163. data/lib/rubocop/cop/mixin.rb +86 -0
  164. data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +1 -1
  165. data/lib/rubocop/cop/naming/block_parameter_name.rb +1 -1
  166. data/lib/rubocop/cop/naming/file_name.rb +4 -3
  167. data/lib/rubocop/cop/naming/inclusive_language.rb +8 -2
  168. data/lib/rubocop/cop/naming/memoized_instance_variable_name.rb +10 -1
  169. data/lib/rubocop/cop/naming/predicate_method.rb +2 -2
  170. data/lib/rubocop/cop/naming/predicate_prefix.rb +1 -1
  171. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +10 -4
  172. data/lib/rubocop/cop/offense.rb +8 -0
  173. data/lib/rubocop/cop/registry.rb +62 -38
  174. data/lib/rubocop/cop/security/eval.rb +15 -2
  175. data/lib/rubocop/cop/security/io_methods.rb +2 -2
  176. data/lib/rubocop/cop/security/marshal_load.rb +1 -1
  177. data/lib/rubocop/cop/style/access_modifier_declarations.rb +14 -2
  178. data/lib/rubocop/cop/style/accessor_grouping.rb +15 -3
  179. data/lib/rubocop/cop/style/alias.rb +15 -3
  180. data/lib/rubocop/cop/style/and_or.rb +2 -1
  181. data/lib/rubocop/cop/style/arguments_forwarding.rb +25 -7
  182. data/lib/rubocop/cop/style/array_first_last.rb +12 -1
  183. data/lib/rubocop/cop/style/array_intersect.rb +4 -0
  184. data/lib/rubocop/cop/style/array_intersect_with_single_element.rb +3 -0
  185. data/lib/rubocop/cop/style/array_join.rb +4 -2
  186. data/lib/rubocop/cop/style/ascii_comments.rb +6 -3
  187. data/lib/rubocop/cop/style/attr.rb +5 -2
  188. data/lib/rubocop/cop/style/bare_percent_literals.rb +3 -1
  189. data/lib/rubocop/cop/style/begin_block.rb +3 -1
  190. data/lib/rubocop/cop/style/block_delimiters.rb +37 -31
  191. data/lib/rubocop/cop/style/case_equality.rb +18 -2
  192. data/lib/rubocop/cop/style/character_literal.rb +2 -2
  193. data/lib/rubocop/cop/style/class_and_module_children.rb +18 -2
  194. data/lib/rubocop/cop/style/class_equality_comparison.rb +21 -13
  195. data/lib/rubocop/cop/style/class_methods_definitions.rb +11 -5
  196. data/lib/rubocop/cop/style/collection_compact.rb +36 -16
  197. data/lib/rubocop/cop/style/colon_method_call.rb +16 -7
  198. data/lib/rubocop/cop/style/combinable_loops.rb +5 -0
  199. data/lib/rubocop/cop/style/comparable_clamp.rb +12 -1
  200. data/lib/rubocop/cop/style/concat_array_literals.rb +7 -1
  201. data/lib/rubocop/cop/style/conditional_assignment.rb +6 -5
  202. data/lib/rubocop/cop/style/constant_visibility.rb +4 -1
  203. data/lib/rubocop/cop/style/copyright.rb +22 -11
  204. data/lib/rubocop/cop/style/data_inheritance.rb +4 -0
  205. data/lib/rubocop/cop/style/date_time.rb +4 -4
  206. data/lib/rubocop/cop/style/dig_chain.rb +5 -0
  207. data/lib/rubocop/cop/style/dir_empty.rb +4 -0
  208. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +1 -1
  209. data/lib/rubocop/cop/style/document_dynamic_eval_definition.rb +6 -1
  210. data/lib/rubocop/cop/style/each_for_simple_loop.rb +1 -1
  211. data/lib/rubocop/cop/style/each_with_object.rb +2 -0
  212. data/lib/rubocop/cop/style/empty_block_parameter.rb +1 -1
  213. data/lib/rubocop/cop/style/empty_case_condition.rb +12 -2
  214. data/lib/rubocop/cop/style/empty_class_definition.rb +51 -21
  215. data/lib/rubocop/cop/style/empty_heredoc.rb +4 -0
  216. data/lib/rubocop/cop/style/empty_lambda_parameter.rb +1 -1
  217. data/lib/rubocop/cop/style/empty_literal.rb +7 -2
  218. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +30 -20
  219. data/lib/rubocop/cop/style/encoding.rb +7 -1
  220. data/lib/rubocop/cop/style/end_block.rb +3 -1
  221. data/lib/rubocop/cop/style/endless_method.rb +8 -3
  222. data/lib/rubocop/cop/style/env_home.rb +4 -0
  223. data/lib/rubocop/cop/style/even_odd.rb +11 -1
  224. data/lib/rubocop/cop/style/exact_regexp_match.rb +8 -1
  225. data/lib/rubocop/cop/style/fetch_env_var.rb +1 -1
  226. data/lib/rubocop/cop/style/file_null.rb +4 -2
  227. data/lib/rubocop/cop/style/file_open.rb +84 -0
  228. data/lib/rubocop/cop/style/file_write.rb +21 -16
  229. data/lib/rubocop/cop/style/for.rb +3 -0
  230. data/lib/rubocop/cop/style/format_string.rb +17 -4
  231. data/lib/rubocop/cop/style/format_string_token.rb +29 -2
  232. data/lib/rubocop/cop/style/global_vars.rb +5 -2
  233. data/lib/rubocop/cop/style/guard_clause.rb +9 -6
  234. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +21 -5
  235. data/lib/rubocop/cop/style/hash_conversion.rb +1 -1
  236. data/lib/rubocop/cop/style/hash_lookup_method.rb +19 -7
  237. data/lib/rubocop/cop/style/hash_slice.rb +16 -0
  238. data/lib/rubocop/cop/style/hash_syntax.rb +2 -0
  239. data/lib/rubocop/cop/style/hash_transform_keys.rb +17 -7
  240. data/lib/rubocop/cop/style/hash_transform_values.rb +17 -7
  241. data/lib/rubocop/cop/style/if_inside_else.rb +16 -7
  242. data/lib/rubocop/cop/style/if_unless_modifier.rb +15 -4
  243. data/lib/rubocop/cop/style/if_with_semicolon.rb +16 -6
  244. data/lib/rubocop/cop/style/inline_comment.rb +5 -2
  245. data/lib/rubocop/cop/style/ip_addresses.rb +1 -2
  246. data/lib/rubocop/cop/style/keyword_arguments_merging.rb +4 -0
  247. data/lib/rubocop/cop/style/keyword_parameters_order.rb +7 -3
  248. data/lib/rubocop/cop/style/lambda.rb +7 -1
  249. data/lib/rubocop/cop/style/magic_comment_format.rb +3 -3
  250. data/lib/rubocop/cop/style/map_compact_with_conditional_block.rb +11 -0
  251. data/lib/rubocop/cop/style/map_into_array.rb +1 -1
  252. data/lib/rubocop/cop/style/map_join.rb +123 -0
  253. data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +5 -3
  254. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +6 -2
  255. data/lib/rubocop/cop/style/method_def_parentheses.rb +1 -1
  256. data/lib/rubocop/cop/style/min_max_comparison.rb +4 -1
  257. data/lib/rubocop/cop/style/module_member_existence_check.rb +7 -14
  258. data/lib/rubocop/cop/style/multiline_if_then.rb +4 -2
  259. data/lib/rubocop/cop/style/multiline_memoization.rb +7 -1
  260. data/lib/rubocop/cop/style/multiline_method_signature.rb +11 -4
  261. data/lib/rubocop/cop/style/mutable_constant.rb +106 -12
  262. data/lib/rubocop/cop/style/nil_comparison.rb +2 -3
  263. data/lib/rubocop/cop/style/nil_lambda.rb +9 -1
  264. data/lib/rubocop/cop/style/non_nil_check.rb +5 -11
  265. data/lib/rubocop/cop/style/not.rb +2 -0
  266. data/lib/rubocop/cop/style/numeric_literals.rb +3 -2
  267. data/lib/rubocop/cop/style/numeric_predicate.rb +1 -1
  268. data/lib/rubocop/cop/style/one_class_per_file.rb +115 -0
  269. data/lib/rubocop/cop/style/one_line_conditional.rb +4 -3
  270. data/lib/rubocop/cop/style/open_struct_use.rb +1 -1
  271. data/lib/rubocop/cop/style/option_hash.rb +1 -1
  272. data/lib/rubocop/cop/style/optional_arguments.rb +1 -0
  273. data/lib/rubocop/cop/style/parallel_assignment.rb +23 -3
  274. data/lib/rubocop/cop/style/partition_instead_of_double_select.rb +270 -0
  275. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +4 -0
  276. data/lib/rubocop/cop/style/perl_backrefs.rb +5 -3
  277. data/lib/rubocop/cop/style/predicate_with_kind.rb +84 -0
  278. data/lib/rubocop/cop/style/proc.rb +3 -2
  279. data/lib/rubocop/cop/style/raise_args.rb +1 -1
  280. data/lib/rubocop/cop/style/reduce_to_hash.rb +200 -0
  281. data/lib/rubocop/cop/style/redundant_array_constructor.rb +2 -2
  282. data/lib/rubocop/cop/style/redundant_begin.rb +3 -3
  283. data/lib/rubocop/cop/style/redundant_constant_base.rb +5 -5
  284. data/lib/rubocop/cop/style/redundant_each.rb +3 -3
  285. data/lib/rubocop/cop/style/redundant_exception.rb +6 -0
  286. data/lib/rubocop/cop/style/redundant_fetch_block.rb +1 -1
  287. data/lib/rubocop/cop/style/redundant_filter_chain.rb +1 -1
  288. data/lib/rubocop/cop/style/redundant_format.rb +29 -0
  289. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +26 -10
  290. data/lib/rubocop/cop/style/redundant_line_continuation.rb +27 -3
  291. data/lib/rubocop/cop/style/redundant_min_max_by.rb +93 -0
  292. data/lib/rubocop/cop/style/redundant_parentheses.rb +25 -22
  293. data/lib/rubocop/cop/style/redundant_percent_q.rb +4 -1
  294. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +2 -2
  295. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +8 -4
  296. data/lib/rubocop/cop/style/redundant_return.rb +3 -1
  297. data/lib/rubocop/cop/style/redundant_self.rb +11 -2
  298. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +0 -5
  299. data/lib/rubocop/cop/style/redundant_struct_keyword_init.rb +133 -0
  300. data/lib/rubocop/cop/style/regexp_literal.rb +31 -2
  301. data/lib/rubocop/cop/style/rescue_modifier.rb +3 -3
  302. data/lib/rubocop/cop/style/safe_navigation.rb +7 -7
  303. data/lib/rubocop/cop/style/select_by_kind.rb +158 -0
  304. data/lib/rubocop/cop/style/select_by_range.rb +197 -0
  305. data/lib/rubocop/cop/style/select_by_regexp.rb +51 -21
  306. data/lib/rubocop/cop/style/self_assignment.rb +1 -1
  307. data/lib/rubocop/cop/style/semicolon.rb +22 -5
  308. data/lib/rubocop/cop/style/single_line_block_params.rb +2 -2
  309. data/lib/rubocop/cop/style/single_line_do_end_block.rb +18 -5
  310. data/lib/rubocop/cop/style/single_line_methods.rb +3 -1
  311. data/lib/rubocop/cop/style/sole_nested_conditional.rb +4 -2
  312. data/lib/rubocop/cop/style/special_global_vars.rb +6 -1
  313. data/lib/rubocop/cop/style/string_hash_keys.rb +1 -0
  314. data/lib/rubocop/cop/style/struct_inheritance.rb +13 -0
  315. data/lib/rubocop/cop/style/symbol_proc.rb +7 -6
  316. data/lib/rubocop/cop/style/tally_method.rb +181 -0
  317. data/lib/rubocop/cop/style/ternary_parentheses.rb +11 -0
  318. data/lib/rubocop/cop/style/top_level_method_definition.rb +2 -2
  319. data/lib/rubocop/cop/style/trailing_comma_in_block_args.rb +1 -1
  320. data/lib/rubocop/cop/style/trailing_method_end_statement.rb +1 -0
  321. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +7 -8
  322. data/lib/rubocop/cop/style/unless_logical_operators.rb +3 -3
  323. data/lib/rubocop/cop/style/while_until_do.rb +7 -0
  324. data/lib/rubocop/cop/style/while_until_modifier.rb +16 -0
  325. data/lib/rubocop/cop/style/word_array.rb +1 -0
  326. data/lib/rubocop/cop/style/yoda_condition.rb +1 -1
  327. data/lib/rubocop/cop/style/yoda_expression.rb +1 -1
  328. data/lib/rubocop/cop/style/zero_length_predicate.rb +6 -3
  329. data/lib/rubocop/cop/team.rb +86 -35
  330. data/lib/rubocop/cop/variable_force/branch.rb +2 -2
  331. data/lib/rubocop/directive_comment.rb +2 -1
  332. data/lib/rubocop/file_patterns.rb +9 -1
  333. data/lib/rubocop/formatter/disabled_config_formatter.rb +19 -9
  334. data/lib/rubocop/formatter/formatter_set.rb +1 -1
  335. data/lib/rubocop/formatter/junit_formatter.rb +1 -1
  336. data/lib/rubocop/formatter/simple_text_formatter.rb +0 -2
  337. data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
  338. data/lib/rubocop/formatter.rb +22 -21
  339. data/lib/rubocop/lsp/diagnostic.rb +1 -0
  340. data/lib/rubocop/lsp/routes.rb +10 -3
  341. data/lib/rubocop/lsp/runtime.rb +1 -2
  342. data/lib/rubocop/mcp/server.rb +200 -0
  343. data/lib/rubocop/options.rb +35 -4
  344. data/lib/rubocop/path_util.rb +14 -2
  345. data/lib/rubocop/plugin/loader.rb +1 -1
  346. data/lib/rubocop/project_index_loader.rb +66 -0
  347. data/lib/rubocop/result_cache.rb +22 -10
  348. data/lib/rubocop/rspec/cop_helper.rb +8 -0
  349. data/lib/rubocop/rspec/shared_contexts.rb +32 -2
  350. data/lib/rubocop/runner.rb +126 -53
  351. data/lib/rubocop/server/cache.rb +5 -7
  352. data/lib/rubocop/server/core.rb +8 -0
  353. data/lib/rubocop/target_finder.rb +14 -7
  354. data/lib/rubocop/target_ruby.rb +18 -12
  355. data/lib/rubocop/version.rb +21 -3
  356. data/lib/rubocop.rb +22 -96
  357. metadata +27 -5
@@ -79,6 +79,11 @@ module RuboCop
79
79
  corrector.replace(range, replacement)
80
80
 
81
81
  comments_in_range(node).reverse_each do |comment|
82
+ # Only relocate comments that the replacement destroys. A trailing
83
+ # comment after the chain survives in place, so moving it would
84
+ # duplicate it (and splitting the line drops the indentation).
85
+ next if comment.source_range.begin_pos >= range.end_pos
86
+
82
87
  corrector.insert_before(node, "#{comment.source}\n")
83
88
  end
84
89
  end
@@ -35,6 +35,10 @@ module RuboCop
35
35
  PATTERN
36
36
 
37
37
  def on_send(node)
38
+ # A trailing block (e.g. `Dir.each_child(path).none? { ... }`) changes
39
+ # the meaning and is not equivalent to `Dir.empty?`.
40
+ return if node.block_literal?
41
+
38
42
  offensive?(node) do |const_node, arg_node|
39
43
  replacement = "#{bang(node)}#{const_node.source}.empty?(#{arg_node.source})"
40
44
  add_offense(node, message: format(MSG, replacement: replacement)) do |corrector|
@@ -6,7 +6,7 @@ module RuboCop
6
6
  module Cop
7
7
  module Style
8
8
  # Detects comments to enable/disable RuboCop.
9
- # This is useful if want to make sure that every RuboCop error gets fixed
9
+ # This is useful if you want to make sure that every RuboCop error gets fixed
10
10
  # and not quickly disabled with a comment.
11
11
  #
12
12
  # Specific cops can be allowed with the `AllowedCops` configuration. Note that
@@ -161,7 +161,12 @@ module RuboCop
161
161
  source = source.gsub(COMMENT_REGEXP, '')
162
162
  return if source.blank?
163
163
 
164
- /\s*#{Regexp.escape(source.strip)}/
164
+ # Treat `\#` (an escaped interpolation marker in the heredoc) as matching
165
+ # either `\#` or `#` in the comment, since the comment may show either
166
+ # the literal source form or the runtime appearance.
167
+ segments = source.strip.split('\\#', -1).map { |segment| Regexp.escape(segment) }
168
+
169
+ /\s*#{segments.join('\\\\?#')}/
165
170
  end
166
171
  end
167
172
  end
@@ -26,7 +26,7 @@ module RuboCop
26
26
 
27
27
  MSG = 'Use `Integer#times` for a simple loop which iterates a fixed number of times.'
28
28
 
29
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
29
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
30
30
  return unless offending?(node)
31
31
 
32
32
  send_node = node.send_node
@@ -40,6 +40,8 @@ module RuboCop
40
40
  end
41
41
  end
42
42
 
43
+ alias on_itblock on_block
44
+
43
45
  def on_numblock(node)
44
46
  each_with_object_numblock_candidate?(node) do |method, body|
45
47
  _, method_name, method_arg = *method
@@ -28,7 +28,7 @@ module RuboCop
28
28
 
29
29
  MSG = 'Omit pipes for the empty block parameters.'
30
30
 
31
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
31
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
32
32
  send_node = node.send_node
33
33
  check(node) unless send_node.send_type? && send_node.lambda_literal?
34
34
  end
@@ -40,7 +40,7 @@ module RuboCop
40
40
  extend AutoCorrector
41
41
 
42
42
  MSG = 'Do not use empty `case` condition, instead use an `if` expression.'
43
- NOT_SUPPORTED_PARENT_TYPES = %i[return break next send csend].freeze
43
+ NOT_SUPPORTED_PARENT_TYPES = %i[return break next send csend yield super].freeze
44
44
 
45
45
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
46
46
  def on_case(case_node)
@@ -90,7 +90,17 @@ module RuboCop
90
90
  range = range_between(conditions.first.source_range.begin_pos,
91
91
  conditions.last.source_range.end_pos)
92
92
 
93
- corrector.replace(range, conditions.map(&:source).join(' || '))
93
+ corrector.replace(range, conditions.map { |c| parenthesize_condition(c) }.join(' || '))
94
+ end
95
+ end
96
+
97
+ # A condition that binds looser than `||` (e.g. a ternary, range, or
98
+ # assignment) must be parenthesized so the joined `||` keeps its meaning.
99
+ def parenthesize_condition(condition)
100
+ if condition.assignment? || condition.type?(:if, :and, :or, :range)
101
+ "(#{condition.source})"
102
+ else
103
+ condition.source
94
104
  end
95
105
  end
96
106
 
@@ -5,15 +5,26 @@ module RuboCop
5
5
  module Style
6
6
  # Enforces consistent style for empty class definitions.
7
7
  #
8
- # This cop can enforce either a two-line class definition or `Class.new`
8
+ # This cop can enforce either a standard class definition or `Class.new`
9
9
  # for classes with no body.
10
10
  #
11
11
  # The supported styles are:
12
12
  #
13
- # * class_definition (default) - prefer two-line class definition over `Class.new`
13
+ # * class_keyword (default) - prefer standard class definition over `Class.new`
14
14
  # * class_new - prefer `Class.new` over class definition
15
15
  #
16
- # @example EnforcedStyle: class_definition (default)
16
+ # One difference between the two styles is that the `Class.new` form does not make
17
+ # the subclass name available to the base class's `inherited` callback.
18
+ # For this reason, `EnforcedStyle: class_keyword` is set as the default style.
19
+ # Class definitions without a superclass, which are not involved in inheritance,
20
+ # are not detected. This ensures safe detection regardless of the applied style.
21
+ # This avoids overlapping responsibilities with the `Lint/EmptyClass` cop.
22
+ #
23
+ # Use `AllowedParentClasses` to permit both styles for specific parent classes.
24
+ # For example, adding `StandardError` allows both `Error = Class.new(StandardError)`
25
+ # and `class Error < StandardError; end` regardless of the enforced style.
26
+ #
27
+ # @example EnforcedStyle: class_keyword (default)
17
28
  # # bad
18
29
  # FooError = Class.new(StandardError)
19
30
  #
@@ -35,33 +46,43 @@ module RuboCop
35
46
  # # good
36
47
  # FooError = Class.new(StandardError)
37
48
  #
49
+ # @example AllowedParentClasses: ['StandardError']
50
+ # # good - allowed regardless of EnforcedStyle
51
+ # FooError = Class.new(StandardError)
52
+ #
53
+ # # good - allowed regardless of EnforcedStyle
54
+ # class FooError < StandardError
55
+ # end
56
+ #
38
57
  class EmptyClassDefinition < Base
39
58
  include ConfigurableEnforcedStyle
40
- include RangeHelp
41
59
  extend AutoCorrector
42
60
 
43
- MSG_CLASS_DEFINITION =
44
- 'Prefer a two-line class definition over `Class.new` for classes with no body.'
45
- MSG_CLASS_NEW = 'Prefer `Class.new` over class definition for classes with no body.'
61
+ MSG_CLASS_KEYWORD =
62
+ 'Use the `class` keyword instead of `Class.new` to define an empty class.'
63
+ MSG_CLASS_NEW = 'Use `Class.new` instead of the `class` keyword to define an empty class.'
46
64
 
47
65
  # @!method class_new_assignment(node)
48
66
  def_node_matcher :class_new_assignment, <<~PATTERN
49
- (casgn _ _ $(send (const _ :Class) :new ...))
67
+ (casgn _ _ $(send (const _ :Class) :new _))
50
68
  PATTERN
51
69
 
52
70
  def on_casgn(node)
53
- return unless style == :class_definition
71
+ return unless %i[class_keyword class_definition].include?(style)
54
72
  return unless (class_new_node = class_new_assignment(node))
55
73
  return if (arg = class_new_node.first_argument) && !arg.const_type?
74
+ return if allowed_parent_class?(class_new_node.first_argument.source)
56
75
 
57
- add_offense(node, message: MSG_CLASS_DEFINITION) do |corrector|
76
+ add_offense(node, message: MSG_CLASS_KEYWORD) do |corrector|
58
77
  autocorrect_class_new(corrector, node, class_new_node)
59
78
  end
60
79
  end
61
80
 
62
81
  def on_class(node)
63
82
  return unless style == :class_new
83
+ return unless node.parent_class
64
84
  return if (body = node.body) && !body.children.empty?
85
+ return if allowed_parent_class?(node.parent_class.source)
65
86
 
66
87
  add_offense(node, message: MSG_CLASS_NEW) do |corrector|
67
88
  autocorrect_class_definition(corrector, node)
@@ -70,25 +91,34 @@ module RuboCop
70
91
 
71
92
  private
72
93
 
94
+ def allowed_parent_class?(parent_class_name)
95
+ allowed_parent_classes.include?(parent_class_name)
96
+ end
97
+
98
+ def allowed_parent_classes
99
+ cop_config.fetch('AllowedParentClasses', [])
100
+ end
101
+
73
102
  def autocorrect_class_new(corrector, node, class_new_node)
74
103
  indent = ' ' * node.loc.column
75
- class_name = node.name
76
- if (parent_class = class_new_node.first_argument)
77
- parent_class_name = " < #{parent_class.source}"
78
- end
104
+ class_name = constant_name(node)
105
+ parent_class_name = class_new_node.first_argument.source
79
106
 
80
- corrector.replace(node, "class #{class_name}#{parent_class_name}\n#{indent}end")
107
+ corrector.replace(node, "class #{class_name} < #{parent_class_name}\n#{indent}end")
108
+ end
109
+
110
+ # Preserve any namespace on the assigned constant (e.g. `Foo::Bar`),
111
+ # which `node.name` drops.
112
+ def constant_name(node)
113
+ namespace = node.namespace
114
+ namespace ? "#{namespace.source}::#{node.name}" : node.name
81
115
  end
82
116
 
83
117
  def autocorrect_class_definition(corrector, node)
84
- indent = ' ' * node.loc.column
85
118
  class_name = node.identifier.source
86
- if (parent_class = node.parent_class)
87
- parent_class_name = "(#{parent_class.source})"
88
- end
89
- range = range_by_whole_lines(node.source_range, include_final_newline: true)
119
+ parent_class_name = node.parent_class.source
90
120
 
91
- corrector.replace(range, "#{indent}#{class_name} = Class.new#{parent_class_name}\n")
121
+ corrector.replace(node, "#{class_name} = Class.new(#{parent_class_name})")
92
122
  end
93
123
  end
94
124
  end
@@ -42,6 +42,10 @@ module RuboCop
42
42
  MSG = 'Use an empty string literal instead of heredoc.'
43
43
 
44
44
  def on_heredoc(node)
45
+ # A backtick heredoc (`<<~`CMD``) executes a command, so it cannot be
46
+ # replaced with an empty string literal.
47
+ return if node.xstr_type?
48
+
45
49
  heredoc_body = node.loc.heredoc_body
46
50
 
47
51
  return unless heredoc_body.source.empty?
@@ -23,7 +23,7 @@ module RuboCop
23
23
 
24
24
  MSG = 'Omit parentheses for the empty lambda parameters.'
25
25
 
26
- def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
26
+ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler, InternalAffairs/ItblockHandler
27
27
  send_node = node.send_node
28
28
  return unless send_node.send_type?
29
29
 
@@ -44,13 +44,18 @@ module RuboCop
44
44
  def_node_matcher :str_node, '(send (const {nil? cbase} :String) :new)'
45
45
 
46
46
  # @!method array_with_block(node)
47
- def_node_matcher :array_with_block, '(block (send (const {nil? cbase} :Array) :new) args _)'
47
+ def_node_matcher :array_with_block, <<~PATTERN
48
+ {
49
+ (block (send (const {nil? cbase} :Array) :new) args _)
50
+ ({numblock itblock} (send (const {nil? cbase} :Array) :new) ...)
51
+ }
52
+ PATTERN
48
53
 
49
54
  # @!method hash_with_block(node)
50
55
  def_node_matcher :hash_with_block, <<~PATTERN
51
56
  {
52
57
  (block (send (const {nil? cbase} :Hash) :new) args _)
53
- (numblock (send (const {nil? cbase} :Hash) :new) ...)
58
+ ({numblock itblock} (send (const {nil? cbase} :Hash) :new) ...)
54
59
  }
55
60
  PATTERN
56
61
 
@@ -45,36 +45,46 @@ module RuboCop
45
45
  MSG_TRAILING_CONDITIONAL = 'Do not use trailing conditionals in string interpolation.'
46
46
  MSG_TERNARY = 'Do not return empty strings in string interpolation.'
47
47
 
48
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
49
48
  def on_interpolation(node)
50
49
  node.each_child_node(:if) do |child_node|
51
50
  if style == :trailing_conditional
52
- if empty_if_outcome?(child_node)
53
- ternary_style_autocorrect(child_node, child_node.else_branch.source, 'unless')
54
- end
55
-
56
- if empty_else_outcome?(child_node)
57
- ternary_style_autocorrect(child_node, child_node.if_branch.source, 'if')
58
- end
51
+ trailing_conditional_correction(child_node)
59
52
  elsif style == :ternary
60
- next unless child_node.modifier_form?
61
-
62
- ternary_component = if child_node.unless?
63
- "'' : #{child_node.if_branch.source}"
64
- else
65
- "#{child_node.if_branch.source} : ''"
66
- end
67
-
68
- add_offense(node, message: MSG_TRAILING_CONDITIONAL) do |corrector|
69
- corrector.replace(node, "\#{#{child_node.condition.source} ? #{ternary_component}}")
70
- end
53
+ ternary_correction(node, child_node)
71
54
  end
72
55
  end
73
56
  end
74
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
75
57
 
76
58
  private
77
59
 
60
+ def trailing_conditional_correction(child_node)
61
+ # A modifier `if`/`unless` is already a trailing conditional and has
62
+ # no `else` branch, so the ternary-to-trailing rewrite does not apply.
63
+ return if child_node.modifier_form?
64
+
65
+ if empty_if_outcome?(child_node)
66
+ ternary_style_autocorrect(child_node, child_node.else_branch.source, 'unless')
67
+ end
68
+
69
+ return unless empty_else_outcome?(child_node)
70
+
71
+ ternary_style_autocorrect(child_node, child_node.if_branch.source, 'if')
72
+ end
73
+
74
+ def ternary_correction(node, child_node)
75
+ return unless child_node.modifier_form?
76
+
77
+ ternary_component = if child_node.unless?
78
+ "'' : #{child_node.if_branch.source}"
79
+ else
80
+ "#{child_node.if_branch.source} : ''"
81
+ end
82
+
83
+ add_offense(node, message: MSG_TRAILING_CONDITIONAL) do |corrector|
84
+ corrector.replace(node, "\#{#{child_node.condition.source} ? #{ternary_component}}")
85
+ end
86
+ end
87
+
78
88
  def empty_if_outcome?(node)
79
89
  empty_branch_outcome?(node.if_branch)
80
90
  end
@@ -3,12 +3,18 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks ensures source files have no utf-8 encoding comments.
6
+ # Checks that source files have no utf-8 encoding comments.
7
+ # Since Ruby 2.0, UTF-8 is the default source encoding, so
8
+ # these comments are no longer necessary and just add noise.
9
+ #
7
10
  # @example
8
11
  # # bad
9
12
  # # encoding: UTF-8
10
13
  # # coding: UTF-8
11
14
  # # -*- coding: UTF-8 -*-
15
+ #
16
+ # # good
17
+ # # No encoding comment needed
12
18
  class Encoding < Base
13
19
  include RangeHelp
14
20
  extend AutoCorrector
@@ -3,7 +3,9 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Style
6
- # Checks for END blocks.
6
+ # Checks for `END` blocks. `END` blocks are Perl-style constructs
7
+ # and `Kernel#at_exit` is the idiomatic Ruby alternative, as it's
8
+ # explicit and can be used anywhere.
7
9
  #
8
10
  # @example
9
11
  # # bad
@@ -157,6 +157,7 @@ module RuboCop
157
157
  handle_require_always_style(node)
158
158
  end
159
159
  end
160
+ alias on_defs on_def
160
161
 
161
162
  private
162
163
 
@@ -170,7 +171,7 @@ module RuboCop
170
171
  end
171
172
 
172
173
  def handle_require_single_line_style(node)
173
- if node.endless? && !node.single_line?
174
+ if node.endless? && node.multiline?
174
175
  add_offense(node, message: MSG_MULTI_LINE) do |corrector|
175
176
  correct_to_multiline(corrector, node)
176
177
  end
@@ -207,7 +208,7 @@ module RuboCop
207
208
 
208
209
  def correct_to_multiline(corrector, node)
209
210
  replacement = <<~RUBY.strip
210
- def #{node.method_name}#{arguments(node)}
211
+ def #{receiver(node)}#{node.method_name}#{arguments(node)}
211
212
  #{node.body.source}
212
213
  end
213
214
  RUBY
@@ -217,10 +218,14 @@ module RuboCop
217
218
 
218
219
  def endless_replacement(node)
219
220
  <<~RUBY.strip
220
- def #{node.method_name}#{arguments(node)} = #{node.body.source}
221
+ def #{receiver(node)}#{node.method_name}#{arguments(node)} = #{node.body.source}
221
222
  RUBY
222
223
  end
223
224
 
225
+ def receiver(node)
226
+ node.receiver ? "#{node.receiver.source}#{node.loc.operator.source}" : ''
227
+ end
228
+
224
229
  def arguments(node, missing = '')
225
230
  node.arguments.any? ? node.arguments.source : missing
226
231
  end
@@ -45,6 +45,10 @@ module RuboCop
45
45
  def on_send(node)
46
46
  return unless env_home?(node)
47
47
  return if node.arguments.count == 2 && !node.arguments[1].nil_type?
48
+ # `ENV.fetch('HOME') { default }` supplies a fallback, just like
49
+ # `ENV.fetch('HOME', default)`. `Dir.home` ignores the block, so
50
+ # converting would silently drop it.
51
+ return if node.block_node
48
52
 
49
53
  add_offense(node) do |corrector|
50
54
  corrector.replace(node, 'Dir.home')
@@ -34,7 +34,7 @@ module RuboCop
34
34
  even_odd_candidate?(node) do |base_number, method, arg|
35
35
  replacement_method = replacement_method(arg, method)
36
36
  add_offense(node, message: format(MSG, method: replacement_method)) do |corrector|
37
- correction = "#{base_number.source}.#{replacement_method}?"
37
+ correction = "#{receiver_source(base_number)}.#{replacement_method}?"
38
38
  corrector.replace(node, correction)
39
39
  end
40
40
  end
@@ -42,6 +42,16 @@ module RuboCop
42
42
 
43
43
  private
44
44
 
45
+ def receiver_source(node)
46
+ # A binary or unary operator receiver (e.g. `a * b`, `-a`) binds looser
47
+ # than the appended method call, so it must be wrapped in parentheses.
48
+ if node.send_type? && node.operator_method? && !node.method?(:[])
49
+ "(#{node.source})"
50
+ else
51
+ node.source
52
+ end
53
+ end
54
+
45
55
  def replacement_method(arg, method)
46
56
  case arg
47
57
  when 0
@@ -43,7 +43,8 @@ module RuboCop
43
43
  return unless (parsed_regexp = parse_regexp(regexp))
44
44
  return unless exact_match_pattern?(parsed_regexp)
45
45
 
46
- prefer = "#{receiver.source} #{new_method(node)} '#{parsed_regexp[1].text}'"
46
+ string = escape_single_quotes(parsed_regexp[1].text)
47
+ prefer = "#{receiver.source} #{new_method(node)} '#{string}'"
47
48
 
48
49
  add_offense(node, message: format(MSG, prefer: prefer)) do |corrector|
49
50
  corrector.replace(node, prefer)
@@ -53,6 +54,12 @@ module RuboCop
53
54
 
54
55
  private
55
56
 
57
+ # Escape characters that are special inside a single-quoted string so the
58
+ # generated literal (e.g. for `/\Afoo'bar\z/`) stays valid Ruby.
59
+ def escape_single_quotes(text)
60
+ text.gsub(/['\\]/) { |char| "\\#{char}" }
61
+ end
62
+
56
63
  def exact_match_pattern?(parsed_regexp)
57
64
  tokens = parsed_regexp.map(&:token)
58
65
  return false unless tokens[0] == :bos && tokens[1] == :literal && tokens[2] == :eos
@@ -70,7 +70,7 @@ module RuboCop
70
70
 
71
71
  def allowed_var?(node)
72
72
  env_key_node = node.children.last
73
- env_key_node.str_type? && cop_config['AllowedVars'].include?(env_key_node.value)
73
+ env_key_node.str_type? && cop_config['AllowedVariables'].include?(env_key_node.value)
74
74
  end
75
75
 
76
76
  def used_as_flag?(node)
@@ -78,10 +78,12 @@ module RuboCop
78
78
 
79
79
  def acceptable?(node)
80
80
  # Using a hardcoded null device is acceptable when inside an array or
81
- # inside a hash to ensure behavior doesn't change.
81
+ # inside a hash to ensure behavior doesn't change. A `str` that is part of
82
+ # an interpolated or concatenated string (`dstr`) is not a standalone null
83
+ # device either, and replacing it would corrupt the surrounding string.
82
84
  return false unless node.parent
83
85
 
84
- node.parent.type?(:array, :pair)
86
+ node.parent.type?(:array, :pair, :dstr)
85
87
  end
86
88
  end
87
89
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Style
6
+ # Checks for `File.open` without a block, which can leak file descriptors.
7
+ #
8
+ # When `File.open` is called without a block, the caller is responsible
9
+ # for closing the file descriptor. If it is not explicitly closed, it
10
+ # will only be closed when the garbage collector runs, which may lead
11
+ # to resource exhaustion. Using the block form ensures the file is
12
+ # automatically closed when the block exits.
13
+ #
14
+ # This cop only registers an offense when the result of `File.open` is
15
+ # assigned to a variable or has a method chained on it, as those are the
16
+ # clearest indicators that the block form should be used instead. When
17
+ # `File.open` is used as a return value or passed as an argument, the
18
+ # caller is likely managing the file descriptor intentionally.
19
+ #
20
+ # @safety
21
+ # This cop is unsafe because it relies on syntax heuristics and cannot
22
+ # verify whether the file descriptor is safely managed. For example, it
23
+ # still flags intentional one-shot reads (`File.open("f").read`) where
24
+ # the file descriptor is closed by the garbage collector.
25
+ #
26
+ # @example
27
+ # # bad
28
+ # f = File.open('file')
29
+ #
30
+ # # bad
31
+ # File.open('file').read
32
+ #
33
+ # # good
34
+ # File.open('file') do |f|
35
+ # f.read
36
+ # end
37
+ #
38
+ # # good
39
+ # File.open('file', &:read)
40
+ #
41
+ # # good - pass an open file object to an API that manages its lifecycle
42
+ # process(io: File.open('file'))
43
+ #
44
+ # # good - return an open file object for the caller to manage
45
+ # def json_key_io
46
+ # File.open('file')
47
+ # end
48
+ #
49
+ # # good - use File.read for one-shot reads
50
+ # File.read('file')
51
+ #
52
+ class FileOpen < Base
53
+ MSG = '`File.open` without a block may leak a file descriptor; use the block form.'
54
+ RESTRICT_ON_SEND = %i[open].freeze
55
+
56
+ # @!method file_open?(node)
57
+ def_node_matcher :file_open?, <<~PATTERN
58
+ (send (const {nil? cbase} :File) :open ...)
59
+ PATTERN
60
+
61
+ def on_send(node)
62
+ return unless file_open?(node)
63
+ return if node.block_argument?
64
+ return unless offensive_usage?(node)
65
+
66
+ add_offense(node)
67
+ end
68
+ alias on_csend on_send
69
+
70
+ private
71
+
72
+ def offensive_usage?(node)
73
+ return true unless node.value_used?
74
+
75
+ node.parent.lvasgn_type? || receiver_of_chained_call?(node)
76
+ end
77
+
78
+ def receiver_of_chained_call?(node)
79
+ node.parent.receiver == node
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -104,28 +104,33 @@ module RuboCop
104
104
 
105
105
  def replacement(mode, filename, content, write_node)
106
106
  replacement = "#{write_method(mode)}(#{filename.source}, #{content.source})"
107
+ heredocs = removed_heredocs(filename, content, write_node)
108
+ return replacement if heredocs.empty?
107
109
 
108
- if heredoc?(write_node)
109
- first_argument = write_node.body.first_argument
110
+ [replacement, *heredocs.map { |heredoc| heredoc_range(heredoc).source }].join("\n")
111
+ end
110
112
 
111
- <<~REPLACEMENT.chomp
112
- #{replacement}
113
- #{heredoc_range(first_argument).source}
114
- REPLACEMENT
115
- else
116
- replacement
117
- end
113
+ # Heredocs opened in the arguments keep working in the replacement, but their
114
+ # bodies are lost when they lie within the replaced range, so they need to be
115
+ # restored after the replacement.
116
+ def removed_heredocs(filename, content, write_node)
117
+ [filename, content].flat_map { |argument| find_heredocs(argument) }
118
+ .select { |heredoc| removed?(heredoc, write_node) }
119
+ .sort_by { |heredoc| heredoc.loc.heredoc_body.begin_pos }
118
120
  end
119
121
 
120
- def heredoc?(write_node)
121
- write_node.block_type? && (first_argument = write_node.body.first_argument) &&
122
- first_argument.respond_to?(:heredoc?) && first_argument.heredoc?
122
+ def heredoc_range(heredoc)
123
+ range_between(heredoc.loc.heredoc_body.begin_pos, heredoc.loc.heredoc_end.end_pos)
123
124
  end
124
125
 
125
- def heredoc_range(first_argument)
126
- range_between(
127
- first_argument.loc.heredoc_body.begin_pos, first_argument.loc.heredoc_end.end_pos
128
- )
126
+ def find_heredocs(node)
127
+ [node, *node.each_descendant(:any_str)].select do |child|
128
+ child.respond_to?(:heredoc?) && child.heredoc?
129
+ end
130
+ end
131
+
132
+ def removed?(heredoc, write_node)
133
+ heredoc.loc.heredoc_end.end_pos <= write_node.source_range.end_pos
129
134
  end
130
135
  end
131
136
  end
@@ -8,6 +8,9 @@ module RuboCop
8
8
  # parameter. An `each` call with a block on a single line is always
9
9
  # allowed.
10
10
  #
11
+ # NOTE: `each` is preferred in idiomatic Ruby because `for` leaks
12
+ # its loop variable into the surrounding scope.
13
+ #
11
14
  # @example EnforcedStyle: each (default)
12
15
  # # bad
13
16
  # def foo