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
@@ -13,6 +13,11 @@ use crate::rules::node_ext::NodeExt;
13
13
  const FORMAT_METHODS: &[&str] = &["format", "sprintf", "printf"];
14
14
 
15
15
  pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
16
+ // Every sequence this cop can report starts with `%`. Avoid collecting and splitting every
17
+ // string literal in files that cannot possibly contain one.
18
+ if !context.source.text().contains('%') {
19
+ return;
20
+ }
16
21
  let style = parse_style(
17
22
  context
18
23
  .setting::<String>("EnforcedStyle")
@@ -26,17 +31,29 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
26
31
  .setting::<String>("Mode")
27
32
  .is_some_and(|mode| mode == "conservative");
28
33
  let allowed_methods: Vec<String> = context.setting("AllowedMethods").unwrap_or_default();
34
+ // `allowed_method?(name) || matches_allowed_pattern?(name)`: both are matched against the
35
+ // **enclosing call's method name**, not against the string.
36
+ let allowed_patterns =
37
+ crate::rules::naming::support::forbidden_patterns_named(context, "AllowedPatterns");
29
38
 
30
39
  for literal in literals(context) {
31
- if surrounded_by_command_or_regexp(literal.anchor)
40
+ if surrounded_by_command_or_regexp(literal.anchor, context)
32
41
  || allowed_method(context, literal.anchor, &allowed_methods)
42
+ || allowed_call_pattern(context, literal.anchor, &allowed_patterns)
33
43
  {
34
44
  continue;
35
45
  }
36
46
  // `format_string_in_typical_context?` reads the node `on_str` was handed: for a literal the
37
47
  // parser split into parts that is the enclosing `dstr`, which is never a call's argument.
38
- let single = literal.parts.len() == 1;
39
- let typical = single && typical_context(context, literal.anchor);
48
+ // `format_string_context?` is `format_string_in_typical_context?(node) || any ancestor
49
+ // `dstr` in one`. The anchor **is** the node upstream asks about, whether the parser split
50
+ // it into parts or not -- requiring a single part made every interpolated format string
51
+ // uncorrectable, and `format("c#{b}%{template}")` was reported and then left alone.
52
+ let typical = typical_context(context, literal.anchor);
53
+ // Whether the parser cut the literal into parts, which is what puts a `dstr` between each
54
+ // part and the call.
55
+ let split = literal.parts.len() > 1
56
+ || crate::rules::send_node::has_interpolation(literal.anchor);
40
57
  let correctable = typical || enclosing_typical_context(context, literal.anchor);
41
58
 
42
59
  for part in &literal.parts {
@@ -50,8 +67,13 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
50
67
  .filter(|sequence| {
51
68
  // `allowed_string?`: an unannotated token outside a format call is left alone,
52
69
  // and `conservative` mode extends that to every token.
70
+ //
71
+ // **The node `on_str` is handed is the part, not the literal.** A `"#{x} %s"`
72
+ // splits into a `dstr` of parts, and a part's parent is that `dstr` rather
73
+ // than the call -- so `format_string_in_typical_context?` is false there and
74
+ // every unannotated token in an interpolated format string is allowed.
53
75
  let allowed = sequence.style == SequenceStyle::Unannotated || conservative;
54
- !(allowed && !typical)
76
+ !(allowed && !(typical && !split))
55
77
  })
56
78
  .collect();
57
79
  if detected.is_empty() || allowed_unannotated(&detected, style, max_unannotated) {
@@ -84,7 +106,7 @@ fn literals<'a>(context: &'a RuleContext<'_>) -> Vec<Literal<'a>> {
84
106
  // `"%s" %[a]` is a `send` upstream: the second literal is the operator's argument, not a
85
107
  // string at all.
86
108
  if open.id() == close.id()
87
- || (super::percent::is_modulo_operand(node)
109
+ || (super::percent::is_modulo_operand(node, context)
88
110
  && context.source.node_text(node).starts_with('%'))
89
111
  {
90
112
  continue;
@@ -203,14 +225,17 @@ fn typical_context(context: &RuleContext<'_>, node: Node<'_>) -> bool {
203
225
  if parent.kind_str() != "argument_list" {
204
226
  return false;
205
227
  }
206
- let Some(call) = parent.parent_of(context).filter(|call| call.kind_str() == "call") else {
228
+ let Some(call) = parent
229
+ .parent_of(context)
230
+ .filter(|call| call.kind_str() == "call")
231
+ else {
207
232
  return false;
208
233
  };
209
234
  let named = call
210
235
  .field("method")
211
236
  .is_some_and(|method| FORMAT_METHODS.contains(&context.source.node_text(method)));
212
237
  named
213
- && super::nodes::children(parent)
238
+ && super::nodes::children_in(parent, context)
214
239
  .first()
215
240
  .is_some_and(|first| first.id() == node.id())
216
241
  }
@@ -220,8 +245,10 @@ fn typical_context(context: &RuleContext<'_>, node: Node<'_>) -> bool {
220
245
  fn enclosing_typical_context(context: &RuleContext<'_>, node: Node<'_>) -> bool {
221
246
  let mut current = node.parent_of(context);
222
247
  while let Some(parent) = current {
223
- if matches!(parent.kind_str(), "chained_string" | "string" | "heredoc_body")
224
- && typical_context(context, parent)
248
+ if matches!(
249
+ parent.kind_str(),
250
+ "chained_string" | "string" | "heredoc_body"
251
+ ) && typical_context(context, parent)
225
252
  {
226
253
  return true;
227
254
  }
@@ -232,13 +259,13 @@ fn enclosing_typical_context(context: &RuleContext<'_>, node: Node<'_>) -> bool
232
259
 
233
260
  /// `format_string_token?`: a literal written inside a command or a regexp is none of this cop's
234
261
  /// business.
235
- fn surrounded_by_command_or_regexp(node: Node<'_>) -> bool {
236
- let mut current = node.parent();
262
+ fn surrounded_by_command_or_regexp(node: Node<'_>, context: &RuleContext<'_>) -> bool {
263
+ let mut current = context.parent(node);
237
264
  while let Some(parent) = current {
238
265
  if matches!(parent.kind_str(), "subshell" | "regex") {
239
266
  return true;
240
267
  }
241
- current = parent.parent();
268
+ current = context.parent(parent);
242
269
  }
243
270
  false
244
271
  }
@@ -355,3 +382,25 @@ fn describe(style: SequenceStyle) -> &'static str {
355
382
  _ => "unannotated tokens (like `%s`)",
356
383
  }
357
384
  }
385
+
386
+ /// `matches_allowed_pattern?(send_parent.method_name)`.
387
+ fn allowed_call_pattern(
388
+ context: &RuleContext<'_>,
389
+ anchor: Node<'_>,
390
+ patterns: &[&regex::Regex],
391
+ ) -> bool {
392
+ if patterns.is_empty() {
393
+ return false;
394
+ }
395
+ let mut current = anchor.parent_of(context);
396
+ while let Some(node) = current {
397
+ if node.kind_str() == "call"
398
+ && let Some(method) = node.field("method")
399
+ {
400
+ let name = context.source.node_text(method);
401
+ return patterns.iter().any(|pattern| pattern.is_match(name));
402
+ }
403
+ current = node.parent_of(context);
404
+ }
405
+ false
406
+ }
@@ -2,6 +2,7 @@ use std::ops::Range;
2
2
 
3
3
  use crate::diagnostic::{Edit, Offense};
4
4
  use crate::magic_comment::MagicComment;
5
+ use crate::ruby_version::RubyVersion;
5
6
  use crate::rules::RuleContext;
6
7
 
7
8
  const MSG_MISSING: &str = "Missing frozen string literal comment.";
@@ -9,8 +10,16 @@ const MSG_MISSING_TRUE: &str = "Missing magic comment `# frozen_string_literal:
9
10
  const MSG_UNNECESSARY: &str = "Unnecessary frozen string literal comment.";
10
11
  const MSG_DISABLED: &str = "Frozen string literal comment must be set to `true`.";
11
12
 
13
+ /// `minimum_target_ruby_version 2.3`: the frozen string literal comment is only valid in Ruby 2.3+.
14
+ const MINIMUM: RubyVersion = RubyVersion::new(2, 3);
15
+
12
16
  pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
13
- if context.source.text().trim().is_empty() {
17
+ if context.target_ruby_version() < MINIMUM {
18
+ return;
19
+ }
20
+ // `processed_source.tokens.empty?`: a file of nothing but a data section has no tokens at
21
+ // all, and the cop returns before it looks for a comment.
22
+ if crate::rules::support::code_before_data(context).trim().is_empty() {
14
23
  return;
15
24
  }
16
25
  let style: String = context
@@ -19,6 +28,15 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
19
28
 
20
29
  match style.as_str() {
21
30
  "never" => {
31
+ // `ensure_no_comment` asks `frozen_string_literal_comment_exists?`, which reads only
32
+ // the **leading** comments -- a magic comment written after code does not reach Ruby,
33
+ // so there is nothing unnecessary to remove. The offense itself is then placed on the
34
+ // comment found anywhere, which is what `specified_comment` returns.
35
+ let leading = leading_comments(context)
36
+ .any(|(_, line)| MagicComment::parse(line).valid_literal_value());
37
+ if !leading {
38
+ return;
39
+ }
22
40
  if let Some(comment) = specified_comment(context) {
23
41
  offenses.push(
24
42
  context
@@ -44,7 +62,10 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
44
62
  .corrected_by(Edit {
45
63
  start: comment.start,
46
64
  end: comment.end,
47
- replacement: "# frozen_string_literal: true".to_owned(),
65
+ // `MagicComment#new_frozen_string_literal(true)` keeps the comment's
66
+ // own shape -- `# -*- frozen_string_literal: false -*-` becomes the
67
+ // same line with `true`, not a plain one.
68
+ replacement: enabled_form(context.source.slice(comment.clone())),
48
69
  safe: false,
49
70
  }),
50
71
  );
@@ -151,9 +172,21 @@ fn line_content_end(context: &RuleContext<'_>, line_number: usize) -> usize {
151
172
  /// Removes the comment along with the line it sits on, so no blank line is left behind.
152
173
  fn remove_comment(context: &RuleContext<'_>, comment: Range<usize>) -> Edit {
153
174
  let line_number = context.source.line_column(comment.start).0;
175
+ let start = context.source.line_start(line_number);
176
+ // `range_with_surrounding_space(node.pos, side: :right)`: the blank lines the comment left
177
+ // behind go with it, so removing it does not open a gap at the head of the file.
178
+ let text = context.source.text();
179
+ let mut end = context.source.line_range(line_number).end;
180
+ while text[end..]
181
+ .chars()
182
+ .next()
183
+ .is_some_and(|character| character.is_whitespace())
184
+ {
185
+ end += text[end..].chars().next().map_or(0, char::len_utf8);
186
+ }
154
187
  Edit {
155
- start: context.source.line_start(line_number),
156
- end: context.source.line_range(line_number).end,
188
+ start,
189
+ end,
157
190
  replacement: String::new(),
158
191
  safe: false,
159
192
  }
@@ -187,3 +220,28 @@ fn specified_comment(context: &RuleContext<'_>) -> Option<Range<usize>> {
187
220
  Some(start..start + trimmed.len())
188
221
  })
189
222
  }
223
+
224
+ /// `MagicComment#new_frozen_string_literal(true)`: the value changes and nothing else does.
225
+ fn enabled_form(comment: &str) -> String {
226
+ let mut out = String::with_capacity(comment.len());
227
+ let mut rest = comment;
228
+ while let Some(index) = rest.find("frozen_string_literal") {
229
+ let (before, after) = rest.split_at(index + "frozen_string_literal".len());
230
+ out.push_str(before);
231
+ let value_start = after
232
+ .find(|character: char| character != ':' && character != ' ' && character != '=')
233
+ .unwrap_or(after.len());
234
+ out.push_str(&after[..value_start]);
235
+ let tail = &after[value_start..];
236
+ let value_end = tail
237
+ .find(|character: char| !character.is_ascii_alphanumeric())
238
+ .unwrap_or(tail.len());
239
+ out.push_str("true");
240
+ rest = &tail[value_end..];
241
+ }
242
+ out.push_str(rest);
243
+ match out.is_empty() {
244
+ true => "# frozen_string_literal: true".to_owned(),
245
+ false => out,
246
+ }
247
+ }
@@ -11,6 +11,7 @@ use super::conditional::{
11
11
  use crate::diagnostic::{Edit, Offense};
12
12
  use crate::rules::RuleContext;
13
13
  use crate::rules::node_ext::NodeExt;
14
+ use crate::rules::send_node::named_children_of;
14
15
 
15
16
  const MSG: &str = "Use a guard clause (`%<example>s`) instead of wrapping the code inside a \
16
17
  conditional expression.";
@@ -331,7 +332,7 @@ impl Cop<'_, '_> {
331
332
  /// The span of one branch, which is the whole statement list when it holds more than one.
332
333
  fn branch_range(&self, node: Node<'_>, field: &str) -> Option<Range<usize>> {
333
334
  let clause = node.field(field)?;
334
- let statements = super::nodes::children(clause);
335
+ let statements = super::nodes::children_in(clause, self.context);
335
336
  let first = statements.first()?;
336
337
  let last = statements.last()?;
337
338
  Some(first.start_byte()..last.end_byte())
@@ -421,7 +422,7 @@ impl Cop<'_, '_> {
421
422
  /// *descendant* of the condition holds, while `if x, y = foo` writes two.
422
423
  fn assigned_locals(&self, condition: Node<'_>) -> Vec<String> {
423
424
  let mut names = Vec::new();
424
- for inner in descendants(condition) {
425
+ for inner in descendants(condition, self.context) {
425
426
  if !matches!(inner.kind_str(), "assignment" | "operator_assignment") {
426
427
  continue;
427
428
  }
@@ -454,13 +455,13 @@ impl Cop<'_, '_> {
454
455
  let body = body_of(consequence);
455
456
  let roots: Vec<Node<'_>> = match &body {
456
457
  Body::Missing => return Vec::new(),
457
- Body::One(node) => super::nodes::children(*node),
458
+ Body::One(node) => super::nodes::children_in(*node, self.context),
458
459
  Body::Begin(statements) => statements.clone(),
459
460
  };
460
461
  let mut names = Vec::new();
461
462
  let mut pending: Vec<Node<'_>> = roots;
462
463
  while let Some(root) = pending.pop() {
463
- for inner in descendants(root) {
464
+ for inner in descendants(root, self.context) {
464
465
  if inner.kind_str() == "heredoc_beginning"
465
466
  && let Some(body) = self.heredoc_body(inner)
466
467
  {
@@ -602,9 +603,9 @@ fn heredoc_end(context: &RuleContext<'_>, beginning: Node<'_>) -> Option<Range<u
602
603
  .nodes_of("heredoc_beginning")
603
604
  .position(|node| node.id() == beginning.id())?;
604
605
  let body = context.nodes_of("heredoc_body").nth(index)?;
605
- let mut cursor = body.walk();
606
- let terminator = body
607
- .named_children(&mut cursor)
606
+ let _cursor = body.walk();
607
+ let terminator = named_children_of(body, context)
608
+ .into_iter()
608
609
  .find(|child| child.kind_str() == "heredoc_end")?;
609
610
  Some(terminator.byte_range())
610
611
  }
@@ -142,8 +142,8 @@ fn trailing_comma(context: &RuleContext<'_>, end: usize) -> Option<usize> {
142
142
  }
143
143
 
144
144
  /// `array.values`: the elements, with the trailing key-value pairs folded into one hash.
145
- fn values<'tree>(context: &RuleContext<'_>, array: Node<'tree>) -> Vec<Value<'tree>> {
146
- let children = super::nodes::children(array);
145
+ fn values<'tree>(context: &'tree RuleContext<'_>, array: Node<'tree>) -> Vec<Value<'tree>> {
146
+ let children = super::nodes::children_in(array, context);
147
147
  let split = children.iter().position(|child| is_pair(*child));
148
148
  let (elements, pairs) = match split {
149
149
  Some(index) => children.split_at(index),
@@ -158,10 +158,10 @@ fn values<'tree>(context: &RuleContext<'_>, array: Node<'tree>) -> Vec<Value<'tr
158
158
  is_hash: element.kind_str() == "hash",
159
159
  braces: element.kind_str() == "hash",
160
160
  opens_with_splat: element.kind_str() == "hash"
161
- && super::nodes::children(*element)
161
+ && super::nodes::children_in(*element, context)
162
162
  .first()
163
163
  .is_some_and(|first| first.kind_str() == "hash_splat_argument"),
164
- empty: element.kind_str() == "hash" && super::nodes::children(*element).is_empty(),
164
+ empty: element.kind_str() == "hash" && super::nodes::children_in(*element, context).is_empty(),
165
165
  })
166
166
  .collect();
167
167
  let _ = context;
@@ -200,7 +200,7 @@ fn on_block_pass(context: &RuleContext<'_>, offenses: &mut Vec<Offense>, node: N
200
200
  if block_pass.kind_str() != "block_argument" {
201
201
  return;
202
202
  }
203
- let symbol = send_node::named_children(block_pass);
203
+ let symbol = send_node::named_children_of(block_pass, context);
204
204
  if !matches!(symbol.as_slice(), [only] if send_node::symbol_name(*only, context).is_some()) {
205
205
  return;
206
206
  }
@@ -17,7 +17,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
17
17
  .unwrap_or(3) as usize;
18
18
 
19
19
  for node in context.nodes_of("case") {
20
- let branches = super::nodes::children(node);
20
+ let branches = super::nodes::children_in(node, context);
21
21
  let whens: Vec<Node<'_>> = branches
22
22
  .iter()
23
23
  .copied()
@@ -33,7 +33,7 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
33
33
  let mut conditions = Vec::new();
34
34
  let mut bodies = Vec::new();
35
35
  for when in &whens {
36
- let children = super::nodes::children(*when);
36
+ let children = super::nodes::children_in(*when, context);
37
37
  let [pattern, body] = children.as_slice() else {
38
38
  conditions.clear();
39
39
  break;
@@ -42,8 +42,8 @@ pub(super) fn check(context: &RuleContext<'_>, offenses: &mut Vec<Offense>) {
42
42
  conditions.clear();
43
43
  break;
44
44
  }
45
- let patterns = super::nodes::children(*pattern);
46
- let statements = super::nodes::children(*body);
45
+ let patterns = super::nodes::children_in(*pattern, context);
46
+ let statements = super::nodes::children_in(*body, context);
47
47
  let ([condition], [statement]) = (patterns.as_slice(), statements.as_slice()) else {
48
48
  conditions.clear();
49
49
  break;
@@ -88,14 +88,14 @@ fn recursive_basic_literal(context: &RuleContext<'_>, node: Node<'_>) -> bool {
88
88
  .is_some_and(|operator| {
89
89
  RECURSIVE_METHODS.contains(&context.source.node_text(operator))
90
90
  })
91
- && super::nodes::children(node)
91
+ && super::nodes::children_in(node, context)
92
92
  .into_iter()
93
93
  .all(|child| recursive_basic_literal(context, child))
94
94
  }
95
95
  "call" => {
96
96
  node.field("method")
97
97
  .is_some_and(|method| RECURSIVE_METHODS.contains(&context.source.node_text(method)))
98
- && super::nodes::children(node)
98
+ && super::nodes::children_in(node, context)
99
99
  .into_iter()
100
100
  .all(|child| recursive_basic_literal(context, child))
101
101
  }
@@ -107,10 +107,10 @@ fn recursive_basic_literal(context: &RuleContext<'_>, node: Node<'_>) -> bool {
107
107
  | "regex"
108
108
  | "subshell"
109
109
  | "boolean"
110
- | "parenthesized_statements" => super::nodes::children(node)
110
+ | "parenthesized_statements" => super::nodes::children_in(node, context)
111
111
  .into_iter()
112
112
  .all(|child| recursive_basic_literal(context, child)),
113
- "string" | "delimited_symbol" if is_interpolated(node) => super::nodes::children(node)
113
+ "string" | "delimited_symbol" if is_interpolated(node) => super::nodes::children_in(node, context)
114
114
  .into_iter()
115
115
  .all(|child| recursive_basic_literal(context, child)),
116
116
  _ => matches!(upstream_type(context, node), Some(name) if BASIC_LITERALS.contains(&name)),
@@ -53,7 +53,7 @@ fn bracket_offense(
53
53
  if node.field("block").is_some() || !send_node::is_plain_send(node, context) {
54
54
  return None;
55
55
  }
56
- let arguments = super::nodes::children(node.field("arguments")?);
56
+ let arguments = super::nodes::children_in(node.field("arguments")?, context);
57
57
  let [key] = arguments.as_slice() else {
58
58
  return None;
59
59
  };
@@ -75,10 +75,16 @@ fn bracket_offense(
75
75
 
76
76
  /// `offense_for_fetch?`: a one-argument `[]`, in either of the two spellings the grammar has for it.
77
77
  fn fetch_offense(node: Node<'_>, context: &RuleContext<'_>) -> Option<Offense> {
78
+ // `node.method?(:[])`: written on the left of an assignment the same brackets are `[]=`, which
79
+ // `fetch` has no counterpart for. The grammar spells both as an `element_reference`, so the
80
+ // parent has to be asked which one this is.
81
+ if is_assignment_target(node, context) {
82
+ return None;
83
+ }
78
84
  let (key, selector_start, safe_navigation) = match node.kind_str() {
79
85
  "element_reference" => {
80
86
  let object = node.field("object")?;
81
- let indices = super::nodes::children(node);
87
+ let indices = super::nodes::children_in(node, context);
82
88
  let [_, key] = indices.as_slice() else {
83
89
  return None;
84
90
  };
@@ -91,7 +97,7 @@ fn fetch_offense(node: Node<'_>, context: &RuleContext<'_>) -> Option<Offense> {
91
97
  if context.source.node_text(selector) != "[]" {
92
98
  return None;
93
99
  }
94
- let arguments = super::nodes::children(node.field("arguments")?);
100
+ let arguments = super::nodes::children_in(node.field("arguments")?, context);
95
101
  let [key] = arguments.as_slice() else {
96
102
  return None;
97
103
  };
@@ -142,3 +148,37 @@ fn receiver_name(receiver: Node<'_>, context: &RuleContext<'_>) -> String {
142
148
  }
143
149
  context.source.node_text(receiver).to_owned()
144
150
  }
151
+
152
+ /// Whether the node stands on the left of an assignment, where its brackets mean `[]=`.
153
+ ///
154
+ /// Only a plain assignment counts. `x[k] += 1` is `(op-asgn (send x :[] k) :+ 1)` upstream -- the
155
+ /// read is a `send :[]` of its own, and the cop reports it -- while `x[k] = 1` is a single
156
+ /// `send :[]=` with no read in it at all. A multiple assignment writes through `[]=` as well, so
157
+ /// its target list is excluded too.
158
+ fn is_assignment_target(node: Node<'_>, context: &RuleContext<'_>) -> bool {
159
+ let Some(parent) = node.parent_of(context) else {
160
+ return false;
161
+ };
162
+ match parent.kind_str() {
163
+ // The grammar also spells `value[index] =~ /pattern/` as an `assignment`: `=` is the
164
+ // assignment token and `~ /pattern/` its unary right side. With no whitespace between
165
+ // those two characters Ruby reads the pair as the match operator, so the brackets remain
166
+ // a `send :[]` upstream and must not be mistaken for an `[]=` target.
167
+ "assignment" => {
168
+ if parent.field("left") != Some(node) {
169
+ return false;
170
+ }
171
+ let match_operator = parent.field("right").is_some_and(|right| {
172
+ right.kind_str() == "unary"
173
+ && right
174
+ .field("operator")
175
+ .is_some_and(|operator| context.source.node_text(operator) == "~")
176
+ && right.start_byte() > 0
177
+ && context.source.text().as_bytes()[right.start_byte() - 1] == b'='
178
+ });
179
+ !match_operator
180
+ }
181
+ "left_assignment_list" | "rest_assignment" => true,
182
+ _ => false,
183
+ }
184
+ }
@@ -50,20 +50,26 @@ pub(super) fn check(
50
50
  if !arguments(node).is_empty() {
51
51
  continue;
52
52
  }
53
- let parameters = super::nodes::children(match block.field("parameters") {
54
- Some(parameters) => parameters,
55
- None => continue,
56
- });
53
+ let parameters = super::nodes::children_in(
54
+ match block.field("parameters") {
55
+ Some(parameters) => parameters,
56
+ None => continue,
57
+ },
58
+ context,
59
+ );
57
60
  let [key, value] = parameters.as_slice() else {
58
61
  continue;
59
62
  };
60
63
  if key.kind_str() != "identifier" || value.kind_str() != "identifier" {
61
64
  continue;
62
65
  }
63
- let body = super::nodes::children(match block.field("body") {
64
- Some(body) => body,
65
- None => continue,
66
- });
66
+ let body = super::nodes::children_in(
67
+ match block.field("body") {
68
+ Some(body) => body,
69
+ None => continue,
70
+ },
71
+ context,
72
+ );
67
73
  let [statement] = body.as_slice() else {
68
74
  continue;
69
75
  };
@@ -221,13 +227,13 @@ fn semantically_except(selector: &str, test: &Test<'_>) -> bool {
221
227
  /// `except_key_source`: the keys as the replacement writes them out.
222
228
  fn key_source(key: Node<'_>, context: &RuleContext<'_>) -> String {
223
229
  match key.kind_str() {
224
- "array" => super::nodes::children(key)
230
+ "array" => super::nodes::children_in(key, context)
225
231
  .iter()
226
232
  .map(|value| context.source.node_text(*value).to_owned())
227
233
  .collect::<Vec<_>>()
228
234
  .join(", "),
229
235
  // `percent_literal?`: the elements are written bare, so each is spelled back as a literal.
230
- "string_array" | "symbol_array" => super::nodes::children(key)
236
+ "string_array" | "symbol_array" => super::nodes::children_in(key, context)
231
237
  .iter()
232
238
  .map(|value| decorate_source(*value, context))
233
239
  .collect::<Vec<_>>()