rubocop 1.91.0 → 1.92.2

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 (278) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/config/default.yml +4 -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.rb +0 -4
  14. data/lib/rubocop/config_loader.rb +6 -3
  15. data/lib/rubocop/config_loader_resolver.rb +37 -12
  16. data/lib/rubocop/config_obsoletion/removed_cop.rb +1 -1
  17. data/lib/rubocop/config_obsoletion.rb +9 -0
  18. data/lib/rubocop/config_validator.rb +11 -0
  19. data/lib/rubocop/cop/autocorrect_logic.rb +10 -5
  20. data/lib/rubocop/cop/badge.rb +1 -1
  21. data/lib/rubocop/cop/base.rb +42 -21
  22. data/lib/rubocop/cop/bundler/gem_comment.rb +0 -1
  23. data/lib/rubocop/cop/bundler/ordered_gems.rb +0 -8
  24. data/lib/rubocop/cop/cop_documentation.rb +81 -0
  25. data/lib/rubocop/cop/cop_explanation.rb +140 -0
  26. data/lib/rubocop/cop/correctors/alignment_corrector.rb +13 -1
  27. data/lib/rubocop/cop/correctors/each_to_for_corrector.rb +37 -7
  28. data/lib/rubocop/cop/correctors/empty_line_corrector.rb +1 -1
  29. data/lib/rubocop/cop/correctors/lambda_literal_to_method_corrector.rb +25 -4
  30. data/lib/rubocop/cop/correctors/multiline_literal_brace_corrector.rb +34 -0
  31. data/lib/rubocop/cop/correctors/parentheses_corrector.rb +45 -8
  32. data/lib/rubocop/cop/documentation.rb +1 -1
  33. data/lib/rubocop/cop/gemspec/dependency_version.rb +0 -6
  34. data/lib/rubocop/cop/gemspec/ordered_dependencies.rb +0 -6
  35. data/lib/rubocop/cop/internal_affairs/node_matcher_directive.rb +2 -5
  36. data/lib/rubocop/cop/internal_affairs/node_type_group.rb +0 -7
  37. data/lib/rubocop/cop/internal_affairs/redundant_context_config_parameter.rb +1 -7
  38. data/lib/rubocop/cop/internal_affairs/style_detected_api_use.rb +1 -7
  39. data/lib/rubocop/cop/layout/argument_alignment.rb +5 -1
  40. data/lib/rubocop/cop/layout/block_alignment.rb +27 -0
  41. data/lib/rubocop/cop/layout/case_indentation.rb +25 -9
  42. data/lib/rubocop/cop/layout/class_structure.rb +29 -7
  43. data/lib/rubocop/cop/layout/def_end_alignment.rb +4 -0
  44. data/lib/rubocop/cop/layout/dot_position.rb +25 -15
  45. data/lib/rubocop/cop/layout/empty_line_after_multiline_condition.rb +21 -8
  46. data/lib/rubocop/cop/layout/empty_lines.rb +5 -2
  47. data/lib/rubocop/cop/layout/empty_lines_after_module_inclusion.rb +1 -1
  48. data/lib/rubocop/cop/layout/empty_lines_around_attribute_accessor.rb +1 -0
  49. data/lib/rubocop/cop/layout/empty_lines_around_block_body.rb +9 -3
  50. data/lib/rubocop/cop/layout/empty_lines_around_exception_handling_keywords.rb +23 -1
  51. data/lib/rubocop/cop/layout/end_alignment.rb +21 -25
  52. data/lib/rubocop/cop/layout/extra_spacing.rb +21 -6
  53. data/lib/rubocop/cop/layout/first_argument_indentation.rb +27 -1
  54. data/lib/rubocop/cop/layout/first_array_element_line_break.rb +15 -0
  55. data/lib/rubocop/cop/layout/first_hash_element_indentation.rb +16 -8
  56. data/lib/rubocop/cop/layout/first_method_argument_line_break.rb +6 -0
  57. data/lib/rubocop/cop/layout/first_method_parameter_line_break.rb +4 -0
  58. data/lib/rubocop/cop/layout/hash_alignment.rb +16 -12
  59. data/lib/rubocop/cop/layout/heredoc_argument_closing_parenthesis.rb +7 -3
  60. data/lib/rubocop/cop/layout/heredoc_indentation.rb +15 -0
  61. data/lib/rubocop/cop/layout/indentation_consistency.rb +22 -5
  62. data/lib/rubocop/cop/layout/indentation_style.rb +4 -1
  63. data/lib/rubocop/cop/layout/indentation_width.rb +2 -2
  64. data/lib/rubocop/cop/layout/line_continuation_spacing.rb +8 -1
  65. data/lib/rubocop/cop/layout/line_end_string_concatenation_indentation.rb +25 -9
  66. data/lib/rubocop/cop/layout/line_length.rb +176 -26
  67. data/lib/rubocop/cop/layout/multiline_array_brace_layout.rb +12 -0
  68. data/lib/rubocop/cop/layout/multiline_block_layout.rb +8 -1
  69. data/lib/rubocop/cop/layout/multiline_hash_brace_layout.rb +9 -0
  70. data/lib/rubocop/cop/layout/multiline_method_call_brace_layout.rb +6 -0
  71. data/lib/rubocop/cop/layout/multiline_method_call_indentation.rb +30 -5
  72. data/lib/rubocop/cop/layout/multiline_method_definition_brace_layout.rb +4 -0
  73. data/lib/rubocop/cop/layout/redundant_line_break.rb +22 -1
  74. data/lib/rubocop/cop/layout/rescue_ensure_alignment.rb +16 -3
  75. data/lib/rubocop/cop/layout/single_line_block_chain.rb +10 -2
  76. data/lib/rubocop/cop/layout/space_after_not.rb +5 -0
  77. data/lib/rubocop/cop/layout/space_around_block_parameters.rb +6 -11
  78. data/lib/rubocop/cop/layout/space_around_keyword.rb +10 -0
  79. data/lib/rubocop/cop/layout/space_around_operators.rb +31 -4
  80. data/lib/rubocop/cop/layout/space_before_block_braces.rb +16 -1
  81. data/lib/rubocop/cop/layout/space_inside_array_literal_brackets.rb +15 -2
  82. data/lib/rubocop/cop/layout/space_inside_block_braces.rb +32 -9
  83. data/lib/rubocop/cop/layout/space_inside_range_literal.rb +29 -11
  84. data/lib/rubocop/cop/layout/space_inside_reference_brackets.rb +2 -12
  85. data/lib/rubocop/cop/lint/ambiguous_operator.rb +1 -1
  86. data/lib/rubocop/cop/lint/ambiguous_operator_precedence.rb +2 -1
  87. data/lib/rubocop/cop/lint/ambiguous_regexp_literal.rb +6 -1
  88. data/lib/rubocop/cop/lint/empty_conditional_body.rb +3 -1
  89. data/lib/rubocop/cop/lint/heredoc_method_call_position.rb +4 -0
  90. data/lib/rubocop/cop/lint/interpolation_check.rb +20 -17
  91. data/lib/rubocop/cop/lint/literal_as_condition.rb +15 -0
  92. data/lib/rubocop/cop/lint/literal_in_interpolation.rb +63 -19
  93. data/lib/rubocop/cop/lint/loop.rb +24 -2
  94. data/lib/rubocop/cop/lint/misplaced_magic_comment.rb +71 -11
  95. data/lib/rubocop/cop/lint/missing_cop_enable_directive.rb +18 -3
  96. data/lib/rubocop/cop/lint/nested_percent_literal.rb +3 -0
  97. data/lib/rubocop/cop/lint/number_conversion.rb +3 -1
  98. data/lib/rubocop/cop/lint/parentheses_as_grouped_expression.rb +22 -1
  99. data/lib/rubocop/cop/lint/percent_string_array.rb +75 -9
  100. data/lib/rubocop/cop/lint/redundant_cop_disable_directive.rb +39 -9
  101. data/lib/rubocop/cop/lint/redundant_dir_glob_sort.rb +13 -2
  102. data/lib/rubocop/cop/lint/redundant_splat_expansion.rb +11 -4
  103. data/lib/rubocop/cop/lint/safe_navigation_consistency.rb +47 -1
  104. data/lib/rubocop/cop/lint/symbol_conversion.rb +1 -0
  105. data/lib/rubocop/cop/lint/unescaped_bracket_in_regexp.rb +18 -1
  106. data/lib/rubocop/cop/lint/useless_assignment.rb +14 -7
  107. data/lib/rubocop/cop/lint/useless_setter_call.rb +7 -4
  108. data/lib/rubocop/cop/lint/void.rb +8 -1
  109. data/lib/rubocop/cop/metrics/class_length.rb +4 -2
  110. data/lib/rubocop/cop/metrics/cyclomatic_complexity.rb +0 -4
  111. data/lib/rubocop/cop/metrics/module_length.rb +4 -2
  112. data/lib/rubocop/cop/mixin/alignment.rb +4 -0
  113. data/lib/rubocop/cop/mixin/check_line_breakable.rb +23 -19
  114. data/lib/rubocop/cop/mixin/check_single_line_suitability.rb +8 -2
  115. data/lib/rubocop/cop/mixin/code_length.rb +10 -2
  116. data/lib/rubocop/cop/mixin/comments_help.rb +1 -1
  117. data/lib/rubocop/cop/mixin/empty_lines_around_body.rb +12 -1
  118. data/lib/rubocop/cop/mixin/endless_method_body_help.rb +105 -0
  119. data/lib/rubocop/cop/mixin/forbidden_identifiers.rb +2 -2
  120. data/lib/rubocop/cop/mixin/hash_alignment_styles.rb +41 -6
  121. data/lib/rubocop/cop/mixin/hash_shorthand_syntax.rb +26 -2
  122. data/lib/rubocop/cop/mixin/min_branches_count.rb +3 -1
  123. data/lib/rubocop/cop/mixin/multiline_element_line_breaks.rb +11 -1
  124. data/lib/rubocop/cop/mixin/multiline_literal_brace_layout.rb +9 -0
  125. data/lib/rubocop/cop/mixin/ordered_gem_node.rb +1 -1
  126. data/lib/rubocop/cop/mixin/pattern_matching_help.rb +35 -0
  127. data/lib/rubocop/cop/mixin/preceding_following_alignment.rb +17 -2
  128. data/lib/rubocop/cop/mixin/project_index_help.rb +15 -0
  129. data/lib/rubocop/cop/mixin/reparsed_equivalence.rb +13 -3
  130. data/lib/rubocop/cop/mixin/statement_modifier.rb +39 -5
  131. data/lib/rubocop/cop/mixin/trailing_comma.rb +1 -1
  132. data/lib/rubocop/cop/mixin/visibility_help.rb +2 -1
  133. data/lib/rubocop/cop/mixin.rb +2 -0
  134. data/lib/rubocop/cop/naming/accessor_method_name.rb +0 -16
  135. data/lib/rubocop/cop/naming/block_forwarding.rb +1 -1
  136. data/lib/rubocop/cop/naming/heredoc_delimiter_case.rb +9 -2
  137. data/lib/rubocop/cop/naming/heredoc_delimiter_naming.rb +5 -3
  138. data/lib/rubocop/cop/naming/predicate_prefix.rb +7 -22
  139. data/lib/rubocop/cop/naming/rescued_exceptions_variable_name.rb +1 -1
  140. data/lib/rubocop/cop/offense.rb +59 -3
  141. data/lib/rubocop/cop/registry.rb +1 -1
  142. data/lib/rubocop/cop/security/json_load.rb +16 -4
  143. data/lib/rubocop/cop/style/access_modifier_declarations.rb +37 -2
  144. data/lib/rubocop/cop/style/alias.rb +18 -5
  145. data/lib/rubocop/cop/style/ambiguous_endless_method_definition.rb +4 -3
  146. data/lib/rubocop/cop/style/and_or.rb +37 -10
  147. data/lib/rubocop/cop/style/arguments_forwarding.rb +32 -3
  148. data/lib/rubocop/cop/style/attr.rb +3 -4
  149. data/lib/rubocop/cop/style/bitwise_predicate.rb +6 -4
  150. data/lib/rubocop/cop/style/block_delimiters.rb +35 -3
  151. data/lib/rubocop/cop/style/case_equality.rb +14 -3
  152. data/lib/rubocop/cop/style/case_like_if.rb +6 -3
  153. data/lib/rubocop/cop/style/class_and_module_children.rb +57 -16
  154. data/lib/rubocop/cop/style/class_methods_definitions.rb +15 -0
  155. data/lib/rubocop/cop/style/collection_querying.rb +79 -46
  156. data/lib/rubocop/cop/style/combinable_loops.rb +89 -11
  157. data/lib/rubocop/cop/style/concat_array_literals.rb +58 -7
  158. data/lib/rubocop/cop/style/conditional_assignment.rb +4 -0
  159. data/lib/rubocop/cop/style/disable_cops_within_source_code_directive.rb +65 -9
  160. data/lib/rubocop/cop/style/double_negation.rb +11 -0
  161. data/lib/rubocop/cop/style/empty_literal.rb +1 -1
  162. data/lib/rubocop/cop/style/empty_method.rb +3 -1
  163. data/lib/rubocop/cop/style/empty_string_inside_interpolation.rb +53 -11
  164. data/lib/rubocop/cop/style/endless_method.rb +104 -11
  165. data/lib/rubocop/cop/style/exact_regexp_match.rb +45 -8
  166. data/lib/rubocop/cop/style/explicit_block_argument.rb +50 -10
  167. data/lib/rubocop/cop/style/float_division.rb +37 -2
  168. data/lib/rubocop/cop/style/for.rb +53 -2
  169. data/lib/rubocop/cop/style/format_string.rb +28 -6
  170. data/lib/rubocop/cop/style/frozen_string_literal_comment.rb +2 -1
  171. data/lib/rubocop/cop/style/guard_clause.rb +31 -22
  172. data/lib/rubocop/cop/style/hash_as_last_array_item.rb +4 -0
  173. data/lib/rubocop/cop/style/hash_conversion.rb +28 -8
  174. data/lib/rubocop/cop/style/hash_lookup_method.rb +42 -9
  175. data/lib/rubocop/cop/style/hash_syntax.rb +16 -3
  176. data/lib/rubocop/cop/style/identical_conditional_branches.rb +22 -3
  177. data/lib/rubocop/cop/style/if_inside_else.rb +21 -1
  178. data/lib/rubocop/cop/style/if_unless_modifier.rb +32 -26
  179. data/lib/rubocop/cop/style/if_unless_modifier_of_if_unless.rb +4 -0
  180. data/lib/rubocop/cop/style/if_with_semicolon.rb +47 -4
  181. data/lib/rubocop/cop/style/inverse_methods.rb +24 -2
  182. data/lib/rubocop/cop/style/it_block_parameter.rb +94 -4
  183. data/lib/rubocop/cop/style/keyword_parameters_order.rb +3 -1
  184. data/lib/rubocop/cop/style/lambda.rb +34 -1
  185. data/lib/rubocop/cop/style/lambda_call.rb +7 -2
  186. data/lib/rubocop/cop/style/map_into_array.rb +1 -2
  187. data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +80 -13
  188. data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +26 -2
  189. data/lib/rubocop/cop/style/method_call_without_args_parentheses.rb +26 -3
  190. data/lib/rubocop/cop/style/method_def_parentheses.rb +38 -4
  191. data/lib/rubocop/cop/style/missing_else.rb +7 -1
  192. data/lib/rubocop/cop/style/mixin_grouping.rb +12 -3
  193. data/lib/rubocop/cop/style/multiline_if_modifier.rb +45 -5
  194. data/lib/rubocop/cop/style/multiline_in_pattern_then.rb +7 -0
  195. data/lib/rubocop/cop/style/multiline_method_signature.rb +2 -5
  196. data/lib/rubocop/cop/style/multiline_ternary_operator.rb +7 -3
  197. data/lib/rubocop/cop/style/multiline_when_then.rb +7 -0
  198. data/lib/rubocop/cop/style/mutable_constant.rb +52 -7
  199. data/lib/rubocop/cop/style/negated_if_else_condition.rb +2 -1
  200. data/lib/rubocop/cop/style/negative_array_index.rb +9 -1
  201. data/lib/rubocop/cop/style/nested_modifier.rb +27 -8
  202. data/lib/rubocop/cop/style/nested_ternary_operator.rb +26 -8
  203. data/lib/rubocop/cop/style/next.rb +24 -10
  204. data/lib/rubocop/cop/style/nil_comparison.rb +16 -4
  205. data/lib/rubocop/cop/style/non_nil_check.rb +1 -1
  206. data/lib/rubocop/cop/style/not.rb +10 -5
  207. data/lib/rubocop/cop/style/numeric_predicate.rb +28 -5
  208. data/lib/rubocop/cop/style/one_line_conditional.rb +13 -2
  209. data/lib/rubocop/cop/style/parentheses_around_condition.rb +19 -1
  210. data/lib/rubocop/cop/style/percent_literal_delimiters.rb +14 -3
  211. data/lib/rubocop/cop/style/quoted_symbols.rb +5 -7
  212. data/lib/rubocop/cop/style/raise_args.rb +80 -11
  213. data/lib/rubocop/cop/style/reduce_to_hash.rb +47 -33
  214. data/lib/rubocop/cop/style/redundant_begin.rb +45 -5
  215. data/lib/rubocop/cop/style/redundant_condition.rb +30 -13
  216. data/lib/rubocop/cop/style/redundant_double_splat_hash_braces.rb +6 -0
  217. data/lib/rubocop/cop/style/redundant_exception.rb +27 -11
  218. data/lib/rubocop/cop/style/redundant_fetch_block.rb +9 -1
  219. data/lib/rubocop/cop/style/redundant_freeze.rb +11 -3
  220. data/lib/rubocop/cop/style/redundant_interpolation_unfreeze.rb +24 -1
  221. data/lib/rubocop/cop/style/redundant_parentheses.rb +12 -1
  222. data/lib/rubocop/cop/style/redundant_regexp_argument.rb +7 -0
  223. data/lib/rubocop/cop/style/redundant_regexp_constructor.rb +20 -0
  224. data/lib/rubocop/cop/style/redundant_regexp_escape.rb +4 -0
  225. data/lib/rubocop/cop/style/redundant_self.rb +9 -1
  226. data/lib/rubocop/cop/style/redundant_self_assignment_branch.rb +28 -0
  227. data/lib/rubocop/cop/style/redundant_sort.rb +38 -4
  228. data/lib/rubocop/cop/style/regexp_literal.rb +75 -26
  229. data/lib/rubocop/cop/style/require_order.rb +25 -8
  230. data/lib/rubocop/cop/style/rescue_modifier.rb +11 -9
  231. data/lib/rubocop/cop/style/return_nil.rb +17 -2
  232. data/lib/rubocop/cop/style/return_nil_in_predicate_method_definition.rb +11 -1
  233. data/lib/rubocop/cop/style/sample.rb +9 -8
  234. data/lib/rubocop/cop/style/self_assignment.rb +12 -15
  235. data/lib/rubocop/cop/style/semicolon.rb +16 -5
  236. data/lib/rubocop/cop/style/send_with_literal_method_name.rb +4 -0
  237. data/lib/rubocop/cop/style/single_line_methods.rb +42 -4
  238. data/lib/rubocop/cop/style/stabby_lambda_parentheses.rb +29 -5
  239. data/lib/rubocop/cop/style/string_concatenation.rb +21 -4
  240. data/lib/rubocop/cop/style/struct_inheritance.rb +6 -0
  241. data/lib/rubocop/cop/style/super_arguments.rb +3 -0
  242. data/lib/rubocop/cop/style/symbol_proc.rb +24 -6
  243. data/lib/rubocop/cop/style/ternary_parentheses.rb +23 -1
  244. data/lib/rubocop/cop/style/trailing_underscore_variable.rb +23 -19
  245. data/lib/rubocop/cop/style/while_until_modifier.rb +1 -1
  246. data/lib/rubocop/cop/style/yoda_condition.rb +24 -9
  247. data/lib/rubocop/cop/style/yoda_expression.rb +10 -1
  248. data/lib/rubocop/cop/team.rb +10 -5
  249. data/lib/rubocop/cop/variable_force/assignment.rb +1 -1
  250. data/lib/rubocop/cop/variable_force/variable.rb +14 -1
  251. data/lib/rubocop/formatter/base_formatter.rb +22 -0
  252. data/lib/rubocop/formatter/formatter_set.rb +4 -0
  253. data/lib/rubocop/formatter/json_formatter.rb +71 -1
  254. data/lib/rubocop/formatter/offense_count_formatter.rb +3 -1
  255. data/lib/rubocop/formatter/pacman_formatter.rb +3 -1
  256. data/lib/rubocop/formatter/sarif_formatter.rb +12 -7
  257. data/lib/rubocop/formatter/worst_offenders_formatter.rb +3 -1
  258. data/lib/rubocop/lsp/routes.rb +23 -23
  259. data/lib/rubocop/lsp/runtime.rb +47 -11
  260. data/lib/rubocop/lsp/stdin_runner.rb +11 -5
  261. data/lib/rubocop/lsp.rb +5 -2
  262. data/lib/rubocop/mcp/autocorrection_request.rb +52 -0
  263. data/lib/rubocop/mcp/explain_tool.rb +68 -0
  264. data/lib/rubocop/mcp/scope.rb +65 -0
  265. data/lib/rubocop/mcp/server.rb +184 -76
  266. data/lib/rubocop/mcp/source_files.rb +35 -0
  267. data/lib/rubocop/offense_limit.rb +30 -0
  268. data/lib/rubocop/options.rb +45 -10
  269. data/lib/rubocop/plugin/configuration_integrator.rb +36 -8
  270. data/lib/rubocop/remote_config.rb +4 -1
  271. data/lib/rubocop/result_cache.rb +20 -13
  272. data/lib/rubocop/runner.rb +110 -42
  273. data/lib/rubocop/server/cache.rb +6 -6
  274. data/lib/rubocop/target_ruby.rb +21 -9
  275. data/lib/rubocop/util.rb +69 -0
  276. data/lib/rubocop/version.rb +1 -1
  277. data/lib/rubocop.rb +5 -0
  278. metadata +16 -5
