sonicop 26.8.111 → 26.8.113

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 (436) hide show
  1. checksums.yaml +4 -4
  2. data/CONFORMANCE.md +64 -31
  3. data/Cargo.lock +94 -19
  4. data/Cargo.toml +13 -8
  5. data/NOTICE +15 -3
  6. data/README.ja.md +145 -57
  7. data/README.md +140 -48
  8. data/build.rs +155 -0
  9. data/lib/sonicop/runner.rb +7 -1
  10. data/lib/sonicop/version.rb +1 -1
  11. data/src/cli.rs +216 -45
  12. data/src/config/inheritance.rs +637 -64
  13. data/src/config/mod.rs +130 -1
  14. data/src/config/paths.rs +550 -48
  15. data/src/cop_name.rs +18 -1
  16. data/src/diagnostic.rs +168 -3
  17. data/src/directives.rs +398 -77
  18. data/src/display_width.rs +82 -42
  19. data/src/display_width_table.rs +506 -0
  20. data/src/engine.rs +1532 -51
  21. data/src/formatter.rs +10 -1
  22. data/src/lib.rs +2 -0
  23. data/src/main.rs +112 -1
  24. data/src/nul_bytes.rs +245 -47
  25. data/src/parser.rs +37 -0
  26. data/src/profile.rs +174 -24
  27. data/src/ruby_version.rs +167 -21
  28. data/src/rules/bundler/duplicated_group.rs +3 -2
  29. data/src/rules/bundler/gem_comment.rs +5 -4
  30. data/src/rules/gemspec/attribute_assignment.rs +4 -3
  31. data/src/rules/gemspec/deprecated_attribute_assignment.rs +7 -6
  32. data/src/rules/gemspec/duplicated_assignment.rs +4 -3
  33. data/src/rules/gemspec/require_mfa.rs +10 -9
  34. data/src/rules/gemspec/required_ruby_version.rs +3 -2
  35. data/src/rules/gemspec/support.rs +28 -8
  36. data/src/rules/layout/access_modifier_indentation.rs +13 -6
  37. data/src/rules/layout/argument_alignment.rs +19 -7
  38. data/src/rules/layout/array_alignment.rs +1 -2
  39. data/src/rules/layout/block_alignment.rs +74 -30
  40. data/src/rules/layout/block_end_newline.rs +5 -5
  41. data/src/rules/layout/class_structure.rs +13 -4
  42. data/src/rules/layout/closing_heredoc_indentation.rs +24 -5
  43. data/src/rules/layout/closing_parenthesis_indentation.rs +3 -3
  44. data/src/rules/layout/comment_indentation.rs +2 -1
  45. data/src/rules/layout/condition_position.rs +3 -2
  46. data/src/rules/layout/def_end_alignment.rs +10 -6
  47. data/src/rules/layout/dot_position.rs +1 -2
  48. data/src/rules/layout/else_alignment.rs +24 -14
  49. data/src/rules/layout/empty_line_after_guard_clause.rs +8 -7
  50. data/src/rules/layout/empty_line_after_multiline_condition.rs +5 -5
  51. data/src/rules/layout/empty_line_between_defs.rs +8 -6
  52. data/src/rules/layout/empty_lines_around_access_modifier.rs +22 -10
  53. data/src/rules/layout/empty_lines_around_arguments.rs +4 -4
  54. data/src/rules/layout/empty_lines_around_body.rs +8 -2
  55. data/src/rules/layout/empty_lines_around_exception_handling_keywords.rs +1 -2
  56. data/src/rules/layout/empty_lines_around_method_body.rs +3 -2
  57. data/src/rules/layout/end_alignment.rs +32 -7
  58. data/src/rules/layout/end_of_line.rs +14 -1
  59. data/src/rules/layout/extra_spacing.rs +9 -11
  60. data/src/rules/layout/first_array_element_indentation.rs +1 -2
  61. data/src/rules/layout/first_hash_element_indentation.rs +8 -6
  62. data/src/rules/layout/hash_alignment.rs +113 -19
  63. data/src/rules/layout/heredoc_argument_closing_parenthesis.rs +6 -5
  64. data/src/rules/layout/heredoc_indentation.rs +4 -3
  65. data/src/rules/layout/indentation_style.rs +6 -4
  66. data/src/rules/layout/indentation_width.rs +76 -16
  67. data/src/rules/layout/initial_indentation.rs +18 -5
  68. data/src/rules/layout/line_continuation_leading_space.rs +4 -3
  69. data/src/rules/layout/line_continuation_spacing.rs +4 -3
  70. data/src/rules/layout/line_length.rs +417 -43
  71. data/src/rules/layout/mod.rs +1 -1
  72. data/src/rules/layout/multiline_assignment_layout.rs +20 -3
  73. data/src/rules/layout/multiline_block_layout.rs +1 -2
  74. data/src/rules/layout/multiline_expression.rs +60 -31
  75. data/src/rules/layout/multiline_hash_brace_layout.rs +4 -3
  76. data/src/rules/layout/multiline_method_call_indentation.rs +1 -1
  77. data/src/rules/layout/multiline_operation_indentation.rs +3 -3
  78. data/src/rules/layout/redundant_line_break.rs +16 -5
  79. data/src/rules/layout/rescue_ensure_alignment.rs +20 -12
  80. data/src/rules/layout/single_line_block_chain.rs +13 -4
  81. data/src/rules/layout/space_around_block_parameters.rs +16 -2
  82. data/src/rules/layout/space_around_equals_in_parameter_default.rs +1 -2
  83. data/src/rules/layout/space_around_keyword.rs +22 -4
  84. data/src/rules/layout/space_around_operators.rs +34 -2
  85. data/src/rules/layout/space_before_brackets.rs +13 -6
  86. data/src/rules/layout/space_before_comma.rs +3 -2
  87. data/src/rules/layout/space_inside_array_literal_brackets.rs +25 -10
  88. data/src/rules/layout/space_inside_array_percent_literal.rs +7 -1
  89. data/src/rules/layout/space_inside_parens.rs +170 -87
  90. data/src/rules/layout/space_inside_range_literal.rs +53 -19
  91. data/src/rules/layout/space_inside_reference_brackets.rs +65 -3
  92. data/src/rules/layout/space_inside_string_interpolation.rs +147 -48
  93. data/src/rules/layout/support.rs +172 -26
  94. data/src/rules/layout/tokens.rs +47 -7
  95. data/src/rules/layout/trailing_whitespace.rs +5 -2
  96. data/src/rules/lint/access_modifier.rs +50 -5
  97. data/src/rules/lint/ambiguity.rs +97 -7
  98. data/src/rules/lint/ambiguous_block_association.rs +29 -12
  99. data/src/rules/lint/ambiguous_operator.rs +8 -0
  100. data/src/rules/lint/ambiguous_operator_precedence.rs +18 -1
  101. data/src/rules/lint/ambiguous_range.rs +15 -1
  102. data/src/rules/lint/ambiguous_regexp_literal.rs +40 -1
  103. data/src/rules/lint/array_literal_in_regexp.rs +13 -5
  104. data/src/rules/lint/assignment_in_condition.rs +3 -2
  105. data/src/rules/lint/blocks.rs +2 -1
  106. data/src/rules/lint/boolean_symbol.rs +4 -4
  107. data/src/rules/lint/constant_definition_in_block.rs +1 -2
  108. data/src/rules/lint/constant_reassignment.rs +9 -13
  109. data/src/rules/lint/constant_resolution.rs +8 -0
  110. data/src/rules/lint/cop_directives.rs +28 -4
  111. data/src/rules/lint/debugger.rs +59 -9
  112. data/src/rules/lint/deprecated_open_ssl_constant.rs +1 -2
  113. data/src/rules/lint/disjunctive_assignment_in_constructor.rs +2 -2
  114. data/src/rules/lint/duplicate_branch.rs +16 -6
  115. data/src/rules/lint/duplicate_case_condition.rs +5 -5
  116. data/src/rules/lint/duplicate_hash_key.rs +8 -4
  117. data/src/rules/lint/duplicate_match_pattern.rs +7 -5
  118. data/src/rules/lint/duplicate_methods.rs +133 -54
  119. data/src/rules/lint/duplicate_require.rs +1 -2
  120. data/src/rules/lint/duplicate_set_element.rs +19 -5
  121. data/src/rules/lint/empty_conditional_body.rs +19 -3
  122. data/src/rules/lint/empty_expression.rs +16 -1
  123. data/src/rules/lint/empty_in_pattern.rs +3 -2
  124. data/src/rules/lint/empty_when.rs +4 -4
  125. data/src/rules/lint/ensure_return.rs +3 -2
  126. data/src/rules/lint/float_comparison.rs +8 -6
  127. data/src/rules/lint/flow.rs +8 -5
  128. data/src/rules/lint/format_parameter_mismatch.rs +4 -3
  129. data/src/rules/lint/hash_compare_by_identity.rs +5 -4
  130. data/src/rules/lint/heredoc_method_call_position.rs +3 -2
  131. data/src/rules/lint/implicit_string_concatenation.rs +33 -3
  132. data/src/rules/lint/ineffective_access_modifier.rs +5 -4
  133. data/src/rules/lint/inherit_exception.rs +4 -3
  134. data/src/rules/lint/literal_as_condition.rs +51 -31
  135. data/src/rules/lint/literal_assignment_in_condition.rs +4 -3
  136. data/src/rules/lint/literal_in_interpolation.rs +66 -5
  137. data/src/rules/lint/literals.rs +10 -8
  138. data/src/rules/lint/missing_cop_enable_directive.rs +95 -18
  139. data/src/rules/lint/mod.rs +1 -0
  140. data/src/rules/lint/nested_method_definition.rs +36 -11
  141. data/src/rules/lint/nil_receiver.rs +420 -0
  142. data/src/rules/lint/no_return_in_begin_end_blocks.rs +2 -2
  143. data/src/rules/lint/node_equality.rs +8 -5
  144. data/src/rules/lint/non_atomic_file_operation.rs +9 -3
  145. data/src/rules/lint/non_deterministic_require_order.rs +5 -3
  146. data/src/rules/lint/non_local_exit_from_iterator.rs +22 -9
  147. data/src/rules/lint/number_conversion.rs +1 -1
  148. data/src/rules/lint/out_of_range_regexp_ref.rs +6 -4
  149. data/src/rules/lint/parentheses_as_grouped_expression.rs +5 -0
  150. data/src/rules/lint/redundant_cop_enable_directive.rs +3 -1
  151. data/src/rules/lint/redundant_dir_glob_sort.rs +1 -2
  152. data/src/rules/lint/redundant_safe_navigation.rs +47 -22
  153. data/src/rules/lint/redundant_splat_expansion.rs +4 -2
  154. data/src/rules/lint/redundant_string_coercion.rs +3 -2
  155. data/src/rules/lint/redundant_type_conversion.rs +16 -4
  156. data/src/rules/lint/regexp.rs +1 -3
  157. data/src/rules/lint/regexp_source.rs +2 -2
  158. data/src/rules/lint/regexp_tree.rs +38 -5
  159. data/src/rules/lint/require_range_parentheses.rs +5 -5
  160. data/src/rules/lint/rescue_exception.rs +4 -3
  161. data/src/rules/lint/rescue_type.rs +3 -2
  162. data/src/rules/lint/return_in_void_context.rs +8 -3
  163. data/src/rules/lint/safe_navigation_chain.rs +47 -11
  164. data/src/rules/lint/self_assignment.rs +20 -2
  165. data/src/rules/lint/shadowed_argument.rs +2 -2
  166. data/src/rules/lint/shadowed_exception.rs +2 -2
  167. data/src/rules/lint/suppressed_exception.rs +21 -3
  168. data/src/rules/lint/suppressed_exception_in_number_conversion.rs +4 -3
  169. data/src/rules/lint/symbol_conversion.rs +56 -8
  170. data/src/rules/lint/syntax.rs +195 -5
  171. data/src/rules/lint/top_level_return_with_argument.rs +11 -1
  172. data/src/rules/lint/triple_quotes.rs +3 -2
  173. data/src/rules/lint/unescaped_bracket_in_regexp.rs +1 -1
  174. data/src/rules/lint/unexpected_block_arity.rs +23 -5
  175. data/src/rules/lint/unified_integer.rs +18 -12
  176. data/src/rules/lint/unmodified_reduce_accumulator.rs +36 -7
  177. data/src/rules/lint/unreachable_code.rs +4 -4
  178. data/src/rules/lint/unreachable_loop.rs +26 -10
  179. data/src/rules/lint/unreachable_pattern_branch.rs +7 -5
  180. data/src/rules/lint/unused_block_argument.rs +2 -2
  181. data/src/rules/lint/unused_method_argument.rs +1 -2
  182. data/src/rules/lint/useless_assignment.rs +3 -2
  183. data/src/rules/lint/useless_constant_scoping.rs +3 -2
  184. data/src/rules/lint/useless_else_without_rescue.rs +4 -3
  185. data/src/rules/lint/useless_rescue.rs +5 -4
  186. data/src/rules/lint/useless_ruby2_keywords.rs +2 -1
  187. data/src/rules/lint/useless_setter_call.rs +5 -4
  188. data/src/rules/lint/useless_times.rs +4 -3
  189. data/src/rules/lint/variable_force.rs +307 -79
  190. data/src/rules/lint/void.rs +32 -20
  191. data/src/rules/metrics/abc_size.rs +197 -7
  192. data/src/rules/metrics/block_length.rs +19 -10
  193. data/src/rules/metrics/block_nesting.rs +1 -2
  194. data/src/rules/metrics/class_length.rs +16 -4
  195. data/src/rules/metrics/collection_literal_length.rs +2 -1
  196. data/src/rules/metrics/complexity.rs +52 -14
  197. data/src/rules/metrics/fragments.rs +10 -14
  198. data/src/rules/metrics/locals.rs +35 -14
  199. data/src/rules/metrics/method_length.rs +14 -6
  200. data/src/rules/metrics/parameter_lists.rs +7 -6
  201. data/src/rules/metrics/perceived_complexity.rs +1 -3
  202. data/src/rules/metrics/support.rs +94 -4
  203. data/src/rules/mod.rs +525 -80
  204. data/src/rules/naming/ascii_identifiers.rs +3 -3
  205. data/src/rules/naming/binary_operator_parameter_name.rs +5 -4
  206. data/src/rules/naming/block_forwarding.rs +9 -8
  207. data/src/rules/naming/constant_name.rs +56 -18
  208. data/src/rules/naming/file_name.rs +174 -43
  209. data/src/rules/naming/method_name.rs +8 -1
  210. data/src/rules/naming/predicate_method.rs +42 -14
  211. data/src/rules/naming/predicate_prefix.rs +70 -4
  212. data/src/rules/naming/rescued_exceptions_variable_name.rs +14 -11
  213. data/src/rules/naming/support.rs +54 -295
  214. data/src/rules/naming/variable_name.rs +17 -4
  215. data/src/rules/naming/variable_number.rs +9 -10
  216. data/src/rules/node_ext.rs +1 -1
  217. data/src/rules/ruby_literal.rs +8 -3
  218. data/src/rules/security/compound_hash.rs +3 -2
  219. data/src/rules/security/open.rs +5 -4
  220. data/src/rules/send_node.rs +134 -0
  221. data/src/rules/single_line.rs +4 -4
  222. data/src/rules/style/access_modifier_declarations.rs +76 -22
  223. data/src/rules/style/accessor_grouping.rs +44 -6
  224. data/src/rules/style/alias.rs +4 -3
  225. data/src/rules/style/and_or.rs +2 -2
  226. data/src/rules/style/arguments_forwarding.rs +5 -5
  227. data/src/rules/style/array_coercion.rs +5 -5
  228. data/src/rules/style/array_first_last.rs +19 -4
  229. data/src/rules/style/array_intersect.rs +26 -8
  230. data/src/rules/style/array_intersect_with_single_element.rs +1 -1
  231. data/src/rules/style/ascii_comments.rs +2 -0
  232. data/src/rules/style/attr.rs +1 -1
  233. data/src/rules/style/auto_resource_cleanup.rs +1 -1
  234. data/src/rules/style/bitwise_predicate.rs +2 -2
  235. data/src/rules/style/block_args.rs +1 -1
  236. data/src/rules/style/block_comments.rs +11 -2
  237. data/src/rules/style/block_delimiters.rs +154 -10
  238. data/src/rules/style/case_equality.rs +7 -2
  239. data/src/rules/style/case_like_if.rs +26 -2
  240. data/src/rules/style/class_and_module_children.rs +162 -10
  241. data/src/rules/style/class_methods.rs +1 -1
  242. data/src/rules/style/class_methods_definitions.rs +9 -0
  243. data/src/rules/style/collection_compact.rs +4 -4
  244. data/src/rules/style/combinable_defined.rs +2 -2
  245. data/src/rules/style/combinable_loops.rs +9 -11
  246. data/src/rules/style/comments.rs +6 -3
  247. data/src/rules/style/comparable_clamp.rs +2 -2
  248. data/src/rules/style/concat_array_literals.rs +3 -3
  249. data/src/rules/style/conditional.rs +12 -1
  250. data/src/rules/style/conditional_assignment.rs +133 -4
  251. data/src/rules/style/constant_visibility.rs +3 -3
  252. data/src/rules/style/copyright.rs +64 -3
  253. data/src/rules/style/data_inheritance.rs +1 -1
  254. data/src/rules/style/def_with_parentheses.rs +1 -1
  255. data/src/rules/style/document_dynamic_eval_definition.rs +50 -12
  256. data/src/rules/style/documentation.rs +1 -1
  257. data/src/rules/style/each_for_simple_loop.rs +1 -1
  258. data/src/rules/style/each_with_object.rs +4 -4
  259. data/src/rules/style/empty_block_parameter.rs +1 -1
  260. data/src/rules/style/empty_case_condition.rs +6 -6
  261. data/src/rules/style/empty_class_definition.rs +1 -1
  262. data/src/rules/style/empty_else.rs +1 -1
  263. data/src/rules/style/empty_heredoc.rs +14 -1
  264. data/src/rules/style/empty_lambda_parameter.rs +1 -1
  265. data/src/rules/style/empty_literal.rs +34 -2
  266. data/src/rules/style/empty_method.rs +1 -1
  267. data/src/rules/style/empty_string_inside_interpolation.rs +1 -1
  268. data/src/rules/style/endless.rs +11 -3
  269. data/src/rules/style/env_home.rs +1 -1
  270. data/src/rules/style/eval_with_location.rs +6 -0
  271. data/src/rules/style/even_odd.rs +1 -1
  272. data/src/rules/style/exact_regexp_match.rs +1 -1
  273. data/src/rules/style/expand_path_arguments.rs +3 -3
  274. data/src/rules/style/explicit_block_argument.rs +3 -3
  275. data/src/rules/style/file_open.rs +5 -0
  276. data/src/rules/style/file_open_call.rs +3 -3
  277. data/src/rules/style/file_write.rs +1 -1
  278. data/src/rules/style/for.rs +1 -1
  279. data/src/rules/style/format_sequences.rs +6 -0
  280. data/src/rules/style/format_string.rs +75 -11
  281. data/src/rules/style/format_string_token.rs +61 -12
  282. data/src/rules/style/frozen_string_literal_comment.rs +62 -4
  283. data/src/rules/style/guard_clause.rs +8 -7
  284. data/src/rules/style/hash_as_last_array_item.rs +4 -4
  285. data/src/rules/style/hash_each_methods.rs +1 -1
  286. data/src/rules/style/hash_like_case.rs +8 -8
  287. data/src/rules/style/hash_lookup_method.rs +43 -3
  288. data/src/rules/style/hash_subset.rs +16 -10
  289. data/src/rules/style/hash_syntax.rs +609 -71
  290. data/src/rules/style/hash_transform.rs +20 -18
  291. data/src/rules/style/hash_transform_keys.rs +7 -0
  292. data/src/rules/style/hash_transform_values.rs +7 -0
  293. data/src/rules/style/identical_conditional_branches.rs +54 -5
  294. data/src/rules/style/if_inside_else.rs +19 -3
  295. data/src/rules/style/if_unless_modifier.rs +13 -14
  296. data/src/rules/style/if_with_boolean_literal_branches.rs +1 -1
  297. data/src/rules/style/if_with_semicolon.rs +7 -1
  298. data/src/rules/style/implicit_runtime_error.rs +7 -0
  299. data/src/rules/style/in_pattern_then.rs +2 -2
  300. data/src/rules/style/infinite_loop.rs +6 -0
  301. data/src/rules/style/inline_comment.rs +4 -1
  302. data/src/rules/style/inverse_methods.rs +12 -2
  303. data/src/rules/style/invertible_unless_condition.rs +12 -12
  304. data/src/rules/style/it_block_parameter.rs +5 -2
  305. data/src/rules/style/keyword_arguments_merging.rs +8 -2
  306. data/src/rules/style/keyword_parameters_order.rs +1 -1
  307. data/src/rules/style/line_end_concatenation.rs +1 -1
  308. data/src/rules/style/magic_comment_format.rs +14 -6
  309. data/src/rules/style/map_chain.rs +1 -1
  310. data/src/rules/style/map_compact_with_conditional_block.rs +3 -3
  311. data/src/rules/style/map_into_array.rs +7 -7
  312. data/src/rules/style/map_join.rs +3 -3
  313. data/src/rules/style/method_call_with_args_parentheses.rs +7 -7
  314. data/src/rules/style/method_call_without_args_parentheses.rs +2 -2
  315. data/src/rules/style/method_def_parentheses.rs +2 -3
  316. data/src/rules/style/min_max.rs +3 -3
  317. data/src/rules/style/min_max_comparison.rs +2 -2
  318. data/src/rules/style/missing_else.rs +1 -1
  319. data/src/rules/style/missing_respond_to_missing.rs +1 -1
  320. data/src/rules/style/mixin_grouping.rs +6 -7
  321. data/src/rules/style/multiline_block_chain.rs +6 -8
  322. data/src/rules/style/multiline_if_then.rs +1 -1
  323. data/src/rules/style/multiline_in_pattern_then.rs +16 -3
  324. data/src/rules/style/multiline_method_signature.rs +1 -1
  325. data/src/rules/style/multiline_when_then.rs +2 -2
  326. data/src/rules/style/mutable_constant.rs +31 -35
  327. data/src/rules/style/negated_if.rs +2 -2
  328. data/src/rules/style/negated_if_else_condition.rs +5 -3
  329. data/src/rules/style/negated_while.rs +1 -1
  330. data/src/rules/style/negative_array_index.rs +25 -3
  331. data/src/rules/style/nested_modifier.rs +1 -1
  332. data/src/rules/style/nested_parenthesized_calls.rs +2 -2
  333. data/src/rules/style/nested_ternary_operator.rs +2 -2
  334. data/src/rules/style/next.rs +12 -12
  335. data/src/rules/style/nil_lambda.rs +2 -2
  336. data/src/rules/style/nodes.rs +21 -0
  337. data/src/rules/style/non_nil_check.rs +82 -2
  338. data/src/rules/style/numbered_parameters.rs +4 -1
  339. data/src/rules/style/numbered_parameters_limit.rs +2 -2
  340. data/src/rules/style/numeric_literals.rs +17 -1
  341. data/src/rules/style/numeric_predicate.rs +25 -6
  342. data/src/rules/style/one_line_conditional.rs +24 -7
  343. data/src/rules/style/operator_method_call.rs +5 -0
  344. data/src/rules/style/optional_arguments.rs +1 -1
  345. data/src/rules/style/or_assignment.rs +1 -1
  346. data/src/rules/style/parallel_assignment.rs +20 -9
  347. data/src/rules/style/parens.rs +1 -1
  348. data/src/rules/style/parentheses_around_condition.rs +3 -3
  349. data/src/rules/style/partition_instead_of_double_select.rs +3 -3
  350. data/src/rules/style/percent.rs +18 -9
  351. data/src/rules/style/percent_q_literals.rs +40 -7
  352. data/src/rules/style/preferred_hash_methods.rs +1 -1
  353. data/src/rules/style/quoted_symbols.rs +6 -0
  354. data/src/rules/style/raise_args.rs +26 -20
  355. data/src/rules/style/random_with_offset.rs +1 -1
  356. data/src/rules/style/reduce_to_hash.rs +6 -6
  357. data/src/rules/style/redundant_argument.rs +1 -1
  358. data/src/rules/style/redundant_assignment.rs +5 -5
  359. data/src/rules/style/redundant_begin.rs +5 -3
  360. data/src/rules/style/redundant_capital_w.rs +3 -2
  361. data/src/rules/style/redundant_condition.rs +17 -8
  362. data/src/rules/style/redundant_current_directory_in_path.rs +2 -2
  363. data/src/rules/style/redundant_double_splat_hash_braces.rs +15 -5
  364. data/src/rules/style/redundant_exception.rs +3 -1
  365. data/src/rules/style/redundant_file_extension_in_require.rs +2 -2
  366. data/src/rules/style/redundant_format.rs +98 -20
  367. data/src/rules/style/redundant_freeze.rs +1 -1
  368. data/src/rules/style/redundant_heredoc_delimiter_quotes.rs +1 -1
  369. data/src/rules/style/redundant_initialize.rs +6 -3
  370. data/src/rules/style/redundant_interpolation.rs +8 -8
  371. data/src/rules/style/redundant_interpolation_unfreeze.rs +10 -3
  372. data/src/rules/style/redundant_line_continuation.rs +10 -7
  373. data/src/rules/style/redundant_parentheses.rs +96 -19
  374. data/src/rules/style/redundant_regexp_argument.rs +1 -1
  375. data/src/rules/style/redundant_regexp_character_class.rs +4 -3
  376. data/src/rules/style/redundant_regexp_escape.rs +3 -2
  377. data/src/rules/style/redundant_return.rs +1 -2
  378. data/src/rules/style/redundant_self.rs +28 -16
  379. data/src/rules/style/redundant_sort.rs +1 -1
  380. data/src/rules/style/redundant_sort_by.rs +1 -1
  381. data/src/rules/style/redundant_string_escape.rs +6 -5
  382. data/src/rules/style/regexp_literal.rs +3 -2
  383. data/src/rules/style/require_order.rs +2 -2
  384. data/src/rules/style/rescue_modifier.rs +9 -4
  385. data/src/rules/style/rescue_standard_error.rs +1 -1
  386. data/src/rules/style/return_nil.rs +2 -2
  387. data/src/rules/style/return_nil_in_predicate_method_definition.rs +2 -2
  388. data/src/rules/style/safe_navigation.rs +2 -2
  389. data/src/rules/style/safe_navigation_chain_length.rs +8 -1
  390. data/src/rules/style/sample.rs +3 -3
  391. data/src/rules/style/select_by.rs +6 -2
  392. data/src/rules/style/select_by_range.rs +1 -1
  393. data/src/rules/style/semicolon.rs +77 -4
  394. data/src/rules/style/signal_exception.rs +60 -3
  395. data/src/rules/style/single_argument_dig.rs +10 -2
  396. data/src/rules/style/single_line_block_params.rs +14 -4
  397. data/src/rules/style/single_line_do_end_block.rs +13 -12
  398. data/src/rules/style/single_line_methods.rs +2 -2
  399. data/src/rules/style/slicing_with_range.rs +2 -2
  400. data/src/rules/style/sole_nested_conditional.rs +4 -4
  401. data/src/rules/style/special_global_vars.rs +2 -2
  402. data/src/rules/style/stabby_lambda_parentheses.rs +1 -1
  403. data/src/rules/style/statement_modifier.rs +3 -2
  404. data/src/rules/style/static_class.rs +7 -1
  405. data/src/rules/style/string_concatenation.rs +12 -7
  406. data/src/rules/style/string_hash_keys.rs +3 -2
  407. data/src/rules/style/string_literals.rs +3 -3
  408. data/src/rules/style/string_literals_in_interpolation.rs +4 -1
  409. data/src/rules/style/struct_inheritance.rs +3 -3
  410. data/src/rules/style/super_arguments.rs +15 -7
  411. data/src/rules/style/super_with_args_parentheses.rs +49 -12
  412. data/src/rules/style/swap_values.rs +1 -1
  413. data/src/rules/style/symbol_array.rs +9 -2
  414. data/src/rules/style/symbol_proc.rs +3 -3
  415. data/src/rules/style/tally_method.rs +4 -4
  416. data/src/rules/style/ternary_parentheses.rs +30 -15
  417. data/src/rules/style/top_level_method_definition.rs +10 -5
  418. data/src/rules/style/trailing_body.rs +1 -1
  419. data/src/rules/style/trailing_comma.rs +206 -10
  420. data/src/rules/style/trailing_comma_in_arguments.rs +9 -10
  421. data/src/rules/style/trailing_comma_in_array_literal.rs +2 -1
  422. data/src/rules/style/trailing_comma_in_hash_literal.rs +2 -1
  423. data/src/rules/style/trailing_method_end_statement.rs +6 -1
  424. data/src/rules/style/trailing_underscore_variable.rs +4 -5
  425. data/src/rules/style/trivial_accessors.rs +2 -2
  426. data/src/rules/style/unless_logical_operators.rs +1 -1
  427. data/src/rules/style/unpack_first.rs +11 -2
  428. data/src/rules/style/when_then.rs +4 -4
  429. data/src/rules/style/while_until_do.rs +1 -1
  430. data/src/rules/style/while_until_modifier.rs +3 -3
  431. data/src/rules/style/word_array.rs +9 -7
  432. data/src/rules/style/yoda_condition.rs +2 -2
  433. data/src/rules/style/zero_length_predicate.rs +5 -0
  434. data/src/rules/support.rs +128 -11
  435. data/src/source.rs +18 -15
  436. metadata +5 -1
