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
@@ -17,7 +17,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
17
17
  continue;
18
18
  };
19
19
  // `empty_and_without_delimiters?`: bars were written around nothing.
20
- if !super::nodes::children(parameters).is_empty() {
20
+ if !super::nodes::children_in(parameters, context).is_empty() {
21
21
  continue;
22
22
  }
23
23
  let Some(begin) = block.child(0) else {
@@ -15,7 +15,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
15
15
  if is_unsupported_parent(context, node) {
16
16
  continue;
17
17
  }
18
- let branches = super::nodes::children(node);
18
+ let branches = super::nodes::children_in(node, context);
19
19
  let whens: Vec<Node<'_>> = branches
20
20
  .iter()
21
21
  .copied()
@@ -29,7 +29,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
29
29
  .iter()
30
30
  .filter_map(|when| when.field("body"))
31
31
  .chain(branches.iter().copied().filter(|it| it.kind_str() == "else"));
32
- if bodies.into_iter().any(holds_return) {
32
+ if bodies.into_iter().any(|body| holds_return(body, context)) {
33
33
  continue;
34
34
  }
35
35
  let (Some(case_keyword), Some(when_keyword)) = (node.child(0), first_when.child(0)) else {
@@ -109,9 +109,9 @@ fn is_unsupported_parent(context: &RuleContext<'_>, node: Node<'_>) -> bool {
109
109
  }
110
110
  }
111
111
 
112
- fn holds_return(body: Node<'_>) -> bool {
112
+ fn holds_return(body: Node<'_>, context: &RuleContext<'_>) -> bool {
113
113
  let mut found = false;
114
- walk_named(body, &mut |node| {
114
+ walk_named(body, context, &mut |node| {
115
115
  found = found || node.kind_str() == "return";
116
116
  });
117
117
  found
@@ -148,7 +148,7 @@ fn correct_when_conditions(
148
148
  // the `case`, because the node it reaches for is not there.
149
149
  let nested = has_upstream_parent(case_node);
150
150
  for when in whens {
151
- let conditions: Vec<Node<'_>> = super::nodes::children(*when)
151
+ let conditions: Vec<Node<'_>> = super::nodes::children_in(*when, context)
152
152
  .into_iter()
153
153
  .filter(|child| child.kind_str() == "pattern")
154
154
  .collect();
@@ -191,7 +191,7 @@ fn then_keyword<'tree>(when: Node<'tree>) -> Option<Node<'tree>> {
191
191
  /// once the conditions are joined with one.
192
192
  fn parenthesize(context: &RuleContext<'_>, condition: Node<'_>) -> String {
193
193
  let source = context.source.node_text(condition);
194
- let inner = super::nodes::children(condition);
194
+ let inner = super::nodes::children_in(condition, context);
195
195
  let binds_looser = match inner.as_slice() {
196
196
  [only] => match only.kind_str() {
197
197
  "if" | "unless" | "if_modifier" | "unless_modifier" | "conditional" | "range" => true,
@@ -101,7 +101,7 @@ fn check_class(
101
101
  if node.field("body").is_some() {
102
102
  return;
103
103
  }
104
- let parts = super::nodes::children(superclass);
104
+ let parts = super::nodes::children_in(superclass, context);
105
105
  let [parent] = parts.as_slice() else {
106
106
  return;
107
107
  };
@@ -39,7 +39,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
39
39
  let Some(keyword) = clause.child(0) else {
40
40
  continue;
41
41
  };
42
- let body = super::nodes::children(clause);
42
+ let body = super::nodes::children_in(clause, context);
43
43
  let reportable = match body.as_slice() {
44
44
  [] => empty_style,
45
45
  [only] => nil_style && only.kind_str() == "nil",
@@ -15,10 +15,16 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
15
15
  if context.source.node_text(node).contains('`') {
16
16
  continue;
17
17
  }
18
+ // **An empty delimiter leaves upstream with nothing to point at.** `<<''` parses to a
19
+ // `dstr` with no children and no expression range, so `add_offense` raises a `TypeError`
20
+ // the runner swallows and the cop reports nothing. Silence is what upstream does here.
21
+ if has_empty_delimiter(node, context) {
22
+ continue;
23
+ }
18
24
  let Some(body) = heredoc_body(node, context) else {
19
25
  continue;
20
26
  };
21
- let Some(terminator) = super::nodes::children(body)
27
+ let Some(terminator) = super::nodes::children_in(body, context)
22
28
  .into_iter()
23
29
  .find(|child| child.kind_str() == "heredoc_end")
24
30
  else {
@@ -49,6 +55,13 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
49
55
  }
50
56
  }
51
57
 
58
+ /// Whether the opener names its terminator with an empty quoted string, as `<<''` does.
59
+ fn has_empty_delimiter(node: Node<'_>, context: &RuleContext<'_>) -> bool {
60
+ let text = context.source.node_text(node);
61
+ let delimiter = text.trim_start_matches('<').trim_start_matches(['~', '-']);
62
+ matches!(delimiter, "''" | "\"\"")
63
+ }
64
+
52
65
  /// `node.loc.heredoc_body.source.empty?`.
53
66
  ///
54
67
  /// The grammar's body starts where the opener was written rather than on the line below, and it runs
@@ -10,7 +10,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
10
10
  continue;
11
11
  };
12
12
  // `empty_and_without_delimiters?`: parentheses were written around nothing.
13
- if !super::nodes::children(parameters).is_empty() {
13
+ if !super::nodes::children_in(parameters, context).is_empty() {
14
14
  continue;
15
15
  }
16
16
  // `send_node.source_range.end_pos`: the end of the `->` the parameters follow.
@@ -73,7 +73,7 @@ impl Literal {
73
73
  match node.kind_str() {
74
74
  // `Array[]` and `Hash[]`, which upstream reads as a call to `:[]` with no arguments.
75
75
  "element_reference" => {
76
- if super::nodes::children(node).len() != 1 {
76
+ if super::nodes::children_in(node, context).len() != 1 {
77
77
  return None;
78
78
  }
79
79
  let object = node.field("object")?;
@@ -110,6 +110,13 @@ impl Literal {
110
110
  if literal != Self::String && node.field("block").is_some() {
111
111
  return None;
112
112
  }
113
+ // **`hash_with_block(node.parent)` reaches past the call's own block.**
114
+ // A `Hash.new` written as the body of another `Hash.new`'s block has that
115
+ // block for a parent upstream, so the check meant for the outer call
116
+ // silences the inner one too.
117
+ if literal == Self::Hash && inside_hash_new_block(node, context) {
118
+ return None;
119
+ }
113
120
  Some(literal)
114
121
  }
115
122
  // `Array([])` and `Hash([])`, the conversion functions.
@@ -135,6 +142,31 @@ impl Literal {
135
142
  }
136
143
 
137
144
  /// `(const {nil? cbase} :Array)` and its `Hash` and `String` counterparts.
145
+ /// Whether the call stands as the body of a `Hash.new { ... }` block, which is what
146
+ /// `hash_with_block(node.parent)` answers to for a nested call.
147
+ fn inside_hash_new_block(node: Node<'_>, context: &RuleContext<'_>) -> bool {
148
+ let Some(body) = node.parent_of(context) else {
149
+ return false;
150
+ };
151
+ let Some(block) = body
152
+ .parent_of(context)
153
+ .filter(|parent| matches!(parent.kind_str(), "block" | "do_block"))
154
+ .or_else(|| matches!(body.kind_str(), "block" | "do_block").then_some(body))
155
+ else {
156
+ return false;
157
+ };
158
+ block
159
+ .parent_of(context)
160
+ .filter(|call| call.kind_str() == "call")
161
+ .is_some_and(|call| {
162
+ call.field("method")
163
+ .is_some_and(|method| context.source.node_text(method) == "new")
164
+ && call
165
+ .field("receiver")
166
+ .is_some_and(|receiver| context.source.node_text(receiver) == "Hash")
167
+ })
168
+ }
169
+
138
170
  fn named_constant(node: Node<'_>, context: &RuleContext<'_>) -> Option<Literal> {
139
171
  for (name, literal) in [
140
172
  ("Array", Literal::Array),
@@ -150,7 +182,7 @@ fn named_constant(node: Node<'_>, context: &RuleContext<'_>) -> Option<Literal>
150
182
 
151
183
  fn is_empty_array(node: Node<'_>, context: &RuleContext<'_>) -> bool {
152
184
  node.kind_str() == "array"
153
- && super::nodes::children(node).is_empty()
185
+ && super::nodes::children_in(node, context).is_empty()
154
186
  && context.source.node_text(node).starts_with('[')
155
187
  }
156
188
 
@@ -89,7 +89,7 @@ fn parameters(context: &RuleContext<'_>, node: Node<'_>) -> String {
89
89
  let Some(list) = node.field("parameters") else {
90
90
  return String::new();
91
91
  };
92
- let sources: Vec<&str> = super::nodes::children(list)
92
+ let sources: Vec<&str> = super::nodes::children_in(list, context)
93
93
  .into_iter()
94
94
  .map(|parameter| context.source.node_text(parameter))
95
95
  .collect();
@@ -14,7 +14,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
14
14
  .setting::<String>("EnforcedStyle")
15
15
  .is_some_and(|style| style == "ternary");
16
16
  for node in context.nodes_of("interpolation") {
17
- for child in super::nodes::children(node) {
17
+ for child in super::nodes::children_in(node, context) {
18
18
  if ternary_style {
19
19
  ternary_correction(context, node, child, offenses);
20
20
  } else {
@@ -37,10 +37,18 @@ pub(super) fn receiver(context: &RuleContext<'_>, node: Node<'_>) -> String {
37
37
  format!("{}{operator}", context.source.node_text(object))
38
38
  }
39
39
 
40
- /// `arguments`: the parameter list as it was written, parentheses and all.
40
+ /// `arguments`: `node.arguments.any? ? node.arguments.source : ''`.
41
+ ///
42
+ /// Upstream asks the list what it holds, not whether it was written, so an empty `()` is dropped:
43
+ /// `def foo() = x` rewrites to `def foo`, not `def foo()`.
41
44
  pub(super) fn arguments<'a>(context: &'a RuleContext<'_>, node: Node<'_>) -> &'a str {
42
- node.field("parameters")
43
- .map_or("", |parameters| context.source.node_text(parameters))
45
+ let Some(parameters) = node.field("parameters") else {
46
+ return "";
47
+ };
48
+ match super::nodes::children_in(parameters, context).is_empty() {
49
+ true => "",
50
+ false => context.source.node_text(parameters),
51
+ }
44
52
  }
45
53
 
46
54
  /// `configured_indentation_width`.
@@ -24,7 +24,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
24
24
  if is_assignment_target(node, context) {
25
25
  continue;
26
26
  }
27
- let parts = super::nodes::children(node);
27
+ let parts = super::nodes::children_in(node, context);
28
28
  let [object, index] = parts.as_slice() else {
29
29
  continue;
30
30
  };
@@ -354,6 +354,12 @@ fn arguments<'tree>(call: Node<'tree>) -> Vec<Arg<'tree>> {
354
354
  let mut out = Vec::new();
355
355
  for argument in send_node::arguments(call) {
356
356
  let (node, range) = (argument.first(), argument.range());
357
+ // **A heredoc's body is written inside the argument list here.** Upstream keeps it under
358
+ // the opening marker, so the body is no argument of its own -- reading it as the last one
359
+ // made `__LINE__ + 1` invisible and quoted the whole body back at the user.
360
+ if node.kind_str() == "heredoc_body" {
361
+ continue;
362
+ }
357
363
  let folded = (argument.parts().len() == 1 && node.kind_str() == "assignment")
358
364
  .then(|| node.field("left"))
359
365
  .flatten()
@@ -21,7 +21,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
21
21
  };
22
22
  // `{(send $_ :% (int 2)) (begin (send $_ :% (int 2)))}`.
23
23
  let modulo = match left.kind_str() {
24
- "parenthesized_statements" => match super::nodes::children(left).as_slice() {
24
+ "parenthesized_statements" => match super::nodes::children_in(left, context).as_slice() {
25
25
  [only] => *only,
26
26
  _ => continue,
27
27
  },
@@ -83,7 +83,7 @@ fn match_call<'tree>(
83
83
  /// sequences, so what is left is checking that the run between them holds nothing a regexp engine
84
84
  /// would read as syntax, and that no flag was written after the closing delimiter.
85
85
  fn exact_match_text<'a>(node: Node<'_>, context: &'a RuleContext<'_>) -> Option<&'a str> {
86
- let parts = super::nodes::children(node);
86
+ let parts = super::nodes::children_in(node, context);
87
87
  let [start, literal, end] = parts.as_slice() else {
88
88
  return None;
89
89
  };
@@ -77,7 +77,7 @@ fn report_file(
77
77
  if context.source.node_text(default_dir) != "__FILE__"
78
78
  || path.kind_str() != "string"
79
79
  || path.start_position().row != path.end_position().row
80
- || super::nodes::children(path)
80
+ || super::nodes::children_in(path, context)
81
81
  .iter()
82
82
  .any(|child| child.kind_str() == "interpolation")
83
83
  {
@@ -158,7 +158,7 @@ fn file_expand_path<'tree>(
158
158
  /// `Pathname(x).parent.expand_path` and `Pathname.new(x).parent.expand_path`: the `.parent` call,
159
159
  /// the argument, and which of the two spellings it was.
160
160
  fn pathname_parent<'tree>(
161
- context: &RuleContext<'_>,
161
+ context: &'tree RuleContext<'_>,
162
162
  node: Node<'tree>,
163
163
  ) -> Option<(Node<'tree>, Node<'tree>, bool)> {
164
164
  if node.field("arguments").is_some() {
@@ -178,7 +178,7 @@ fn pathname_parent<'tree>(
178
178
  return None;
179
179
  }
180
180
  let arguments = call.field("arguments")?;
181
- let [argument] = super::nodes::children(arguments)[..] else {
181
+ let [argument] = super::nodes::children_in(arguments, context)[..] else {
182
182
  return None;
183
183
  };
184
184
  let method = call.field("method")?;
@@ -23,7 +23,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
23
23
  .map(super::nodes::children)
24
24
  .unwrap_or_default();
25
25
  // The grammar hangs a `yield`'s arguments off it without a field name.
26
- let yield_arguments = super::nodes::children(node)
26
+ let yield_arguments = super::nodes::children_in(node, context)
27
27
  .into_iter()
28
28
  .find(|child| child.kind_str() == "argument_list")
29
29
  .map(super::nodes::children)
@@ -92,7 +92,7 @@ fn yielding_block<'tree>(node: Node<'tree>, context: &RuleContext<'_>) -> Option
92
92
  if !matches!(body.kind_str(), "block_body" | "body_statement") {
93
93
  return None;
94
94
  }
95
- match super::nodes::children(body).as_slice() {
95
+ match super::nodes::children_in(body, context).as_slice() {
96
96
  [only] if only.id() == node.id() => {}
97
97
  _ => return None,
98
98
  }
@@ -193,7 +193,7 @@ fn zsuper_arguments(
193
193
  if call.field("method")?.kind_str() != "super" {
194
194
  return None;
195
195
  }
196
- let parameters = super::nodes::children(definition.field("parameters")?);
196
+ let parameters = super::nodes::children_in(definition.field("parameters")?, context);
197
197
  let written: Vec<String> = parameters
198
198
  .into_iter()
199
199
  .map(|parameter| match parameter.kind_str() {
@@ -19,6 +19,11 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
19
19
  {
20
20
  continue;
21
21
  }
22
+ // `on_send` is never called for a `csend`, and this cop does not alias `on_csend`, so
23
+ // `File&.open` is not its business. The grammar spells both as `call`.
24
+ if !crate::rules::send_node::is_plain_send(node, context) {
25
+ continue;
26
+ }
22
27
  // `node.block_argument?`: `File.open(path, &:read)` hands the descriptor to something that
23
28
  // closes it.
24
29
  if arguments(node)
@@ -53,7 +53,7 @@ pub(super) fn parse<'a>(
53
53
  if node.kind_str() != "block_argument" {
54
54
  return None;
55
55
  }
56
- let symbol = super::nodes::children(node);
56
+ let symbol = super::nodes::children_in(node, context);
57
57
  let [symbol] = symbol.as_slice() else {
58
58
  return None;
59
59
  };
@@ -77,14 +77,14 @@ pub(super) fn block_calls<'tree>(
77
77
  arity: usize,
78
78
  ) -> Option<Vec<Range<usize>>> {
79
79
  let block = node.field("block")?;
80
- let parameters = super::nodes::children(block.field("parameters")?);
80
+ let parameters = super::nodes::children_in(block.field("parameters")?, context);
81
81
  let [parameter] = parameters.as_slice() else {
82
82
  return None;
83
83
  };
84
84
  if parameter.kind_str() != "identifier" {
85
85
  return None;
86
86
  }
87
- let body = super::nodes::children(block.field("body")?);
87
+ let body = super::nodes::children_in(block.field("body")?, context);
88
88
  let [statement] = body.as_slice() else {
89
89
  return None;
90
90
  };
@@ -109,7 +109,7 @@ fn removed_heredocs(
109
109
  let Some(body) = crate::rules::send_node::heredoc_body(opener, context) else {
110
110
  continue;
111
111
  };
112
- let parts = super::nodes::children(body);
112
+ let parts = super::nodes::children_in(body, context);
113
113
  let Some(terminator) = parts.iter().find(|part| part.kind_str() == "heredoc_end") else {
114
114
  continue;
115
115
  };
@@ -33,7 +33,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
33
33
  fn for_to_each(context: &RuleContext<'_>, node: Node<'_>) -> Option<Offense> {
34
34
  let variable = node.field("pattern")?;
35
35
  let value = node.field("value")?;
36
- let collection = *super::nodes::children(value).first()?;
36
+ let collection = *super::nodes::children_in(value, context).first()?;
37
37
  let body = node.field("body")?;
38
38
  // `for_node.do?`: the head ends at the `do` when there is one, and at the collection otherwise.
39
39
  let end = match body.child(0).filter(|first| first.kind_str() == "do") {
@@ -24,6 +24,9 @@ pub(super) struct Sequence {
24
24
  pub flags: String,
25
25
  pub width: String,
26
26
  pub precision: String,
27
+ /// Whether a `.` was written at all. `%.d` is a precision of **zero**, which prints no digits,
28
+ /// and an empty `precision` cannot tell that apart from a sequence with no precision.
29
+ pub has_precision: bool,
27
30
  pub name: Option<String>,
28
31
  /// The type character, absent from a template sequence.
29
32
  pub kind: Option<char>,
@@ -65,6 +68,7 @@ fn match_at(text: &str, bytes: &[u8], start: usize) -> Option<Sequence> {
65
68
  flags: String::new(),
66
69
  width: String::new(),
67
70
  precision: String::new(),
71
+ has_precision: false,
68
72
  name: None,
69
73
  kind: Some('%'),
70
74
  style: SequenceStyle::Percent,
@@ -204,12 +208,14 @@ fn build(
204
208
  Some(_) if source.contains('{') => SequenceStyle::Template,
205
209
  _ => SequenceStyle::Unannotated,
206
210
  };
211
+ let has_precision = source.contains('.');
207
212
  Sequence {
208
213
  begin,
209
214
  end,
210
215
  flags,
211
216
  width,
212
217
  precision,
218
+ has_precision,
213
219
  name,
214
220
  kind,
215
221
  style,
@@ -6,8 +6,8 @@ use tree_sitter::Node;
6
6
 
7
7
  use crate::diagnostic::{Edit, Offense};
8
8
  use crate::rules::RuleContext;
9
- use crate::rules::send_node::{Argument, arguments, is_plain_send};
10
9
  use crate::rules::node_ext::NodeExt;
10
+ use crate::rules::send_node::{Argument, arguments, is_plain_send};
11
11
 
12
12
  /// Literal kinds a leading sign belongs to rather than turning into a `:-@` call.
13
13
  const NUMBER_KINDS: &[&str] = &["integer", "float", "rational", "complex"];
@@ -68,6 +68,8 @@ struct Formatter<'tree> {
68
68
  receiver: Option<Range<usize>>,
69
69
  /// `node.first_argument`, which is all either correction reads.
70
70
  argument: Option<Operand<'tree>>,
71
+ /// `node.arguments` of a `format` / `sprintf` call, which only the `percent` correction reads.
72
+ written: Vec<Argument<'tree>>,
71
73
  }
72
74
 
73
75
  impl<'tree> Formatter<'tree> {
@@ -99,6 +101,7 @@ impl<'tree> Formatter<'tree> {
99
101
  span: node.byte_range(),
100
102
  receiver: Some(left.byte_range()),
101
103
  argument: Some(Operand::Node(right)),
104
+ written: Vec::new(),
102
105
  })
103
106
  }
104
107
 
@@ -125,6 +128,7 @@ impl<'tree> Formatter<'tree> {
125
128
  span: node.byte_range(),
126
129
  receiver: Some(first.byte_range()),
127
130
  argument: Some(Operand::Text(second.start_byte() + 1..second.end_byte())),
131
+ written: Vec::new(),
128
132
  })
129
133
  }
130
134
 
@@ -148,6 +152,7 @@ impl<'tree> Formatter<'tree> {
148
152
  span: node.byte_range(),
149
153
  receiver: Some(receiver.byte_range()),
150
154
  argument: first.map(Operand::Node),
155
+ written: Vec::new(),
151
156
  });
152
157
  }
153
158
  // `(send nil? ${:sprintf :format} _ _ ...)`: no receiver and at least two arguments.
@@ -157,7 +162,8 @@ impl<'tree> Formatter<'tree> {
157
162
  {
158
163
  return None;
159
164
  }
160
- if arguments(node).len() < 2 {
165
+ let written = arguments(node);
166
+ if written.len() < 2 {
161
167
  return None;
162
168
  }
163
169
  Some(Self {
@@ -166,15 +172,14 @@ impl<'tree> Formatter<'tree> {
166
172
  span: node.byte_range(),
167
173
  receiver: None,
168
174
  argument: None,
175
+ written,
169
176
  })
170
177
  }
171
178
  }
172
179
 
173
180
  fn correction(context: &RuleContext<'_>, found: &Formatter<'_>, style: &str) -> Option<Edit> {
174
- // `autocorrect_to_percent` is only reachable with `EnforcedStyle: percent`, which no corpus
175
- // configures; the offense is reported without a correction rather than guessed at.
176
181
  if style == "percent" {
177
- return None;
182
+ return to_percent(context, found);
178
183
  }
179
184
  if found.detected != "percent" {
180
185
  return Some(Edit {
@@ -193,7 +198,7 @@ fn correction(context: &RuleContext<'_>, found: &Formatter<'_>, style: &str) ->
193
198
  let receiver = found.receiver.clone()?;
194
199
  let args = match argument {
195
200
  Operand::Node(node) if matches!(node.kind_str(), "array" | "hash") => {
196
- super::nodes::children(*node)
201
+ super::nodes::children_in(*node, context)
197
202
  .iter()
198
203
  .map(|child| context.source.node_text(*child))
199
204
  .collect::<Vec<_>>()
@@ -213,6 +218,67 @@ fn correction(context: &RuleContext<'_>, found: &Formatter<'_>, style: &str) ->
213
218
  })
214
219
  }
215
220
 
221
+ /// `autocorrect_to_percent`: `format(fmt, a, b)` becomes `fmt % [a, b]`, and a single parameter
222
+ /// stands on its own.
223
+ fn to_percent(context: &RuleContext<'_>, found: &Formatter<'_>) -> Option<Edit> {
224
+ let (format, parameters) = found.written.split_first()?;
225
+ let args = match parameters {
226
+ // A brace-less hash is one `hash` argument upstream, and `format_single_parameter` writes
227
+ // a hash back in braces: `fmt % { a: 1, b: 2 }`.
228
+ [only] if only.parts().len() > 1 || only.first().kind_str() == "pair" => {
229
+ format!("{{ {} }}", context.source.slice(only.range()))
230
+ }
231
+ [only] => format_single_parameter(context, only.first()),
232
+ _ => format!(
233
+ "[{}]",
234
+ parameters
235
+ .iter()
236
+ .map(|argument| context.source.slice(argument.range()).to_owned())
237
+ .collect::<Vec<_>>()
238
+ .join(", ")
239
+ ),
240
+ };
241
+ Some(Edit {
242
+ start: found.span.start,
243
+ end: found.span.end,
244
+ replacement: format!("{} % {args}", context.source.slice(format.range())),
245
+ safe: true,
246
+ })
247
+ }
248
+
249
+ /// `format_single_parameter`.
250
+ fn format_single_parameter(context: &RuleContext<'_>, node: Node<'_>) -> String {
251
+ // `format(fmt, *args)` prints what `fmt % args` does, so the splat comes off.
252
+ if node.kind_str() == "splat_argument" {
253
+ return match super::nodes::children_in(node, context).first() {
254
+ Some(inner) => format_single_parameter(context, *inner),
255
+ None => context.source.node_text(node).to_owned(),
256
+ };
257
+ }
258
+ let source = context.source.node_text(node);
259
+ if node.kind_str() == "hash" {
260
+ return format!("{{ {source} }}");
261
+ }
262
+ match requires_parentheses(context, node) {
263
+ true => format!("({source})"),
264
+ false => source.to_owned(),
265
+ }
266
+ }
267
+
268
+ /// `requires_parentheses?`: anything binding looser than `%` keeps its meaning only in brackets.
269
+ fn requires_parentheses(context: &RuleContext<'_>, node: Node<'_>) -> bool {
270
+ match node.kind_str() {
271
+ "assignment" | "operator_assignment" | "if" | "unless" | "conditional" | "range" => true,
272
+ // `and` / `or` are `binary` here; so are the operator calls, which need brackets only when
273
+ // written without them -- and an operator call never is.
274
+ "binary" => node.field("operator").is_some_and(|operator| {
275
+ let text = context.source.node_text(operator);
276
+ matches!(text, "&&" | "||" | "and" | "or") || super::nodes::is_operator_method(text)
277
+ }),
278
+ _ => false,
279
+ }
280
+ }
281
+
216
282
  /// `children.map(&:source).join(', ')` for the array or hash the grammar left as text.
217
283
  fn elements_of(source: &str) -> String {
218
284
  let inner = source
@@ -278,11 +344,9 @@ fn is_variable_argument(
278
344
  .is_some_and(|operand| NUMBER_KINDS.contains(&operand.kind_str()));
279
345
  !signed_number && matches!(operator, "-" | "+" | "!" | "~" | "not")
280
346
  }
281
- "binary" => node
282
- .field("operator")
283
- .is_some_and(|operator| {
284
- super::nodes::is_operator_method(context.source.node_text(operator))
285
- }),
347
+ "binary" => node.field("operator").is_some_and(|operator| {
348
+ super::nodes::is_operator_method(context.source.node_text(operator))
349
+ }),
286
350
  _ => false,
287
351
  }
288
352
  }