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
@@ -11,6 +11,8 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
11
11
  .unwrap_or_default();
12
12
  for range in context.comment_ranges() {
13
13
  let range = super::comments::parser_range(range, context);
14
+ // The parser's span for a block comment can run past a file with no final newline.
15
+ let range = range.start..range.end.min(context.source.text().len());
14
16
  let comment = &context.source.text()[range.clone()];
15
17
  if comment.is_ascii() {
16
18
  continue;
@@ -97,7 +97,7 @@ fn is_class_eval(context: &RuleContext<'_>, block: Node<'_>) -> bool {
97
97
 
98
98
  /// `each_descendant(:def).any? { |def_node| def_node.method?(:attr) }`.
99
99
  fn defines_attr(context: &RuleContext<'_>, node: Node<'_>) -> bool {
100
- super::conditional::descendants(node)
100
+ super::conditional::descendants(node, context)
101
101
  .into_iter()
102
102
  .filter(|descendant| descendant.kind_str() == "method")
103
103
  .any(|definition| {
@@ -61,7 +61,7 @@ fn cleans_up(node: Node<'_>, context: &RuleContext<'_>) -> bool {
61
61
  return false;
62
62
  };
63
63
  if parent.kind_str() == "program" {
64
- let statements: Vec<Node<'_>> = super::nodes::children(parent)
64
+ let statements: Vec<Node<'_>> = super::nodes::children_in(parent, context)
65
65
  .into_iter()
66
66
  .filter(|child| child.kind_str() != "comment")
67
67
  .collect();
@@ -95,12 +95,12 @@ fn comparison<'tree>(
95
95
  /// `(begin (send _ :& _))`: the two operands of a parenthesized mask.
96
96
  fn masked<'tree>(
97
97
  node: Node<'tree>,
98
- context: &RuleContext<'_>,
98
+ context: &'tree RuleContext<'_>,
99
99
  ) -> Option<(Node<'tree>, Node<'tree>)> {
100
100
  if node.kind_str() != "parenthesized_statements" {
101
101
  return None;
102
102
  }
103
- let inner = super::nodes::children(node);
103
+ let inner = super::nodes::children_in(node, context);
104
104
  let [inner] = inner.as_slice() else {
105
105
  return None;
106
106
  };
@@ -36,7 +36,7 @@ pub(super) fn implicit(context: &RuleContext<'_>, block: Node<'_>) -> bool {
36
36
  /// The part of the body that belongs to this block: a nested block's `_1` is that block's, not
37
37
  /// this one's.
38
38
  fn scan(context: &RuleContext<'_>, node: Node<'_>, numbered: &mut bool, it: &mut bool) {
39
- for child in super::nodes::children(node) {
39
+ for child in super::nodes::children_in(node, context) {
40
40
  if matches!(child.kind_str(), "block" | "do_block" | "lambda") {
41
41
  continue;
42
42
  }
@@ -18,9 +18,18 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
18
18
  // the grammar stops at the last character of that line.
19
19
  let start = node.start_byte();
20
20
  let mut end = node.end_byte();
21
- if text.as_bytes().get(end) == Some(&b'\n') {
21
+ // **A file that does not end in a newline still has one to the parser**, and a block
22
+ // comment's range runs one character past even that. Only the report reaches there; every
23
+ // read below stays inside the text.
24
+ let closed_by_newline = text.as_bytes().get(end) == Some(&b'\n');
25
+ if closed_by_newline {
22
26
  end += 1;
23
27
  }
28
+ let reported_end = match closed_by_newline {
29
+ true => end,
30
+ false => end + 2,
31
+ };
32
+
24
33
  // `=begin\n=end` is the shortest block comment the parser accepts; anything shorter would
25
34
  // only reach here from a file that does not parse, where no cop runs at all.
26
35
  if end - start < BEGIN_LENGTH + 4 {
@@ -61,7 +70,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
61
70
  replacement: String::new(),
62
71
  safe: true,
63
72
  });
64
- offenses.push(context.offense(MSG, start..end).corrected_by_all(edits));
73
+ offenses.push(context.offense(MSG, start..reported_end).corrected_by_all(edits));
65
74
  }
66
75
  }
67
76
 
@@ -7,6 +7,7 @@ use tree_sitter::Node;
7
7
  use crate::diagnostic::{Edit, Offense};
8
8
  use crate::rules::RuleContext;
9
9
  use crate::rules::node_ext::NodeExt;
10
+ use crate::rules::send_node::all_children_iter;
10
11
 
11
12
  const MSG_MULTILINE: &str = "Avoid using `{...}` for multi-line blocks.";
12
13
  const MSG_SINGLE_LINE: &str = "Prefer `{...}` over `do...end` for single-line blocks.";
@@ -26,6 +27,32 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
26
27
  .setting::<String>("EnforcedStyle")
27
28
  .unwrap_or_else(|| "line_count_based".to_owned()),
28
29
  braces_required_methods: context.setting("BracesRequiredMethods").unwrap_or_default(),
30
+ functional_methods: context.setting("FunctionalMethods").unwrap_or_else(|| {
31
+ ["let", "let!", "subject", "watch"]
32
+ .iter()
33
+ .map(|name| (*name).to_owned())
34
+ .collect()
35
+ }),
36
+ procedural_methods: context.setting("ProceduralMethods").unwrap_or_else(|| {
37
+ [
38
+ "benchmark",
39
+ "bm",
40
+ "bmbm",
41
+ "create",
42
+ "each_with_object",
43
+ "measure",
44
+ "new",
45
+ "realtime",
46
+ "tap",
47
+ "with_object",
48
+ ]
49
+ .iter()
50
+ .map(|name| (*name).to_owned())
51
+ .collect()
52
+ }),
53
+ procedural_oneliners_may_have_braces: context
54
+ .setting("AllowBracesOnProceduralOneLiners")
55
+ .unwrap_or(false),
29
56
  allowed_methods: context.setting("AllowedMethods").unwrap_or_else(|| {
30
57
  ["lambda", "proc", "it"]
31
58
  .iter()
@@ -84,6 +111,12 @@ struct Cop<'a, 'tree> {
84
111
  braces_required_methods: Vec<String>,
85
112
  allowed_methods: Vec<String>,
86
113
  allowed_patterns: Vec<regex::Regex>,
114
+ /// `FunctionalMethods`: the ones whose block always yields a value, so braces suit them.
115
+ functional_methods: Vec<String>,
116
+ /// `ProceduralMethods`: the ones whose block is run for its effect, so `do ... end` suits.
117
+ procedural_methods: Vec<String>,
118
+ /// `AllowBracesOnProceduralOneLiners`.
119
+ procedural_oneliners_may_have_braces: bool,
87
120
  }
88
121
 
89
122
  /// One block: its delimiters, the call it hangs off, and the body between them.
@@ -98,7 +131,7 @@ struct Block<'t> {
98
131
  }
99
132
 
100
133
  impl<'t> Block<'t> {
101
- fn new(context: &RuleContext<'_>, node: Node<'t>) -> Option<Self> {
134
+ fn new(context: &'t RuleContext<'_>, node: Node<'t>) -> Option<Self> {
102
135
  let braces = node.kind_str() == "block";
103
136
  let call = node.parent()?;
104
137
  // `-> { }` is a block whose call is `lambda` upstream, however the arrow is written.
@@ -109,8 +142,8 @@ impl<'t> Block<'t> {
109
142
  .map(|method| context.source.node_text(method).to_owned())
110
143
  .unwrap_or_default(),
111
144
  };
112
- let mut cursor = node.walk();
113
- let children: Vec<Node<'t>> = node.children(&mut cursor).collect();
145
+ let _cursor = node.walk();
146
+ let children: Vec<Node<'t>> = all_children_iter(node, context).collect();
114
147
  let begin = *children
115
148
  .iter()
116
149
  .find(|child| !child.is_named() && matches!(child.kind_str(), "{" | "do"))?;
@@ -164,7 +197,7 @@ impl Cop<'_, '_> {
164
197
  vec![right]
165
198
  }
166
199
  "element_reference" => {
167
- let mut arguments = super::nodes::children(node);
200
+ let mut arguments = super::nodes::children_in(node, self.context);
168
201
  if node.field("object").is_some() && !arguments.is_empty() {
169
202
  arguments.remove(0);
170
203
  }
@@ -252,11 +285,117 @@ impl Cop<'_, '_> {
252
285
  },
253
286
  false => block.braces,
254
287
  },
288
+ "semantic" => self.semantic_block_style(block),
255
289
  // `line_count_based_block_style?`.
256
290
  _ => block.multiline != block.braces,
257
291
  }
258
292
  }
259
293
 
294
+ /// `semantic_block_style?`: braces mark a block whose value is used, `do ... end` one that is
295
+ /// run for its effect. Which one a block is comes from the method it hangs off and from where
296
+ /// the block itself sits -- a block whose result nothing reads is procedural whatever it does.
297
+ fn semantic_block_style(&self, block: &Block<'_>) -> bool {
298
+ if block.braces {
299
+ self.functional_methods.contains(&block.method)
300
+ || self.functional_block(block)
301
+ || (self.procedural_oneliners_may_have_braces && !block.multiline)
302
+ } else {
303
+ self.procedural_methods.contains(&block.method) || !self.return_value_used(block.call)
304
+ }
305
+ }
306
+
307
+ /// `functional_block?`.
308
+ fn functional_block(&self, block: &Block<'_>) -> bool {
309
+ self.return_value_used(block.call) || self.return_value_of_scope(block.call)
310
+ }
311
+
312
+ /// `return_value_used?`: the block feeds an assignment or another call.
313
+ ///
314
+ /// The node walked from is upstream's `block`, which the grammar spells as the call the braces
315
+ /// hang off, so the parent chain reads the same from there.
316
+ fn return_value_used(&self, node: Node<'_>) -> bool {
317
+ let Some(parent) = node.parent_of(self.context) else {
318
+ return false;
319
+ };
320
+ match parent.kind_str() {
321
+ // `node.parent.begin_type?`: parentheses around the block, which pass the question on.
322
+ "parenthesized_statements" => self.return_value_used(parent),
323
+ "assignment" | "operator_assignment" => true,
324
+ // `call_type?`: Parser folds arguments, indexing and non-keyword operators into
325
+ // `send` nodes. tree-sitter keeps the argument-list wrapper and operator forms.
326
+ "call" | "method_call" | "argument_list" | "element_reference" | "unary" => true,
327
+ // Ordinary operators are `send` nodes to Parser, while `&&` / `||` / `and` / `or`
328
+ // remain keyword operators. The latter make braces functional through
329
+ // `return_value_of_scope?`, but do not make a `do...end` block's value directly used.
330
+ "binary" => parent.field("operator").is_some_and(|operator| {
331
+ !LOGICAL_OPERATORS.contains(&self.context.source.node_text(operator))
332
+ }),
333
+ _ => false,
334
+ }
335
+ }
336
+
337
+ /// `return_value_of_scope?`: the block sits where a value is read off -- a condition, an
338
+ /// operand, an element, or the last expression of whatever encloses it.
339
+ fn return_value_of_scope(&self, node: Node<'_>) -> bool {
340
+ let Some(parent) = node.parent_of(self.context) else {
341
+ return false;
342
+ };
343
+ if matches!(
344
+ parent.kind_str(),
345
+ "if" | "unless"
346
+ | "elsif"
347
+ | "if_modifier"
348
+ | "unless_modifier"
349
+ | "while"
350
+ | "until"
351
+ | "while_modifier"
352
+ | "until_modifier"
353
+ | "case"
354
+ | "case_match"
355
+ | "conditional"
356
+ | "array"
357
+ | "range"
358
+ ) {
359
+ return true;
360
+ }
361
+ // `operator_keyword?`: `and`, `or`, `not`.
362
+ if parent.kind_str() == "binary"
363
+ && parent.field("operator").is_some_and(|operator| {
364
+ LOGICAL_OPERATORS.contains(&&self.context.source.text()[operator.byte_range()])
365
+ })
366
+ {
367
+ return true;
368
+ }
369
+ if parent.kind_str() == "unary" {
370
+ return true;
371
+ }
372
+ // `return false unless (parent = node.parent)`: a lone top-level statement **has no
373
+ // parent** upstream. The grammar always hands back a `program`, whose only child is that
374
+ // statement -- reading it as "the last thing the parent holds" made every such block look
375
+ // like the value of its scope, and `each { |x| x }` went unreported under `semantic`.
376
+ if parent.kind_str() == "program" && super::nodes::children(parent).len() == 1 {
377
+ return false;
378
+ }
379
+ // `parent.children.last == node`: the block is the last thing the parent holds, so its
380
+ // value is the parent's.
381
+ let children = super::nodes::children(parent);
382
+ // Parser represents a method-level rescue as a `rescue` whose protected body is a
383
+ // separate `begin`. tree-sitter appends the rescue clauses to one `body_statement`; the
384
+ // last expression before the first clause is the value of that virtual begin.
385
+ let value_children = match parent.kind_str() {
386
+ "body_statement" => {
387
+ &children[..children
388
+ .iter()
389
+ .position(|child| matches!(child.kind_str(), "rescue" | "else" | "ensure"))
390
+ .unwrap_or(children.len())]
391
+ }
392
+ _ => children.as_slice(),
393
+ };
394
+ value_children
395
+ .last()
396
+ .is_some_and(|last| last.id() == node.id())
397
+ }
398
+
260
399
  fn braces_required_method(&self, method: &str) -> bool {
261
400
  self.braces_required_methods
262
401
  .iter()
@@ -271,7 +410,7 @@ impl Cop<'_, '_> {
271
410
  let Some(body) = block.body() else {
272
411
  return false;
273
412
  };
274
- let clauses = super::nodes::children(body);
413
+ let clauses = super::nodes::children_in(body, self.context);
275
414
  if clauses.iter().any(|child| child.kind_str() == "ensure") {
276
415
  return true;
277
416
  }
@@ -351,7 +490,7 @@ impl Cop<'_, '_> {
351
490
  // `begin_required?`: a protected body needs its own `begin` once the keywords are gone.
352
491
  if block.multiline
353
492
  && let Some(body) = block.body()
354
- && super::nodes::children(body)
493
+ && super::nodes::children_in(body, self.context)
355
494
  .iter()
356
495
  .any(|child| matches!(child.kind_str(), "rescue" | "ensure"))
357
496
  {
@@ -445,10 +584,15 @@ fn get_blocks(node: Node<'_>, out: &mut Vec<Range<usize>>) {
445
584
  /// Whether the call carrying the block is itself the receiver of another one.
446
585
  fn chained(call: Node<'_>) -> bool {
447
586
  call.parent().is_some_and(|parent| {
448
- matches!(parent.kind_str(), "call" | "method_call")
449
- && parent
450
- .field("receiver")
451
- .is_some_and(|receiver| receiver.id() == call.id())
587
+ let receiver = match parent.kind_str() {
588
+ "call" | "method_call" => parent.field("receiver"),
589
+ // `foo { ... }[key]` is a `send` of `:[]` upstream and therefore extends the same
590
+ // chain as a dotted call. The grammar gives indexing its own node and calls the
591
+ // receiver `object`.
592
+ "element_reference" => parent.field("object"),
593
+ _ => None,
594
+ };
595
+ receiver.is_some_and(|receiver| receiver.id() == call.id())
452
596
  })
453
597
  }
454
598
 
@@ -3,6 +3,7 @@ use tree_sitter::Node;
3
3
  use crate::diagnostic::{Edit, Offense};
4
4
  use crate::rules::RuleContext;
5
5
  use crate::rules::node_ext::NodeExt;
6
+ use crate::rules::send_node::named_children_iter;
6
7
 
7
8
  const MSG: &str = "Avoid the use of the case equality operator `===`.";
8
9
 
@@ -71,6 +72,10 @@ fn case_equality<'tree>(
71
72
  ))
72
73
  }
73
74
  _ => {
75
+ // `on_send` is not aliased to `on_csend`: `a&.===(b)` is outside this cop.
76
+ if !crate::rules::send_node::is_plain_send(node, context) {
77
+ return None;
78
+ }
74
79
  let method = node.field("method")?;
75
80
  if context.source.node_text(method) != "===" {
76
81
  return None;
@@ -99,8 +104,8 @@ fn replacement(
99
104
  let _ = node;
100
105
  let source = context.source.node_text(left);
101
106
  if left.kind_str() == "parenthesized_statements" {
102
- let mut cursor = left.walk();
103
- let inner = left.named_children(&mut cursor).next()?;
107
+ let _cursor = left.walk();
108
+ let inner = named_children_iter(left, context).next()?;
104
109
  if inner.kind_str() != "range" {
105
110
  return None;
106
111
  }
@@ -39,7 +39,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
39
39
  let branches = branch_conditions(node);
40
40
  // `elsif_conditional?` and `min_branches_count?`: the chain has to be an `if`/`elsif` one
41
41
  // with enough arms to be worth a `case`.
42
- if branches.len() < 2 || branches.len() < minimum {
42
+ if branches.len() < 2 || branch_count(node) < minimum {
43
43
  continue;
44
44
  }
45
45
  let Some(target) = find_target(context, branches[0]) else {
@@ -88,6 +88,30 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
88
88
  }
89
89
  }
90
90
 
91
+ /// `if_conditional_branches`: the arms a conditional chain has, which is what `MinBranchesCount`
92
+ /// is counted against. **A ternary is an `if` upstream**, so an `else` holding one contributes its
93
+ /// own arm -- `if a / elsif b / else c ? d : e` has three, not two.
94
+ fn branch_count(node: Node<'_>) -> usize {
95
+ let mut count = 0;
96
+ let mut current = Some(node);
97
+ while let Some(branch) = current {
98
+ if !matches!(branch.kind_str(), "if" | "elsif" | "conditional") {
99
+ break;
100
+ }
101
+ count += 1;
102
+ current = match branch.field("alternative") {
103
+ Some(alternative) if alternative.kind_str() == "else" => {
104
+ match super::nodes::children(alternative).as_slice() {
105
+ [only] => Some(*only),
106
+ _ => None,
107
+ }
108
+ }
109
+ other => other,
110
+ };
111
+ }
112
+ count
113
+ }
114
+
91
115
  /// `branch_conditions`: the condition of every `if`/`elsif` in the chain.
92
116
  fn branch_conditions<'t>(node: Node<'t>) -> Vec<Node<'t>> {
93
117
  let mut conditions = Vec::new();
@@ -159,7 +183,7 @@ fn collect_conditions(
159
183
  conditions: &mut Vec<String>,
160
184
  ) -> bool {
161
185
  if node.kind_str() == "parenthesized_statements" {
162
- let Some(inner) = super::nodes::children(node).into_iter().next() else {
186
+ let Some(inner) = super::nodes::children_in(node, context).into_iter().next() else {
163
187
  return false;
164
188
  };
165
189
  return collect_conditions(context, inner, target, conditions);
@@ -3,6 +3,8 @@ use tree_sitter::Node;
3
3
  use crate::diagnostic::{Edit, Offense};
4
4
  use crate::rules::RuleContext;
5
5
  use crate::rules::node_ext::NodeExt;
6
+ use crate::rules::send_node::all_children_of;
7
+ use crate::rules::send_node::named_children_iter;
6
8
 
7
9
  const NESTED_MSG: &str = "Use nested module/class definitions instead of compact style.";
8
10
  const COMPACT_MSG: &str = "Use compact module/class definition instead of nested style.";
@@ -61,12 +63,20 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
61
63
 
62
64
  if style == "nested" {
63
65
  check_nested(context, node, name, offenses);
64
- } else if matches!(body_statements(node).as_slice(), [only] if matches!(only.kind_str(), "class" | "module"))
66
+ } else if let [only] = body_statements(node).as_slice()
67
+ && matches!(only.kind_str(), "class" | "module")
65
68
  {
66
- // The compact correction rewrites the body's indentation as well, which is not
67
- // expressible as the single edit an offense carries here, so the offense is reported
68
- // without one.
69
- offenses.push(context.offense(COMPACT_MSG, name.byte_range()));
69
+ let inner_style = match only.kind_str() == "class" {
70
+ true => for_classes.as_str(),
71
+ false => for_modules.as_str(),
72
+ };
73
+ let offense = context.offense(COMPACT_MSG, name.byte_range());
74
+ offenses.push(
75
+ match compact_correction(context, node, name, *only, inner_style) {
76
+ Some(edits) => offense.corrected_by_all(edits),
77
+ None => offense,
78
+ },
79
+ );
70
80
  }
71
81
  }
72
82
  }
@@ -158,9 +168,9 @@ fn nested_correction(
158
168
  if closing.kind_str() != "end" {
159
169
  return None;
160
170
  }
161
- let mut cursor = name.walk();
162
- let separator = name
163
- .children(&mut cursor)
171
+ let _cursor = name.walk();
172
+ let separator = all_children_of(name, context)
173
+ .into_iter()
164
174
  .find(|child| child.kind_str() == "::")?;
165
175
 
166
176
  let (_, column) = context.source.line_column(node.start_byte());
@@ -200,6 +210,122 @@ fn nested_correction(
200
210
  ])
201
211
  }
202
212
 
213
+ /// `compact_definition`: `class Foo` holding nothing but `class Bar` becomes `class Foo::Bar`.
214
+ ///
215
+ /// Three edits, as upstream: the header up to the inner name, the inner `end`, and the body's
216
+ /// indentation. The last one is a run of per-line edits rather than a single replacement, so the
217
+ /// cops correcting inside the body still can in the same pass.
218
+ fn compact_correction(
219
+ context: &RuleContext<'_>,
220
+ node: Node<'_>,
221
+ name: Node<'_>,
222
+ inner: Node<'_>,
223
+ inner_style: &str,
224
+ ) -> Option<Vec<Edit>> {
225
+ // Compacting produces a definition whose own style resolves to `nested`, which would make the
226
+ // correction ping-pong between the two forms.
227
+ if inner_style == "nested" {
228
+ return None;
229
+ }
230
+ let keyword = node.child(0)?;
231
+ let inner_name = inner.field("name")?;
232
+ let inner_end = inner.child(inner.child_count().saturating_sub(1) as u32)?;
233
+ if inner_end.kind_str() != "end" {
234
+ return None;
235
+ }
236
+ // The cop is declared `SafeAutoCorrect: false`: compacting removes a definition of the
237
+ // namespace, so the result needs it defined elsewhere.
238
+ let safe = false;
239
+ let compacted = format!(
240
+ "{} {}::{}",
241
+ inner.kind_str(),
242
+ context.source.node_text(name),
243
+ context.source.node_text(inner_name)
244
+ );
245
+ // `compact_replacement`: the comments `ast_with_comments` ties to the inner definition stand
246
+ // inside the range being replaced, so they have to be written back above the compacted line.
247
+ let comments = leading_comments(context, inner);
248
+ let replacement = match comments.is_empty() {
249
+ true => compacted,
250
+ false => format!("{}\n{compacted}", comments.join("\n")),
251
+ };
252
+ let mut edits = vec![Edit {
253
+ start: keyword.start_byte(),
254
+ end: inner_name.end_byte(),
255
+ replacement,
256
+ safe,
257
+ }];
258
+
259
+ // `remove_end`: the inner `end`, the blanks in front of it and the line break after it.
260
+ let same_line = inner_name.end_position().row == inner_end.start_position().row;
261
+ let leading = leading_spaces(context, inner.start_byte());
262
+ let removal_start = match same_line {
263
+ true => inner_name.end_byte(),
264
+ false => inner_end.start_byte().saturating_sub(leading.len()),
265
+ };
266
+ let adjustment = match context.source.text().as_bytes().get(removal_start) {
267
+ Some(b';') => 0,
268
+ _ => 1,
269
+ };
270
+ edits.push(Edit {
271
+ start: removal_start,
272
+ end: (inner_end.end_byte() + adjustment).min(context.source.text().len()),
273
+ replacement: String::new(),
274
+ safe,
275
+ });
276
+
277
+ edits.extend(unindent(context, node, inner, inner_name.end_byte(), safe));
278
+ Some(edits)
279
+ }
280
+
281
+ /// `unindent`: the body moves left by the difference between the configured width and the
282
+ /// indentation the inner definition gave it.
283
+ fn unindent(
284
+ context: &RuleContext<'_>,
285
+ node: Node<'_>,
286
+ inner: Node<'_>,
287
+ header_end: usize,
288
+ safe: bool,
289
+ ) -> Vec<Edit> {
290
+ let Some(last) = body_statements(inner).last().copied() else {
291
+ return Vec::new();
292
+ };
293
+ let outer_indent = leading_spaces(context, node.start_byte()).chars().count();
294
+ let inner_indent = leading_spaces(context, last.start_byte()).chars().count();
295
+ if outer_indent == inner_indent {
296
+ return Vec::new();
297
+ }
298
+ let width: usize = context
299
+ .setting("IndentationWidth")
300
+ .or_else(|| context.setting_of("Layout/IndentationWidth", "Width"))
301
+ .unwrap_or(2);
302
+ let Some(delta) = inner_indent.checked_sub(width).filter(|delta| *delta > 0) else {
303
+ return Vec::new();
304
+ };
305
+ // Only the lines the header edit does not already cover.
306
+ let first = context.source.line_column(header_end).0 + 1;
307
+ let last_line = context.source.line_column(node.end_byte()).0;
308
+ (first..last_line)
309
+ .filter_map(|line| {
310
+ let text = context.source.line(line);
311
+ let removable = text
312
+ .chars()
313
+ // Upstream removes a run of literal spaces. A tab is one source character but
314
+ // does not match that run, so tab-indented bodies remain byte-for-byte intact.
315
+ .take_while(|character| *character == ' ')
316
+ .count()
317
+ .min(delta);
318
+ let start = context.source.line_start(line);
319
+ (removable > 0).then(|| Edit {
320
+ start,
321
+ end: start + removable,
322
+ replacement: String::new(),
323
+ safe,
324
+ })
325
+ })
326
+ .collect()
327
+ }
328
+
203
329
  /// `padding.sub(' ' * column, '')`: one run of that many spaces comes back out of the padding.
204
330
  fn drop_one_run(padding: &str, spaces: usize) -> String {
205
331
  let run = " ".repeat(spaces);
@@ -232,8 +358,8 @@ fn namespace_keyword(context: &RuleContext<'_>, node: Node<'_>, name: Node<'_>)
232
358
  {
233
359
  return "class";
234
360
  }
235
- let mut cursor = current.walk();
236
- stack.extend(current.named_children(&mut cursor));
361
+ let _cursor = current.walk();
362
+ stack.extend(named_children_iter(current, context));
237
363
  }
238
364
  "module"
239
365
  }
@@ -264,6 +390,32 @@ fn normalized(text: &str) -> String {
264
390
  .collect()
265
391
  }
266
392
 
393
+ /// `ast_with_comments[node]` for a definition: the run of comment lines written directly above it,
394
+ /// in source order and without their indentation, which is what `Comment#text` gives.
395
+ fn leading_comments(context: &RuleContext<'_>, node: Node<'_>) -> Vec<String> {
396
+ let opening = context.source.line_column(node.start_byte()).0;
397
+ let mut collected = Vec::new();
398
+ let mut probe = opening;
399
+ while probe > 1 {
400
+ let line = context.source.line(probe - 1);
401
+ let text = line.trim();
402
+ // A `#` opening a line is a comment only if the tokeniser agrees -- a heredoc body can
403
+ // hold anything.
404
+ let is_comment = text.starts_with('#')
405
+ && context
406
+ .comment_ranges()
407
+ .iter()
408
+ .any(|comment| context.source.line_column(comment.start).0 == probe - 1);
409
+ if !is_comment {
410
+ break;
411
+ }
412
+ collected.push(text.to_owned());
413
+ probe -= 1;
414
+ }
415
+ collected.reverse();
416
+ collected
417
+ }
418
+
267
419
  /// `leading_spaces`: the indentation of the line the definition opens on.
268
420
  fn leading_spaces(context: &RuleContext<'_>, offset: usize) -> String {
269
421
  let (line, _) = context.source.line_column(offset);
@@ -17,7 +17,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
17
17
  };
18
18
  // Only the statements the body holds directly are looked at: a definition nested inside a
19
19
  // `begin` or a conditional is not a child of the class body upstream either.
20
- for definition in super::nodes::children(body) {
20
+ for definition in super::nodes::children_in(body, context) {
21
21
  if definition.kind_str() != "singleton_method" {
22
22
  continue;
23
23
  }
@@ -60,6 +60,15 @@ fn check_singleton_class(node: Node<'_>, context: &RuleContext<'_>, offenses: &m
60
60
  if ranges.iter().any(|range| range.end >= node.end_byte()) {
61
61
  return;
62
62
  }
63
+ // A definition written on the `class << self` line itself leaves the correction asking the
64
+ // rewriter for a range it cannot build (`RangeError`), and the offense goes with it.
65
+ let keyword_line = context.source.line_column(node.start_byte()).0;
66
+ if definitions
67
+ .iter()
68
+ .any(|definition| context.source.line_column(definition.start_byte()).0 == keyword_line)
69
+ {
70
+ return;
71
+ }
63
72
  let mut rewritten: Vec<String> = definitions
64
73
  .iter()
65
74
  .zip(&ranges)