@@ -18,66 +18,610 @@ static PLAIN_SYMBOL: LazyLock<Regex> =
18
18
  /// The quoted form only became a legal hash key in Ruby 2.2.
19
19
  const QUOTED_KEY_SINCE: RubyVersion = RubyVersion::new(2, 2);
20
20
 
21
+ /// `EnforcedShorthandSyntax`: what to do about Ruby 3.1's `{ foo: }`, which is a separate axis
22
+ /// from `EnforcedStyle` and reaches the same pairs.
23
+ #[derive(Clone, Copy, Eq, PartialEq)]
24
+ enum Shorthand {
25
+ Either,
26
+ Always,
27
+ Never,
28
+ Consistent,
29
+ EitherConsistent,
30
+ }
31
+
32
+ /// Value omission is Ruby 3.1 syntax. Below that every pair is left alone, whatever the setting
33
+ /// says, because the shorter form would not parse.
34
+ const OMISSION_SINCE: RubyVersion = RubyVersion::new(3, 1);
35
+
36
+ const MSG_19: &str = "Use the new Ruby 1.9 hash syntax.";
37
+ const MSG_HASH_ROCKETS: &str = "Use hash rockets syntax.";
38
+ const MSG_NO_MIXED_KEYS: &str = "Don't mix styles in the same hash.";
39
+ const OMIT_MSG: &str = "Omit the hash value.";
40
+ const EXPLICIT_MSG: &str = "Include the hash value.";
41
+ const MIX_PREFIX: &str = "Do not mix explicit and implicit hash values.";
42
+
21
43
  pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