@@ -69,9 +69,13 @@ module RuboCop
69
69
 
70
70
  # @!method variable_argument?(node)
71
71
  def_node_matcher :variable_argument?, <<~PATTERN
72
- (send {str dstr} :% #autocorrectable?)
72
+ (send {str dstr} :% !#autocorrectable?)
73
73
  PATTERN
74
74
 
75
+ def on_new_investigation
76
+ @corrected_ranges = nil
77
+ end
78
+
75
79
  def on_send(node)
76
80
  formatter(node) do |selector|
77
81
  detected_style = selector == :% ? :percent : selector
@@ -86,10 +90,12 @@ module RuboCop
86
90
 
87
91
  private
88
92
 
93
+ # Only an argument that is provably not an array can be corrected,
94
+ # because `String#%` spreads an array argument and `format` does not.
89
95
  def autocorrectable?(node)
90
- return true if node.lvar_type?
96
+ return true if node.literal?
91
97
 
92
- node.send_type? && !AUTOCORRECTABLE_METHODS.include?(node.method_name)
98
+ node.call_type? && AUTOCORRECTABLE_METHODS.include?(node.method_name)
93
99
  end
94
100
 
95
101
  def message(detected_style)
@@ -101,7 +107,9 @@ module RuboCop
101
107
  end
102
108
 
103
109
  def autocorrect(corrector, node)
104
- return if variable_argument?(node)
110
+ return if variable_argument?(node) || within_corrected_range?(node)
111
+
112
+ corrected_ranges << node.source_range
105
113
 
106
114
  case node.method_name
107
115
  when :%
@@ -116,6 +124,14 @@ module RuboCop
116
124
  end
117
125
  end
118
126
 
127
+ def within_corrected_range?(node)
128
+ corrected_ranges.any? { |range| range.contains?(node.source_range) }
129
+ end
130
+
131
+ def corrected_ranges
132
+ @corrected_ranges ||= []
133
+ end
134
+
119
135
  def autocorrect_from_percent(corrector, node)
120
136
  percent_rhs = node.first_argument
121
137
  args = case percent_rhs.type
@@ -146,14 +162,20 @@ module RuboCop
146
162
  def format_single_parameter(arg)
147
163
  # `format(fmt, *args)` is equivalent to `fmt % args`, so unwrap the splat
148
164
  # and render the argument it splats.
149
- return format_single_parameter(arg.children.first) if arg.splat_type?
165
+ return format_splatted_parameter(arg.children.first) if arg.splat_type?
150
166
 
151
167
  source = arg.source
152
- return "{ #{source} }" if arg.hash_type?
168
+ return "{ #{source} }" if arg.hash_type? && !arg.braces?
153
169
 
154
170
  requires_parentheses?(arg) ? "(#{source})" : source
155
171
  end
156
172
 
173
+ def format_splatted_parameter(arg)
174
+ return "#{arg.source}.to_a" if arg.hash_type?
175
+
176
+ format_single_parameter(arg)
177
+ end
178
+
157
179
  # An argument that binds looser than `%` (a ternary, range, assignment, or
158
180
  # operator call) must be parenthesized to keep its meaning.
159
181
  def requires_parentheses?(arg)
@@ -142,7 +142,8 @@ module RuboCop
142
142
  end
143
143
 
144
144
  next_token = processed_source.tokens[token_number]
145
- if next_token&.text&.valid_encoding? && Encoding::ENCODING_PATTERN.match?(next_token.text)
145
+ if next_token&.text&.valid_encoding? &&
146
+ MagicComment.parse(next_token.text).encoding_specified?
146
147
  token = next_token
147
148
  end
148
149
 
@@ -186,40 +186,37 @@ module RuboCop
186
186
  return if trivial?(node)
187
187
 
188
188
  example = "#{conditional_keyword} #{condition.source}; #{scope_exiting_keyword}; end"
189
- replacement = <<~RUBY.chomp
190
- #{conditional_keyword} #{condition.source}
191
- #{scope_exiting_keyword}
192
- end
193
- RUBY
189
+ replacement = "#{conditional_keyword} #{condition.source}\n #{scope_exiting_keyword}"
194
190
  end
195
191
 
196
192
  add_offense(node.loc.keyword, message: format(MSG, example: example)) do |corrector|
197
193
  next if node.else? && guard.nil?
198
194
 
199
- autocorrect(corrector, node, condition, replacement || example, guard)
195
+ autocorrect(corrector, node, example, replacement, guard)
200
196
  end
201
197
  end
202
198
 
203
199
  # rubocop:disable-next Metrics/AbcSize
204
- def autocorrect(corrector, node, condition, replacement, guard)
205
- corrector.replace(node.loc.keyword.join(condition.source_range), replacement)
206
-
207
- if_branch = node.if_branch
208
- else_branch = node.else_branch
209
-
200
+ def autocorrect(corrector, node, example, multiline_replacement, guard)
201
+ range = node.loc.keyword.join(node.condition.source_range)
202
+ corrector.replace(range, multiline_replacement || example)
210
203
  corrector.replace(node.loc.begin, "\n") if node.then?
211
204
 
212
- if (if_heredoc = find_heredoc_argument(if_branch))
213
- autocorrect_heredoc_argument(corrector, node, if_heredoc, else_branch, guard)
214
- elsif (else_heredoc = find_heredoc_argument(else_branch))
215
- autocorrect_heredoc_argument(corrector, node, else_heredoc, if_branch, guard)
216
- else
217
- corrector.remove(node.loc.end)
218
- return unless node.else?
205
+ branches = [node.if_branch, node.else_branch]
206
+ guard_branch, leave_branch = guard == :else ? branches.reverse : branches
219
207
 
220
- corrector.remove(node.loc.else)
221
- corrector.remove(range_of_branch_to_remove(node, guard))
208
+ heredoc = find_heredoc_argument(guard_branch)
209
+ close_multiline_guard_clause(corrector, range, heredoc, guard) if multiline_replacement
210
+
211
+ if heredoc
212
+ return autocorrect_heredoc_argument(corrector, node, heredoc, leave_branch, guard)
222
213
  end
214
+
215
+ corrector.remove(node.loc.end)
216
+ return unless node.else?
217
+
218
+ corrector.remove(node.loc.else)
219
+ corrector.remove(range_of_branch_to_remove(node, guard))
223
220
  end
224
221
 
225
222
  def heredoc?(argument)
@@ -241,6 +238,10 @@ module RuboCop
241
238
  find_heredoc_argument(node.receiver)
242
239
  end
243
240
 
241
+ def close_multiline_guard_clause(corrector, range, heredoc, guard)
242
+ corrector.insert_after(guard && heredoc ? heredoc.loc.heredoc_end : range, "\nend")
243
+ end
244
+
244
245
  def autocorrect_heredoc_argument(corrector, node, heredoc_node, leave_branch, guard)
245
246
  remove_whole_lines(corrector, node.loc.end)
246
247
  return unless node.else?
@@ -282,7 +283,15 @@ module RuboCop
282
283
  end
283
284
 
284
285
  def accepted_form?(node, ending: false)
285
- accepted_if?(node, ending) || node.condition.multiline? || node.parent&.assignment?
286
+ accepted_if?(node, ending) || node.condition.multiline? ||
287
+ node.parent&.assignment? || endless_method_body?(node)
288
+ end
289
+
290
+ # The body of an endless method is a single expression, so there is no
291
+ # room for a guard clause followed by the rest of the code.
292
+ def endless_method_body?(node)
293
+ parent = node.parent
294
+ parent&.any_def_type? && parent.endless?
286
295
  end
287
296
 
288
297
  def trivial?(node)
@@ -44,6 +44,10 @@ module RuboCop
44
44
  include ConfigurableEnforcedStyle
45
45
  extend AutoCorrector
46
46
 
47
+ def self.autocorrect_incompatible_with
48
+ [Layout::MultilineArrayBraceLayout, Style::TrailingCommaInArrayLiteral]
49
+ end
50
+
47
51
  def on_hash(node)
48
52
  return if node.children.first&.kwsplat_type?
49
53
  return unless (array = containing_array(node))
@@ -53,6 +53,12 @@ module RuboCop
53
53
  # @!method hash_from_array?(node)
54
54
  def_node_matcher :hash_from_array?, '(send (const {nil? cbase} :Hash) :[] ...)'
55
55
 
56
+ # A hash literal as the first argument needs the parentheses of the call,
57
+ # which `Style/MethodCallWithArgsParentheses` removes in the same pass.
58
+ def self.autocorrect_incompatible_with
59
+ [Style::MethodCallWithArgsParentheses]
60
+ end
61
+
56
62
  def on_send(node)
57
63
  return if part_of_ignored_node?(node) || !hash_from_array?(node)
58
64
 
@@ -95,12 +101,30 @@ module RuboCop
95
101
  def register_offense_for_hash(node, hash_argument)
96
102
  add_offense(node, message: MSG_LITERAL_HASH_ARG) do |corrector|
97
103
  corrector.replace(node, "{#{hash_argument.source}}")
98
-
99
- parent = node.parent
100
- add_parentheses(parent, corrector) if parent&.send_type? && !parent.parenthesized?
104
+ add_parentheses_to_parent_call(node, corrector)
101
105
  end
102
106
  end
103
107
 
108
+ # Without parentheses, a hash literal at the start of the first argument would be
109
+ # parsed as a block. That includes an argument like `Hash[a, b].merge(c)`.
110
+ def add_parentheses_to_parent_call(node, corrector)
111
+ node = node.parent while starts_parent?(node)
112
+ parent = node.parent
113
+ return unless parent&.type?(:call, :super, :yield)
114
+ return if parent.parenthesized? || operator_syntax?(parent)
115
+
116
+ add_parentheses(parent, corrector)
117
+ end
118
+
119
+ def starts_parent?(node)
120
+ node.parent && node.parent.source_range.begin_pos == node.source_range.begin_pos
121
+ end
122
+
123
+ # `foo[x]`, `foo.bar = x` and `foo == x` take a hash literal as it is.
124
+ def operator_syntax?(call)
125
+ call.setter_method? || (call.operator_method? && !call.loc?(:dot))
126
+ end
127
+
104
128
  def register_offense_for_zip_method(node, zip_method)
105
129
  add_offense(node, message: MSG_TO_H) do |corrector|
106
130
  if zip_method.parenthesized?
@@ -135,11 +159,7 @@ module RuboCop
135
159
  def correct_multi_argument(node)
136
160
  add_offense(node, message: MSG_LITERAL_MULTI_ARG) do |corrector|
137
161
  corrector.replace(node, args_to_hash(node.arguments))
138
-
139
- parent = node.parent
140
- if parent&.send_type? && !parent.method?(:to_h) && !parent.parenthesized?
141
- add_parentheses(parent, corrector)
142
- end
162
+ add_parentheses_to_parent_call(node, corrector)
143
163
  end
144
164
  end
145
165
 
@@ -34,6 +34,9 @@ module RuboCop
34
34
  # # good - fetch with block is allowed
35
35
  # hash.fetch(key) { default }
36
36
  #
37
+ # # good - `ENV.fetch` is left to `Style/FetchEnvVar` when it is enabled
38
+ # ENV.fetch(key)
39
+ #
37
40
  # @example EnforcedStyle: fetch
38
41
  # # bad
39
42
  # hash[key]
@@ -55,6 +58,9 @@ module RuboCop
55
58
 
56
59
  RESTRICT_ON_SEND = %i[[] fetch].freeze
57
60
 
61
+ # @!method env_const?(node)
62
+ def_node_matcher :env_const?, '(const {nil? cbase} :ENV)'
63
+
58
64
  def on_send(node)
59
65
  return if (receiver = node.receiver) && allowed_receiver?(receiver)
60
66
  return if part_of_ignored_node?(node)
@@ -76,28 +82,55 @@ module RuboCop
76
82
  private
77
83
 
78
84
  def offense_for_brackets?(node)
79
- style == :brackets && node.receiver && node.method?(:fetch) && node.arguments.one? &&
80
- !node.block_literal? && !node.csend_type?
85
+ return false if style != :brackets || !node.receiver
86
+ return false if !node.method?(:fetch) || !node.arguments.one?
87
+ return false if node.block_literal? || node.csend_type?
88
+
89
+ !exempt_from_brackets?(node)
90
+ end
91
+
92
+ def exempt_from_brackets?(node)
93
+ return true if forwarded_or_splatted?(node.first_argument)
94
+
95
+ comment_before_dot?(node) || env_fetch_preferred?(node.receiver)
96
+ end
97
+
98
+ def forwarded_or_splatted?(argument)
99
+ if argument.hash_type? && !argument.braces?
100
+ argument.children.any? { |child| child.type?(:kwsplat, :forwarded_kwrestarg) }
101
+ else
102
+ argument.type?(:forwarded_args, :forwarded_restarg, :splat, :block_pass)
103
+ end
104
+ end
105
+
106
+ def comment_before_dot?(node)
107
+ processed_source.contains_comment?(node.receiver.source_range.end.join(node.loc.dot))
108
+ end
109
+
110
+ def env_fetch_preferred?(receiver)
111
+ env_const?(receiver) && processed_source.registry.enabled?(Style::FetchEnvVar, config)
81
112
  end
82
113
 
83
114
  def offense_for_fetch?(node)
84
- style == :fetch && node.method?(:[]) && node.arguments.one?
115
+ style == :fetch && node.method?(:[]) && node.arguments.one? &&
116
+ !compound_assignment_target?(node)
117
+ end
118
+
119
+ def compound_assignment_target?(node)
120
+ node.parent&.type?(:op_asgn, :or_asgn, :and_asgn) && node.parent.children.first == node
85
121
  end
86
122
 
87
123
  def correct_fetch_to_brackets(corrector, node)
88
124
  key = node.first_argument.source
89
125
 
90
- corrector.replace(node.loc.dot.join(node.source_range.end), "[#{key}]")
126
+ corrector.replace(node.receiver.source_range.end.join(node.source_range.end), "[#{key}]")
91
127
  end
92
128
 
93
129
  def correct_brackets_to_fetch(corrector, node)
94
130
  key = node.first_argument.source
131
+ replacement = node.loc.dot ? "fetch(#{key})" : ".fetch(#{key})"
95
132
 
96
- if node.csend_type?
97
- corrector.replace(node.loc.dot.join(node.source_range.end), "&.fetch(#{key})")
98
- else
99
- corrector.replace(node.loc.selector.join(node.source_range.end), ".fetch(#{key})")
100
- end
133
+ corrector.replace(node.loc.selector.join(node.source_range.end), replacement)
101
134
  end
102
135
  end
103
136
  end
@@ -142,6 +142,13 @@ module RuboCop
142
142
  MSG_HASH_ROCKETS = 'Use hash rockets syntax.'
143
143
  NO_MIXED_KEYS_STYLES = %i[ruby19_no_mixed_keys no_mixed_keys].freeze
144
144
 
145
+ # Omitting the last hash value of a call without parentheses adds the parentheses,
146
+ # which `Style/MethodCallWithArgsParentheses` and `Style/SuperWithArgsParentheses`
147
+ # would add again in the same pass.
148
+ def self.autocorrect_incompatible_with
149
+ [Style::MethodCallWithArgsParentheses, Style::SuperWithArgsParentheses]
150
+ end
151
+
145
152
  def on_hash(node)
146
153
  pairs = node.pairs
147
154
 
@@ -241,6 +248,8 @@ module RuboCop
241
248
  def check(pairs, delim, msg)
242
249
  pairs.each do |pair|
243
250
  if pair.delimiter == delim
251
+ next if pair.value_omission? && keyword_value?(pair)
252
+
244
253
  location = pair.source_range.begin.join(pair.loc.operator)
245
254
  add_offense(location, message: msg) do |corrector|
246
255
  autocorrect(corrector, pair)
@@ -283,12 +292,16 @@ module RuboCop
283
292
  end
284
293
 
285
294
  def autocorrect_hash_rockets(corrector, pair_node)
286
- op = pair_node.loc.operator
287
-
288
295
  key_with_hash_rocket = ":#{pair_node.key.source}#{pair_node.inverse_delimiter(true)}"
289
296
  key_with_hash_rocket += pair_node.key.source if pair_node.value_omission?
290
297
  corrector.replace(pair_node.key, key_with_hash_rocket)
291
- corrector.remove(range_with_surrounding_space(op))
298
+ corrector.remove(operator_range(pair_node))
299
+ end
300
+
301
+ def operator_range(pair_node)
302
+ op = pair_node.loc.operator
303
+
304
+ range_with_surrounding_space(op, newlines: !pair_node.value_omission?)
292
305
  end
293
306
 
294
307
  def autocorrect_no_mixed_keys(corrector, pair_node)
@@ -188,7 +188,7 @@ module RuboCop
188
188
  end
189
189
  end
190
190
 
191
- # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
191
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
192
192
  def check_expressions(node, expressions, insert_position)
193
193
  return if expressions.any?(&:nil?)
194
194
 
@@ -198,8 +198,7 @@ module RuboCop
198
198
  add_offense(expression) do |corrector|
199
199
  next if node.if_type? && (node.ternary? || node.then?)
200
200
 
201
- range = range_by_whole_lines(expression.source_range, include_final_newline: true)
202
- corrector.remove(range)
201
+ corrector.remove(removal_range(expression))
203
202
  next if inserted_expression
204
203
 
205
204
  if node.parent&.assignment?
@@ -213,6 +212,26 @@ module RuboCop
213
212
  end
214
213
  end
215
214
 
215
+ def removal_range(expression)
216
+ range = expression.source_range
217
+ whole_lines = range_by_whole_lines(range, include_final_newline: true)
218
+ return whole_lines if whole_lines.source.strip == expression.source
219
+
220
+ range_with_separator(range)
221
+ end
222
+
223
+ def range_with_separator(range)
224
+ source = range.source_buffer.source
225
+
226
+ if (following = source[range.end_pos..][/\A[ \t]*;[ \t]*/])
227
+ range.adjust(end_pos: following.length)
228
+ elsif (preceding = source[0...range.begin_pos][/[ \t]*;[ \t]*\z/])
229
+ range.adjust(begin_pos: -preceding.length)
230
+ else
231
+ range_with_surrounding_space(range, side: :right, newlines: false)
232
+ end
233
+ end
234
+
216
235
  def correct_assignment(corrector, node, expression, insert_position)
217
236
  indentation = indentation_of(node.parent)
218
237
 
@@ -75,7 +75,7 @@ module RuboCop
75
75
  return if comments_between_else_and_if?(node, else_branch)
76
76
 
77
77
  add_offense(else_branch.loc.keyword) do |corrector|
78
- next if part_of_ignored_node?(node)
78
+ next if part_of_ignored_node?(node) || shares_lines_with_other_code?(else_branch)
79
79
 
80
80
  autocorrect(corrector, else_branch)
81
81
  ignore_node(node)
@@ -84,6 +84,26 @@ module RuboCop
84
84
 
85
85
  private
86
86
 
87
+ # The `elsif` correction moves and removes whole lines, so it can't be applied when
88
+ # the nested `if` condition, its `if` branch or its `end` shares a line with other code.
89
+ # A modifier `if` on the `else` line would leave its body on the `elsif` line.
90
+ def shares_lines_with_other_code?(node)
91
+ return false if node.then?
92
+ return !begins_its_line?(node.source_range) if node.modifier_form?
93
+
94
+ moved_ranges(node).any? { |range| !begins_its_line?(range) || !ends_its_line?(range) }
95
+ end
96
+
97
+ def moved_ranges(node)
98
+ ranges = [if_condition_range(node, node.condition), find_end_range(node)]
99
+ ranges << range_with_comments(node.if_branch) if node.if_branch
100
+ ranges
101
+ end
102
+
103
+ def ends_its_line?(range)
104
+ processed_source.lines[range.last_line - 1][range.last_column..].match?(/\A\s*(#|\z)/)
105
+ end
106
+
87
107
  def autocorrect(corrector, node)
88
108
  if node.then?
89
109
  # If the nested `if` is a then node, correct it first,
@@ -100,6 +100,7 @@ module RuboCop
100
100
  pattern_matching_nodes(condition).any?
101
101
 
102
102
  return unless (msg = message(node))
103
+ return if modifier_sharing_line_in_logical_operator?(node)
103
104
 
104
105
  add_offense(node.loc.keyword, message: format(msg, keyword: node.keyword)) do |corrector|
105
106
  next if part_of_ignored_node?(node)
@@ -159,7 +160,7 @@ module RuboCop
159
160
  corrector.replace(node, replacement)
160
161
  end
161
162
 
162
- def replacement_for_modifier_form(corrector, node) # rubocop:disable Metrics/AbcSize
163
+ def replacement_for_modifier_form(corrector, node)
163
164
  comment = comment_on_node_line(node)
164
165
  if comment && too_long_due_to_comment_after_modifier?(node, comment)
165
166
  remove_comment(corrector, node, comment)
@@ -167,12 +168,11 @@ module RuboCop
167
168
  return to_modifier_form_with_move_comment(node, indent(node), comment)
168
169
  end
169
170
 
170
- last_argument = node.if_branch.last_argument if node.if_branch.send_type?
171
- if last_argument.respond_to?(:heredoc?) && last_argument.heredoc?
172
- heredoc = extract_heredoc_from(last_argument)
173
- remove_heredoc(corrector, heredoc)
171
+ heredoc_ranges = heredoc_ranges_in(node)
172
+ unless heredoc_ranges.empty?
173
+ remove_heredoc(corrector, heredoc_ranges)
174
174
 
175
- return to_normal_form_with_heredoc(node, indent(node), heredoc)
175
+ return to_normal_form_with_heredoc(node, indent(node))
176
176
  end
177
177
 
178
178
  to_normal_form(node, indent(node))
@@ -234,11 +234,18 @@ module RuboCop
234
234
  end
235
235
 
236
236
  def collection_from_ancestor(node)
237
- return node if node&.type?(:array, :call)
237
+ return node if node&.type?(:array, :call, :and, :or)
238
238
 
239
239
  node.parent if node&.type?(:pair)
240
240
  end
241
241
 
242
+ def modifier_sharing_line_in_logical_operator?(node)
243
+ collection = find_containing_collection(node)
244
+ return false if !node.modifier_form? || !collection&.operator_keyword?
245
+
246
+ another_modifier_if_on_same_line?(node)
247
+ end
248
+
242
249
  def another_modifier_if_on_same_line?(node)
243
250
  collection = find_containing_collection(node)
244
251
  return false unless collection
@@ -275,16 +282,20 @@ module RuboCop
275
282
  RUBY
276
283
  end
277
284
 
278
- def to_normal_form_with_heredoc(node, indentation, heredoc)
279
- heredoc_body, heredoc_end = heredoc
285
+ def remove_heredoc(corrector, heredoc_ranges)
286
+ heredoc_ranges.each do |range|
287
+ corrector.remove(range_by_whole_lines(range, include_final_newline: true))
288
+ end
289
+ end
280
290
 
281
- <<~RUBY.chomp
282
- #{node.keyword} #{node.condition.source}
283
- #{indentation} #{node.body.source}
284
- #{indentation} #{heredoc_body.source.chomp}
285
- #{indentation} #{heredoc_end.source.chomp}
286
- #{indentation}end
287
- RUBY
291
+ def to_normal_form_with_heredoc(node, indentation)
292
+ [
293
+ "#{node.keyword} #{node.condition.source}",
294
+ *heredoc_ranges_in(node.condition).map(&:source),
295
+ "#{indentation} #{node.body.source}",
296
+ *heredoc_ranges_in(node.if_branch).map(&:source),
297
+ "#{indentation}end"
298
+ ].join("\n")
288
299
  end
289
300
 
290
301
  def to_modifier_form_with_move_comment(node, indentation, comment)
@@ -294,17 +305,12 @@ module RuboCop
294
305
  RUBY
295
306
  end
296
307
 
297
- def extract_heredoc_from(last_argument)
298
- heredoc_body = last_argument.loc.heredoc_body
299
- heredoc_end = last_argument.loc.heredoc_end
300
-
301
- [heredoc_body, heredoc_end]
302
- end
303
-
304
- def remove_heredoc(corrector, heredoc)
305
- heredoc.each do |range|
306
- corrector.remove(range_by_whole_lines(range, include_final_newline: true))
308
+ def heredoc_ranges_in(body)
309
+ ranges = body.each_node(:any_str).select(&:heredoc?).map do |heredoc|
310
+ range_by_whole_lines(heredoc.loc.heredoc_body.join(heredoc.loc.heredoc_end))
307
311
  end
312
+ # A heredoc opened in another heredoc's interpolation is moved along with it.
313
+ ranges.reject { |range| ranges.any? { |other| other.contains?(range) } }
308
314
  end
309
315
 
310
316
  def comment_on_node_line(node)
@@ -28,6 +28,10 @@ module RuboCop
28
28
 
29
29
  MSG = 'Avoid modifier `%<keyword>s` after another conditional.'
30
30
 
31
+ def self.autocorrect_incompatible_with
32
+ [Style::SoleNestedConditional]
33
+ end
34
+
31
35
  # rubocop:disable-next Metrics/AbcSize
32
36
  def on_if(node)
33
37
  return unless node.modifier_form? && node.body.if_type?
@@ -21,6 +21,10 @@ module RuboCop
21
21
  MSG_NEWLINE = 'Do not use `%<keyword>s %<expr>s;` - use a newline instead.'
22
22
  MSG_TERNARY = 'Do not use `%<keyword>s %<expr>s;` - use a ternary operator instead.'
23
23
 
24
+ LOWER_PRECEDENCE_THAN_TERNARY_TYPES = %i[
25
+ and or if rescue any_match_pattern while until while_post until_post
26
+ ].freeze
27
+
24
28
  def on_normal_if_unless(node)
25
29
  return if node.parent&.if_type?
26
30
  return if part_of_ignored_node?(node)
@@ -77,12 +81,14 @@ module RuboCop
77
81
  def replacement(node)
78
82
  return correct_elsif(node) if node.else_branch&.if_type?
79
83
 
80
- then_code = node.if_branch ? build_expression(node.if_branch) : 'nil'
81
- else_code = node.else_branch ? build_expression(node.else_branch) : 'nil'
84
+ then_code = node.if_branch ? ternary_operand(node.if_branch) : 'nil'
85
+ else_code = node.else_branch ? ternary_operand(node.else_branch) : 'nil'
82
86
 
83
87
  then_code, else_code = else_code, then_code if node.unless?
84
88
 
85
- "#{ternary_condition(node)} ? #{then_code} : #{else_code}"
89
+ ternary = "#{ternary_condition(node)} ? #{then_code} : #{else_code}"
90
+
91
+ operand_of_parent?(node) ? "(#{ternary})" : ternary
86
92
  end
87
93
 
88
94
  def correct_elsif(node)
@@ -108,7 +114,34 @@ module RuboCop
108
114
  # `(a = b) ? c : d`), changing what gets assigned.
109
115
  def ternary_condition(node)
110
116
  condition = node.condition
111
- condition.assignment? ? "(#{condition.source})" : condition.source
117
+ condition.assignment? ? "(#{condition.source})" : ternary_operand(condition)
118
+ end
119
+
120
+ def ternary_operand(expr)
121
+ lower_precedence_than_ternary?(expr) ? "(#{expr.source})" : build_expression(expr)
122
+ end
123
+
124
+ # Expressions that bind looser than `?:` (or cannot appear unparenthesized
125
+ # inside it) would otherwise absorb or break the ternary.
126
+ def lower_precedence_than_ternary?(node)
127
+ return true if node.type?(*LOWER_PRECEDENCE_THAN_TERNARY_TYPES)
128
+ return true if node.send_type? && (node.prefix_not? || prefix_bang_on_command?(node))
129
+
130
+ node.type?(:yield, :super, :defined?) && node.arguments.any? && !node.parenthesized?
131
+ end
132
+
133
+ def operand_of_parent?(node)
134
+ return false unless (parent = node.parent)
135
+ return true if parent.operator_keyword?
136
+
137
+ parent.call_type? && call_operand?(parent, node)
138
+ end
139
+
140
+ def call_operand?(call, node)
141
+ return false if call.prefix_not?
142
+ return true if call.receiver.equal?(node)
143
+
144
+ call.operator_method? && !call.method?(:[]) && !call.method?(:[]=)
112
145
  end
113
146
 
114
147
  def build_else_branch(second_condition)
@@ -131,6 +164,16 @@ module RuboCop
131
164
  result
132
165
  end
133
166
 
167
+ # `!foo a` leaves the arguments of `foo` unparenthesized at the end of the operand,
168
+ # where they would absorb the rest of the ternary.
169
+ def prefix_bang_on_command?(node)
170
+ return false unless node.prefix_bang?
171
+
172
+ receiver = node.receiver
173
+
174
+ require_argument_parentheses?(receiver) || lower_precedence_than_ternary?(receiver)
175
+ end
176
+
134
177
  def require_argument_parentheses?(node)
135
178
  return false if !node.call_type? || node.arithmetic_operation?
136
179