44
+ check_shorthand(context, offenses);
45
+ check_syntax(context, offenses);
46
+ }
47
+
48
+ fn check_syntax(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
22
49
  let style: String = context
23
50
  .setting("EnforcedStyle")
24
51
  .unwrap_or_else(|| "ruby19".to_owned());
25
- if style != "ruby19" && style != "ruby19_no_mixed_keys" {
26
- return;
27
- }
28
52
  let quoted_keys_allowed = context.target_ruby_version() >= QUOTED_KEY_SINCE;
53
+ let prefer_rockets_for_non_alnum = context
54
+ .setting::<bool>("PreferHashRocketsForNonAlnumEndingSymbols")
55
+ .unwrap_or(false);
29
56
 
30
- for node in context.nodes_of("pair") {
31
- let Some(operator) = hash_rocket(node, context) else {
32
- continue;
57
+ for pairs in hash_groups(context) {
58
+ // `sym_indices?`: a hash is left in the old syntax unless every one of its keys can take
59
+ // the new one, so that one rocket that has to stay does not leave the hash written in two
60
+ // styles at once.
61
+ let sym_indices = pairs.iter().all(|pair| {
62
+ word_symbol_pair(
63
+ *pair,
64
+ context,
65
+ quoted_keys_allowed,
66
+ prefer_rockets_for_non_alnum,
67
+ )
68
+ });
69
+ // `force_hash_rockets?`: a hash holding a symbol value is written in the old syntax
70
+ // whatever the style says, so that `{ a: :b }` does not read as two different things.
71
+ let force_hash_rockets = context
72
+ .setting::<bool>("UseHashRocketsWithSymbolValues")
73
+ .unwrap_or(false)
74
+ && pairs.iter().any(|pair| {
75
+ pair.field("value").is_some_and(|value| {
76
+ matches!(value.kind_str(), "simple_symbol" | "delimited_symbol")
77
+ })
78
+ });
79
+ // Which delimiter the style rejects, and what it says about it. `check(pairs, delim, msg)`
80
+ // upstream.
81
+ let (reject_colon, message) = match style.as_str() {
82
+ _ if force_hash_rockets => (true, MSG_HASH_ROCKETS),
83
+ "hash_rockets" => (true, MSG_HASH_ROCKETS),
84
+ "ruby19_no_mixed_keys" if sym_indices => (false, MSG_19),
85
+ "ruby19_no_mixed_keys" => (true, MSG_NO_MIXED_KEYS),
86
+ "no_mixed_keys" if sym_indices => {
87
+ // `pairs.first.inverse_delimiter`: whichever way the hash opens, the others have
88
+ // to follow, so what is rejected is the opposite of the first pair's delimiter.
89
+ let first_colon = pair_delimiter(context, pairs[0]).is_some_and(|(_, colon)| colon);
90
+ (!first_colon, MSG_NO_MIXED_KEYS)
91
+ }
92
+ "no_mixed_keys" => (true, MSG_NO_MIXED_KEYS),
93
+ _ if !sym_indices => continue,
94
+ _ => (false, MSG_19),
33
95
  };
34
- // A hash is left alone unless every one of its keys can take the new syntax, so that one
35
- // rocket that has to stay does not leave the hash written in two styles at once.
36
- if !every_key_takes_the_new_syntax(node, context, quoted_keys_allowed) {
37
- continue;
96
+
97
+ for &node in &pairs {
98
+ let Some((operator, is_colon)) = pair_delimiter(context, node) else {
99
+ continue;
100
+ };
101
+ if is_colon != reject_colon {
102
+ continue;
103
+ }
104
+ if is_colon {
105
+ offenses.push(
106
+ context
107
+ .offense(message, node.start_byte()..operator.end)
108
+ .corrected_by_all(rocket_edits(context, node, &operator)),
109
+ );
110
+ continue;
111
+ }
112
+
113
+ let start = node.start_byte();
114
+ // The opening brace is written **into** the rewrite rather than inserted beside it. An
115
+ // insertion at the byte the rewrite starts at is a second edit at the same position,
116
+ // and `apply_edits` refuses a pair like that -- silently, so the cop reads as having
117
+ // declined to correct at all.
118
+ let wrapping = returned_bare_hash(node, context);
119
+ let opening = if wrapping.is_some() { "{" } else { "" };
120
+ // `argument_without_space?`: `foo:bar => 1` has no space between the selector and the
121
+ // hash, and the old syntax did not need one. `foo` + `bar: 1` runs the two together
122
+ // into `foobar: 1`, which is a different program (and here not a program at all).
123
+ let spacing = if argument_without_space(node, context) {
124
+ " "
125
+ } else {
126
+ ""
127
+ };
128
+ let mut edits = vec![Edit {
129
+ start,
130
+ end: whitespace_end(context, operator.end),
131
+ replacement: format!("{spacing}{opening}{}: ", key_name(node, context)),
132
+ safe: true,
133
+ }];
134
+ // `corrector.wrap(hash_node, '{', '}')`: `return key: value` is not valid Ruby, so a
135
+ // bare hash handed to `return` has to gain braces as it changes syntax. Upstream does
136
+ // this once per hash, on its first pair. Without it the correction turns working code
137
+ // into a syntax error -- and the reparse guard cannot see it, because each pair is a
138
+ // separate offense.
139
+ if let Some((_, close)) = wrapping {
140
+ edits.push(Edit {
141
+ start: close,
142
+ end: close,
143
+ replacement: "}".to_owned(),
144
+ safe: true,
145
+ });
146
+ }
147
+ offenses.push(
148
+ context
149
+ .offense(message, start..operator.end)
150
+ .corrected_by_all(edits),
151
+ );
38
152
  }
153
+ }
154
+ }
39
155
 
40
- let start = node.start_byte();
41
- // The opening brace is written **into** the rewrite rather than inserted beside it. An
42
- // insertion at the byte the rewrite starts at is a second edit at the same position, and
43
- // `apply_edits` refuses a pair like that -- silently, so the cop reads as having declined
44
- // to correct at all.
45
- let wrapping = returned_bare_hash(node, context);
46
- let opening = if wrapping.is_some() { "{" } else { "" };
47
- // `argument_without_space?`: `foo:bar => 1` has no space between the selector and the hash,
48
- // and the old syntax did not need one. `foo` + `bar: 1` runs the two together into
49
- // `foobar: 1`, which is a different program (and here not a program at all).
50
- let spacing = if argument_without_space(node, context) {
51
- " "
52
- } else {
53
- ""
54
- };
55
- let mut edits = vec![Edit {
56
- start,
156
+ /// `autocorrect_hash_rockets`: the key gains the `:` the symbol literal needs and the rocket that
157
+ /// replaces the colon, and the colon itself goes with the space around it.
158
+ fn rocket_edits(
159
+ context: &RuleContext<'_>,
160
+ pair: Node<'_>,
161
+ operator: &std::ops::Range<usize>,
162
+ ) -> Vec<Edit> {
163
+ let Some(key) = pair.field("key") else {
164
+ return Vec::new();
165
+ };
166
+ let key_source = context.source.node_text(key);
167
+ let mut replacement = format!(":{key_source} => ");
168
+ // `key_with_hash_rocket += pair_node.key.source if pair_node.value_omission?`: the old syntax
169
+ // has no short form, so the value the colon left out has to be written back.
170
+ if pair.field("value").is_none() {
171
+ replacement.push_str(key_source);
172
+ }
173
+ vec![
174
+ Edit {
175
+ start: key.start_byte(),
176
+ end: key.end_byte(),
177
+ replacement,
178
+ safe: true,
179
+ },
180
+ Edit {
181
+ start: operator.start,
57
182
  end: whitespace_end(context, operator.end),
58
- replacement: format!("{spacing}{opening}{}: ", key_name(node, context)),
183
+ replacement: String::new(),
59
184
  safe: true,
60
- }];
61
- // `corrector.wrap(hash_node, '{', '}')`: `return key: value` is not valid Ruby, so a bare
62
- // hash handed to `return` has to gain braces as it changes syntax. Upstream does this once
63
- // per hash, on its first pair. Without it the correction turns working code into a syntax
64
- // error -- and the reparse guard cannot see it, because each pair is a separate offense.
65
- if let Some((_, close)) = wrapping {
66
- edits.push(Edit {
67
- start: close,
68
- end: close,
69
- replacement: "}".to_owned(),
70
- safe: true,
71
- });
185
+ },
186
+ ]
187
+ }
188
+
189
+ /// The pair's delimiter: its span, and whether it is the colon of the new syntax.
190
+ fn pair_delimiter(
191
+ context: &RuleContext<'_>,
192
+ pair: Node<'_>,
193
+ ) -> Option<(std::ops::Range<usize>, bool)> {
194
+ let key = pair.field("key")?;
195
+ let after = key.end_byte();
196
+ let text = context.source.text();
197
+ let end = pair
198
+ .field("value")
199
+ .map_or(pair.end_byte(), |v| v.start_byte());
200
+ let between = text.get(after..end)?;
201
+ if let Some(offset) = between.find("=>") {
202
+ return Some((after + offset..after + offset + "=>".len(), false));
203
+ }
204
+ let offset = between.find(':')?;
205
+ Some((after + offset..after + offset + 1, true))
206
+ }
207
+
208
+ /// `HashShorthandSyntax`: the value-omission axis.
209
+ ///
210
+ /// `always` and `never` read one pair at a time -- omit every value that can go, or write every
211
+ /// one back. `consistent` and `either_consistent` read the hash instead, because what they forbid
212
+ /// is the mixture: a hash whose pairs are not all written the same way is reported on the pairs
213
+ /// that break the majority, and which side that is depends on whether some value in the hash
214
+ /// *cannot* be omitted.
215
+ fn check_shorthand(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
216
+ let shorthand = match context
217
+ .setting::<String>("EnforcedShorthandSyntax")
218
+ .as_deref()
219
+ {
220
+ Some("always") => Shorthand::Always,
221
+ Some("never") => Shorthand::Never,
222
+ Some("consistent") => Shorthand::Consistent,
223
+ Some("either_consistent") => Shorthand::EitherConsistent,
224
+ _ => Shorthand::Either,
225
+ };
226
+ if shorthand == Shorthand::Either || context.target_ruby_version() < OMISSION_SINCE {
227
+ return;
228
+ }
229
+ match shorthand {
230
+ Shorthand::Always | Shorthand::Never => {
231
+ for pair in context.nodes_of("pair") {
232
+ if !in_hash_literal(pair, context) {
233
+ continue;
234
+ }
235
+ on_pair(context, pair, shorthand, offenses);
236
+ }
72
237
  }
73
- offenses.push(
74
- context
75
- .offense("Use the new Ruby 1.9 hash syntax.", start..operator.end)
76
- .corrected_by_all(edits),
238
+ _ => {
239
+ for pairs in hash_groups(context) {
240
+ mixed_shorthand(context, &pairs, shorthand, offenses);
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ /// `on_pair` under `always` and `never`.
247
+ fn on_pair(
248
+ context: &RuleContext<'_>,
249
+ pair: Node<'_>,
250
+ shorthand: Shorthand,
251
+ offenses: &mut Vec<Offense>,
252
+ ) {
253
+ let Some(key) = pair.field("key") else {
254
+ return;
255
+ };
256
+ let key_source = context.source.node_text(key);
257
+ if shorthand == Shorthand::Always {
258
+ if omits_value(pair) || require_hash_value(context, pair) {
259
+ return;
260
+ }
261
+ register_shorthand(context, pair, OMIT_MSG, format!("{key_source}:"), offenses);
262
+ } else {
263
+ if !omits_value(pair) {
264
+ return;
265
+ }
266
+ register_shorthand(
267
+ context,
268
+ pair,
269
+ EXPLICIT_MSG,
270
+ format!("{key_source}: {key_source}"),
271
+ offenses,
77
272
  );
78
273
  }
79
274
  }
80
275
 
276
+ /// `on_hash_for_mixed_shorthand` and the two checks it dispatches to.
277
+ fn mixed_shorthand(
278
+ context: &RuleContext<'_>,
279
+ pairs: &[Node<'_>],
280
+ shorthand: Shorthand,
281
+ offenses: &mut Vec<Offense>,
282
+ ) {
283
+ // `breakdown_value_types_of_hash`.
284
+ let (mut omitted, mut needed, mut omittable) = (Vec::new(), Vec::new(), Vec::new());
285
+ for &pair in pairs {
286
+ if omits_value(pair) {
287
+ omitted.push(pair);
288
+ } else if require_hash_value(context, pair) {
289
+ needed.push(pair);
290
+ } else {
291
+ omittable.push(pair);
292
+ }
293
+ }
294
+ let kinds = usize::from(!omitted.is_empty())
295
+ + usize::from(!needed.is_empty())
296
+ + usize::from(!omittable.is_empty());
297
+
298
+ if kinds > 1 {
299
+ // `mixed_shorthand_syntax_check`: a hash holding a value that cannot go is written out in
300
+ // full, and one where every value could go is written short.
301
+ let (targets, message) = if needed.is_empty() {
302
+ (&omittable, format!("{MIX_PREFIX} {OMIT_MSG}"))
303
+ } else {
304
+ (&omitted, format!("{MIX_PREFIX} {EXPLICIT_MSG}"))
305
+ };
306
+ let omit = needed.is_empty();
307
+ for &pair in targets {
308
+ let Some(key) = pair.field("key") else {
309
+ continue;
310
+ };
311
+ let key_source = context.source.node_text(key);
312
+ let replacement = if omit {
313
+ format!("{key_source}:")
314
+ } else {
315
+ format!("{key_source}: {key_source}")
316
+ };
317
+ register_shorthand(context, pair, &message, replacement, offenses);
318
+ }
319
+ return;
320
+ }
321
+
322
+ // `no_mixed_shorthand_syntax_check`.
323
+ if !needed.is_empty() {
324
+ return;
325
+ }
326
+ // `ignore_explicit_omissible_hash_shorthand_syntax?`: under `either_consistent` a hash whose
327
+ // values could all be omitted is consistent as it stands, so writing them out is accepted.
328
+ if shorthand == Shorthand::EitherConsistent && omitted.is_empty() {
329
+ return;
330
+ }
331
+ for &pair in &omittable {
332
+ let Some(key) = pair.field("key") else {
333
+ continue;
334
+ };
335
+ let key_source = context.source.node_text(key);
336
+ register_shorthand(context, pair, OMIT_MSG, format!("{key_source}:"), offenses);
337
+ }
338
+ }
339
+
340
+ /// `register_offense`: the offense sits on the value -- and on a pair that has none, on the key
341
+ /// the parser puts the implicit value's range over.
342
+ fn register_shorthand(
343
+ context: &RuleContext<'_>,
344
+ pair: Node<'_>,
345
+ message: impl Into<String>,
346
+ replacement: String,
347
+ offenses: &mut Vec<Offense>,
348
+ ) {
349
+ let range = match pair.field("value") {
350
+ Some(value) => value.byte_range(),
351
+ None => match pair.field("key") {
352
+ Some(key) => key.byte_range(),
353
+ None => pair.byte_range(),
354
+ },
355
+ };
356
+ // The closing bracket goes **into** the rewrite: an insertion at the byte the rewrite ends on
357
+ // is a second edit at the same position, which `apply_edits` refuses -- silently, so the cop
358
+ // reads as having declined to correct at all.
359
+ let opening = parentheses_the_omission_needs(context, pair);
360
+ let replacement = match opening.is_some() {
361
+ true => format!("{replacement})"),
362
+ false => replacement,
363
+ };
364
+ let mut edits = vec![Edit {
365
+ start: pair.start_byte(),
366
+ end: pair.end_byte(),
367
+ replacement,
368
+ safe: true,
369
+ }];
370
+ edits.extend(opening);
371
+ offenses.push(
372
+ context
373
+ .offense(message.into(), range)
374
+ .corrected_by_all(edits),
375
+ );
376
+ }
377
+
378
+ /// `def_node_that_require_parentheses`: dropping the value from a parenthesis-less call would
379
+ /// leave `foo bar:`, which is not the same program. Upstream adds the parentheses in the same
380
+ /// pass, replacing the space after the selector and closing after the last argument.
381
+ fn parentheses_the_omission_needs(context: &RuleContext<'_>, pair: Node<'_>) -> Option<Edit> {
382
+ let hash = pair.parent_of(context)?;
383
+ if hash.kind_str() != "hash" && hash.kind_str() != "argument_list" {
384
+ return None;
385
+ }
386
+ // Only the last pair carries the closing bracket, and only when it is the one being omitted.
387
+ let pairs = super::nodes::children_in(hash, context);
388
+ if pairs.last().is_none_or(|last| last.id() != pair.id()) {
389
+ return None;
390
+ }
391
+ // A braced hash needs nothing: `{ bar: }` already reads as one.
392
+ if hash.kind_str() == "hash"
393
+ && hash
394
+ .child(0)
395
+ .is_some_and(|first| context.source.node_text(first) == "{")
396
+ {
397
+ return None;
398
+ }
399
+ let call = find_ancestor_method_dispatch(context, hash)?;
400
+ let (selector, arguments) = (call.field("method")?, call.field("arguments")?);
401
+ // `dispatch_node.parenthesized?`
402
+ if arguments
403
+ .child(0)
404
+ .is_some_and(|first| context.source.node_text(first) == "(")
405
+ {
406
+ return None;
407
+ }
408
+ let first_argument = *super::nodes::children_in(arguments, context).first()?;
409
+ // The closing bracket only lands right when the omitted pair ends the argument list.
410
+ if arguments.end_byte() != pair.end_byte() {
411
+ return None;
412
+ }
413
+ // `return if last_expression?(dispatch) && !requires_parentheses_context?(dispatch)`: a call
414
+ // that ends its body needs no help, because `foo bar:` there still parses. One with anything
415
+ // written after it does -- `foo value:` would swallow the next line.
416
+ if last_expression(call, context) && !requires_parentheses_context(call, context) {
417
+ return None;
418
+ }
419
+ Some(Edit {
420
+ start: selector.end_byte(),
421
+ end: first_argument.start_byte(),
422
+ replacement: "(".to_owned(),
423
+ safe: true,
424
+ })
425
+ }
426
+
427
+ /// `find_ancestor_method_dispatch_node`: the call, `super` or `yield` the hash is an argument of.
428
+ fn find_ancestor_method_dispatch<'tree>(
429
+ context: &'tree RuleContext<'_>,
430
+ hash: Node<'tree>,
431
+ ) -> Option<Node<'tree>> {
432
+ let mut node = hash;
433
+ while let Some(parent) = node.parent_of(context) {
434
+ match parent.kind_str() {
435
+ "call" | "super" | "yield" => return Some(parent),
436
+ "argument_list" => node = parent,
437
+ _ => return None,
438
+ }
439
+ }
440
+ None
441
+ }
442
+
443
+ /// `requires_parentheses_context?`: the call sits where a bare `foo bar:` would be read as part of
444
+ /// the surrounding expression.
445
+ /// `last_expression?`: nothing is written after the call, once an enclosing assignment is followed
446
+ /// out to whatever holds *it*.
447
+ fn last_expression(node: Node<'_>, context: &RuleContext<'_>) -> bool {
448
+ if node.next_named_sibling().is_some() {
449
+ return false;
450
+ }
451
+ let mut current = node.parent_of(context);
452
+ while let Some(ancestor) = current {
453
+ if matches!(ancestor.kind_str(), "assignment" | "operator_assignment") {
454
+ // `return last_expression?(assignment.parent) if assignment.parent&.assignment?`.
455
+ return match ancestor
456
+ .parent_of(context)
457
+ .filter(|parent| matches!(parent.kind_str(), "assignment" | "operator_assignment"))
458
+ {
459
+ Some(parent) => last_expression(parent, context),
460
+ None => ancestor.next_named_sibling().is_none(),
461
+ };
462
+ }
463
+ current = ancestor.parent_of(context);
464
+ }
465
+ true
466
+ }
467
+
468
+ fn requires_parentheses_context(call: Node<'_>, context: &RuleContext<'_>) -> bool {
469
+ // `node.parent`: the grammar interposes an `argument_list` upstream has no node for, so the
470
+ // parent of a nested call is that list rather than the call it is an argument of.
471
+ let mut parent = call.parent_of(context);
472
+ while parent.is_some_and(|node| node.kind_str() == "argument_list") {
473
+ parent = parent.and_then(|node| node.parent_of(context));
474
+ }
475
+ parent.is_some_and(|parent| {
476
+ matches!(
477
+ parent.kind_str(),
478
+ "call" | "if" | "unless" | "super" | "until" | "while" | "yield"
479
+ )
480
+ })
481
+ }
482
+
483
+ /// `node.value_omission?`.
484
+ fn omits_value(pair: Node<'_>) -> bool {
485
+ pair.field("value").is_none()
486
+ }
487
+
488
+ /// `!pair_node.parent.hash_type?` inverted. A braceless hash has its pairs sitting in the argument
489
+ /// list, where upstream's parser still builds a `hash` around them; a hash *pattern* does not.
490
+ fn in_hash_literal(pair: Node<'_>, context: &RuleContext<'_>) -> bool {
491
+ // `Hash[foo: foo]` parks its pairs straight under the `element_reference`; upstream still
492
+ // builds a `hash` around them there, as it does for a braceless argument list.
493
+ pair.parent_of(context).is_some_and(|parent| {
494
+ matches!(
495
+ parent.kind_str(),
496
+ "hash" | "argument_list" | "element_reference"
497
+ )
498
+ })
499
+ }
500
+
501
+ /// The pairs of each hash in the file, in source order.
502
+ fn hash_groups<'tree>(context: &'tree RuleContext<'_>) -> Vec<Vec<Node<'tree>>> {
503
+ let mut groups: Vec<(usize, Vec<Node<'tree>>)> = Vec::new();
504
+ for pair in context.nodes_of("pair") {
505
+ let Some(parent) = pair.parent_of(context) else {
506
+ continue;
507
+ };
508
+ if !matches!(
509
+ parent.kind_str(),
510
+ "hash" | "argument_list" | "element_reference"
511
+ ) {
512
+ continue;
513
+ }
514
+ match groups.iter_mut().find(|(id, _)| *id == parent.id()) {
515
+ Some((_, list)) => list.push(pair),
516
+ None => groups.push((parent.id(), vec![pair])),
517
+ }
518
+ }
519
+ groups.into_iter().map(|(_, list)| list).collect()
520
+ }
521
+
522
+ /// `require_hash_value?`: whether the value has to stay written out.
523
+ fn require_hash_value(context: &RuleContext<'_>, pair: Node<'_>) -> bool {
524
+ let Some(key) = pair.field("key") else {
525
+ return true;
526
+ };
527
+ if !is_symbol_key(key) || require_hash_value_around_hash_literal(context, pair) {
528
+ return true;
529
+ }
530
+ let Some(value) = pair.field("value") else {
531
+ return true;
532
+ };
533
+ // `hash_value.type?(:send, :lvar)`: only a bare name can be the value the key stands in for.
534
+ if !matches!(value.kind_str(), "identifier" | "call") {
535
+ return true;
536
+ }
537
+ let key_source = context.source.node_text(key);
538
+ // A key ending in `!` or `?` cannot be shortened: `{ foo?: }` is not a method call on the
539
+ // omitted side.
540
+ key_source != context.source.node_text(value)
541
+ || key_source.ends_with('!')
542
+ || key_source.ends_with('?')
543
+ }
544
+
545
+ /// `node.key.sym_type?`. A key written before a colon is a symbol whatever it looks like.
546
+ fn is_symbol_key(key: Node<'_>) -> bool {
547
+ matches!(
548
+ key.kind_str(),
549
+ "hash_key_symbol" | "simple_symbol" | "delimited_symbol"
550
+ )
551
+ }
552
+
553
+ /// `require_hash_value_for_around_hash_literal?`: a braceless hash handed to a call written
554
+ /// without parentheses, inside a modifier form, keeps its values -- omitting one there changes
555
+ /// where the parser thinks the argument list ends.
556
+ fn require_hash_value_around_hash_literal(context: &RuleContext<'_>, pair: Node<'_>) -> bool {
557
+ let Some(parent) = pair.parent_of(context) else {
558
+ return false;
559
+ };
560
+ // `!node.parent.braces?`: a hash written with braces is delimited already.
561
+ if parent.kind_str() != "argument_list" {
562
+ return false;
563
+ }
564
+ let Some(dispatch) = ancestor_method_dispatch(context, parent) else {
565
+ return false;
566
+ };
567
+ // `use_element_of_hash_literal_as_receiver?`: `{ value: }.do_something` is fine.
568
+ if dispatch.field("receiver") == Some(parent) {
569
+ return false;
570
+ }
571
+ if is_parenthesized_call(context, dispatch) {
572
+ return false;
573
+ }
574
+ // `use_modifier_form_without_parenthesized_method_call?`.
575
+ let mut current = dispatch;
576
+ while let Some(ancestor) = current.parent_of(context) {
577
+ if is_modifier_form(ancestor, context) {
578
+ return true;
579
+ }
580
+ current = ancestor;
581
+ }
582
+ false
583
+ }
584
+
585
+ /// `find_ancestor_method_dispatch_node`: the call the hash is an argument of, unless that "call"
586
+ /// is an index read.
587
+ fn ancestor_method_dispatch<'tree>(
588
+ context: &'tree RuleContext<'_>,
589
+ list: Node<'tree>,
590
+ ) -> Option<Node<'tree>> {
591
+ let ancestor = list.parent_of(context)?;
592
+ if !matches!(
593
+ ancestor.kind_str(),
594
+ "call" | "super" | "yield" | "method_call"
595
+ ) {
596
+ return None;
597
+ }
598
+ // `brackets?`: `foo[bar: 1]` is not a dispatch the omission can confuse.
599
+ let method = ancestor
600
+ .field("method")
601
+ .map(|node| context.source.node_text(node));
602
+ if matches!(method, Some("[]" | "[]=")) {
603
+ return None;
604
+ }
605
+ Some(ancestor)
606
+ }
607
+
608
+ fn is_parenthesized_call(context: &RuleContext<'_>, node: Node<'_>) -> bool {
609
+ node.field("arguments")
610
+ .is_some_and(|list| context.source.node_text(list).starts_with('('))
611
+ }
612
+
613
+ /// `modifier_form?`: a trailing `if`, `unless`, `while`, `until` or `rescue`.
614
+ fn is_modifier_form(node: Node<'_>, context: &RuleContext<'_>) -> bool {
615
+ if !matches!(
616
+ node.kind_str(),
617
+ "if_modifier" | "unless_modifier" | "while_modifier" | "until_modifier" | "rescue_modifier"
618
+ ) {
619
+ return false;
620
+ }
621
+ let _ = context;
622
+ true
623
+ }
624
+
81
625
  /// `hash_node.parent&.return_type? && !hash_node.braces?`: the span of a braceless hash written
82
626
  /// straight after `return`, given only for its first pair so the braces go on once.
83
627
  ///
@@ -91,7 +635,7 @@ fn returned_bare_hash(node: Node<'_>, context: &RuleContext<'_>) -> Option<(usiz
91
635
  if context.parent(list)?.kind_str() != "return" {
92
636
  return None;
93
637
  }
94
- let pairs: Vec<Node<'_>> = super::nodes::children(list)
638
+ let pairs: Vec<Node<'_>> = super::nodes::children_in(list, context)
95
639
  .into_iter()
96
640
  .filter(|child| child.kind_str() == "pair")
97
641
  .collect();
@@ -111,7 +655,7 @@ fn argument_without_space(node: Node<'_>, context: &RuleContext<'_>) -> bool {
111
655
  // The comparison is against **the hash**, not the argument list. A parenthesized call has its
112
656
  // list start at the `(`, which always sits right after the selector, so comparing the list
113
657
  // would put a space into every `func(:a => 0)`.
114
- let first = super::nodes::children(list)
658
+ let first = super::nodes::children_in(list, context)
115
659
  .into_iter()
116
660
  .find(|child| child.kind_str() == "pair");
117
661
  let Some(first) = first else {
@@ -140,44 +684,38 @@ fn key_name<'a>(node: Node<'_>, context: &'a RuleContext<'_>) -> &'a str {
140
684
  text.strip_prefix(':').unwrap_or(text)
141
685
  }
142
686
 
143
- /// `sym_indices?`: whether every key of the hash this pair belongs to is a symbol the new syntax
144
- /// can spell.
145
- fn every_key_takes_the_new_syntax(
146
- node: Node<'_>,
147
- context: &RuleContext<'_>,
148
- quoted_keys_allowed: bool,
149
- ) -> bool {
150
- let Some(container) = node.parent_of(context) else {
151
- return false;
152
- };
153
- let mut cursor = container.walk();
154
- let pairs: Vec<Node<'_>> = container
155
- .named_children(&mut cursor)
156
- .filter(|child| child.kind_str() == "pair")
157
- .collect();
158
- !pairs.is_empty()
159
- && pairs
160
- .iter()
161
- .all(|pair| word_symbol_pair(*pair, context, quoted_keys_allowed))
162
- }
163
-
164
687
  /// `word_symbol_pair?`: a symbol key whose name the new syntax accepts.
165
688
  ///
166
689
  /// A key written with a colon is already a symbol whatever it looks like -- `"a b": 1` reaches
167
690
  /// RuboCop as a `dsym` -- so only the rocket form has to have its node kind checked.
168
- fn word_symbol_pair(node: Node<'_>, context: &RuleContext<'_>, quoted_keys_allowed: bool) -> bool {
691
+ fn word_symbol_pair(
692
+ node: Node<'_>,
693
+ context: &RuleContext<'_>,
694
+ quoted_keys_allowed: bool,
695
+ prefer_rockets_for_non_alnum: bool,
696
+ ) -> bool {
169
697
  let Some(key) = node.field("key") else {
170
698
  return false;
171
699
  };
172
700
  let is_symbol = hash_rocket(node, context).is_none()
173
701
  || matches!(key.kind_str(), "simple_symbol" | "delimited_symbol");
174
- is_symbol && acceptable_19_syntax_symbol(context.source.node_text(key), quoted_keys_allowed)
702
+ is_symbol
703
+ && acceptable_19_syntax_symbol(
704
+ context.source.node_text(key),
705
+ quoted_keys_allowed,
706
+ prefer_rockets_for_non_alnum,
707
+ )
175
708
  }
176
709
 
177
- fn acceptable_19_syntax_symbol(text: &str, quoted_keys_allowed: bool) -> bool {
710
+ fn acceptable_19_syntax_symbol(
711
+ text: &str,
712
+ quoted_keys_allowed: bool,
713
+ prefer_rockets_for_non_alnum: bool,
714
+ ) -> bool {
178
715
  let name = text.strip_prefix(':').unwrap_or(text);
179
716
  if PLAIN_SYMBOL.is_match(name) {
180
- return true;
717
+ return !(prefer_rockets_for_non_alnum
718
+ && matches!(name.as_bytes().last(), Some(b'?' | b'!')));
181
719
  }
182
720
  quoted_keys_allowed
183
721
  && name.len() >= 2