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
@@ -1,4 +1,5 @@
1
- use std::collections::HashSet;
1
+ use std::collections::{HashMap, HashSet};
2
+ use std::fmt::Display;
2
3
  use std::fs;
3
4
  use std::path::{Path, PathBuf};
4
5
  use std::process::Command;
@@ -7,17 +8,55 @@ use std::time::Duration;
7
8
  use anyhow::{Context, Result, bail};
8
9
  use serde_yaml_ng::{Mapping, Value};
9
10
 
11
+ const MAX_REMOTE_CONFIG_BYTES: u64 = 5 * 1024 * 1024;
12
+
13
+ /// Configuration files whose inheritance has already been resolved, keyed by canonical path for
14
+ /// local files and by URL for remote ones.
15
+ ///
16
+ /// `visited` cannot double as this memo: it is a recursion *stack*, popped again on the way out so
17
+ /// that a file legitimately reachable through two different branches still resolves. Without a
18
+ /// separate memo every additional mention of a file re-reads, re-parses and re-merges its whole
19
+ /// subtree, so a chain in which each level names the next one twice costs 2^depth file reads --
20
+ /// or, through `load_remote_with_inheritance`, 2^depth HTTP requests.
21
+ #[derive(Default)]
22
+ struct ResolvedConfigs {
23
+ local: HashMap<PathBuf, Value>,
24
+ remote: HashMap<String, Value>,
25
+ }
26
+
27
+ #[cfg(test)]
28
+ thread_local! {
29
+ /// Counts the configuration documents actually read and parsed, so the memoisation tests can
30
+ /// assert on reads rather than on wall-clock time. Thread-local because the test harness gives
31
+ /// every test its own thread.
32
+ static PARSED_CONFIG_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
33
+ }
34
+
10
35
  pub(super) fn load_with_inheritance(path: &Path, visited: &mut HashSet<PathBuf>) -> Result<Value> {
36
+ load_local_with_inheritance(path, visited, &mut ResolvedConfigs::default())
37
+ }
38
+
39
+ fn load_local_with_inheritance(
40
+ path: &Path,
41
+ visited: &mut HashSet<PathBuf>,
42
+ resolved: &mut ResolvedConfigs,
43
+ ) -> Result<Value> {
11
44
  let canonical = fs::canonicalize(path)
12
45
  .with_context(|| format!("configuration file not found: {}", path.display()))?;
46
+ // Only fully resolved files land in the memo, so a hit can never be a file that is still on
47
+ // the recursion stack -- genuine cycles keep reaching the `visited` check below.
48
+ if let Some(cached) = resolved.local.get(&canonical) {
49
+ return Ok(cached.clone());
50
+ }
13
51
  if !visited.insert(canonical.clone()) {
14
52
  bail!("circular inherit_from detected at {}", canonical.display());
15
53
  }
16
54
 
17
55
  let contents = fs::read_to_string(&canonical)
18
56
  .with_context(|| format!("failed to read configuration: {}", canonical.display()))?;
19
- let mut current: Value = serde_yaml_ng::from_str(&contents)
20
- .with_context(|| format!("invalid YAML in {}", canonical.display()))?;
57
+ #[cfg(test)]
58
+ PARSED_CONFIG_COUNT.with(|count| count.set(count.get() + 1));
59
+ let mut current = parse_yaml_configuration(&contents, canonical.display())?;
21
60
  let inherit = take_mapping_key(&mut current, "inherit_from");
22
61
  let inherit_gem = take_mapping_key(&mut current, "inherit_gem");
23
62
  let parent = canonical.parent().unwrap_or(Path::new("."));
@@ -28,9 +67,9 @@ pub(super) fn load_with_inheritance(path: &Path, visited: &mut HashSet<PathBuf>)
28
67
  for inherited in inherited_paths {
29
68
  if inherited.starts_with("http://") || inherited.starts_with("https://") {
30
69
  let mut remote_visited = HashSet::new();
31
- merged = merge_config(
70
+ merged = merge_inherited_config(
32
71
  merged,
33
- load_remote_with_inheritance(&inherited, &mut remote_visited)?,
72
+ load_remote_with_inheritance(&inherited, &mut remote_visited, resolved)?,
34
73
  );
35
74
  continue;
36
75
  }
@@ -40,20 +79,32 @@ pub(super) fn load_with_inheritance(path: &Path, visited: &mut HashSet<PathBuf>)
40
79
  } else {
41
80
  parent.join(inherited)
42
81
  };
43
- merged = merge_config(merged, load_with_inheritance(&inherited, visited)?);
82
+ merged = merge_inherited_config(
83
+ merged,
84
+ load_local_with_inheritance(&inherited, visited, resolved)?,
85
+ );
44
86
  }
45
87
 
46
88
  visited.remove(&canonical);
47
- Ok(merge_config(merged, current))
89
+ let config = merge_inherited_config(merged, current);
90
+ resolved.local.insert(canonical, config.clone());
91
+ Ok(config)
48
92
  }
49
93
 
50
- fn load_remote_with_inheritance(url: &str, visited: &mut HashSet<String>) -> Result<Value> {
94
+ fn load_remote_with_inheritance(
95
+ url: &str,
96
+ visited: &mut HashSet<String>,
97
+ resolved: &mut ResolvedConfigs,
98
+ ) -> Result<Value> {
99
+ // Same reasoning as the local memo, except that every avoided lookup is an avoided HTTP GET.
100
+ if let Some(cached) = resolved.remote.get(url) {
101
+ return Ok(cached.clone());
102
+ }
51
103
  if !visited.insert(url.to_owned()) {
52
104
  bail!("circular remote inherit_from detected at {url}");
53
105
  }
54
106
  let contents = fetch_remote_config(url)?;
55
- let mut current: Value =
56
- serde_yaml_ng::from_str(&contents).with_context(|| format!("invalid YAML from {url}"))?;
107
+ let mut current = parse_yaml_configuration(&contents, url)?;
57
108
  let inherit = take_mapping_key(&mut current, "inherit_from");
58
109
  let mut merged = Value::Mapping(Mapping::new());
59
110
  for inherited in parse_inherit_paths(inherit)? {
@@ -63,13 +114,36 @@ fn load_remote_with_inheritance(url: &str, visited: &mut HashSet<String>) -> Res
63
114
  } else {
64
115
  join_remote_url(url, &inherited)?
65
116
  };
66
- merged = merge_config(
117
+ merged = merge_inherited_config(
67
118
  merged,
68
- load_remote_with_inheritance(&inherited_url, visited)?,
119
+ load_remote_with_inheritance(&inherited_url, visited, resolved)?,
69
120
  );
70
121
  }
71
122
  visited.remove(url);
72
- Ok(merge_config(merged, current))
123
+ let config = merge_inherited_config(merged, current);
124
+ resolved.remote.insert(url.to_owned(), config.clone());
125
+ Ok(config)
126
+ }
127
+
128
+ /// Parses one configuration document the way `ConfigLoader#load_yaml_configuration` does.
129
+ ///
130
+ /// Upstream normalises the parse result with `hash = yaml_tree_to_hash(yaml_tree) || {}` and then
131
+ /// raises `ValidationError, "Malformed configuration in <path>"` unless what is left is a `Hash`.
132
+ /// Both halves matter. An empty `.rubocop.yml` -- the most common way of saying "the defaults are
133
+ /// fine" -- parses to `Value::Null`, and a stray scalar to `Value::String`; either one would then
134
+ /// win the non-mapping arm of `merge_config` and *replace* the whole `default.yml`-derived
135
+ /// configuration, silently re-enabling every cop and dropping `AllCops/Include` and
136
+ /// `AllCops/Exclude` (so `node_modules` and friends start getting linted).
137
+ fn parse_yaml_configuration(contents: &str, origin: impl Display) -> Result<Value> {
138
+ let value: Value =
139
+ serde_yaml_ng::from_str(contents).with_context(|| format!("invalid YAML in {origin}"))?;
140
+ match value {
141
+ // `nil` and `false` are Ruby's only falsy values, so they are exactly what `|| {}` swaps
142
+ // out for an empty hash.
143
+ Value::Null | Value::Bool(false) => Ok(Value::Mapping(Mapping::new())),
144
+ Value::Mapping(_) => Ok(value),
145
+ _ => bail!("Malformed configuration in {origin}"),
146
+ }
73
147
  }
74
148
 
75
149
  fn fetch_remote_config(url: &str) -> Result<String> {
@@ -92,27 +166,42 @@ fn fetch_remote_config(url: &str) -> Result<String> {
92
166
  response
93
167
  .body_mut()
94
168
  .with_config()
95
- .limit(5 * 1024 * 1024)
169
+ // ureq は指定値と同じ長さでも上限超過にするため、論理上限を含む 1 バイト先を渡す。
170
+ .limit(MAX_REMOTE_CONFIG_BYTES + 1)
96
171
  .read_to_string()
97
172
  .with_context(|| format!("failed to read remote configuration: {url}"))
98
173
  }
99
174
 
175
+ /// Resolves an `inherit_from` entry of a remote configuration against the URL it was found in,
176
+ /// the way RFC 3986 resolves a relative reference: the last path segment of the base is replaced,
177
+ /// and a query string or fragment of the base is dropped rather than carried over.
178
+ ///
179
+ /// Everything after the authority has to be located explicitly. Searching the whole URL for the
180
+ /// last `/` mistakes the second slash of `https://` for a directory separator when the base
181
+ /// carries no path at all -- turning `https://example.com` + `shared.yml` into
182
+ /// `https://shared.yml` -- and mistakes a slash inside a query string for one as well.
100
183
  fn join_remote_url(base: &str, relative: &str) -> Result<String> {
184
+ let scheme_end = base
185
+ .find("://")
186
+ .context("remote configuration URL has no scheme")?
187
+ + 3;
188
+ let hierarchical_end = base[scheme_end..]
189
+ .find(['?', '#'])
190
+ .map_or(base.len(), |offset| scheme_end + offset);
191
+ let hierarchical = &base[..hierarchical_end];
192
+ let authority_end = hierarchical[scheme_end..]
193
+ .find('/')
194
+ .map_or(hierarchical.len(), |offset| scheme_end + offset);
101
195
  if relative.starts_with('/') {
102
- let scheme_end = base
103
- .find("://")
104
- .context("remote configuration URL has no scheme")?
105
- + 3;
106
- let host_end = base[scheme_end..]
107
- .find('/')
108
- .map_or(base.len(), |offset| scheme_end + offset);
109
- return Ok(format!("{}{}", &base[..host_end], relative));
110
- }
111
- let directory_end = base
112
- .rfind('/')
113
- .context("remote configuration URL has no directory")?
114
- + 1;
115
- Ok(format!("{}{}", &base[..directory_end], relative))
196
+ return Ok(format!("{}{relative}", &hierarchical[..authority_end]));
197
+ }
198
+ match hierarchical[authority_end..].rfind('/') {
199
+ Some(offset) => Ok(format!(
200
+ "{}{relative}",
201
+ &hierarchical[..authority_end + offset + 1]
202
+ )),
203
+ None => Ok(format!("{hierarchical}/{relative}")),
204
+ }
116
205
  }
117
206
 
118
207
  fn parse_inherit_paths(value: Option<Value>) -> Result<Vec<String>> {
@@ -166,21 +255,47 @@ fn take_mapping_key(value: &mut Value, key: &str) -> Option<Value> {
166
255
  .remove(Value::String(key.to_owned()))
167
256
  }
168
257
 
258
+ /// Where `should_union?` gets its "root mode" from.
259
+ ///
260
+ /// RuboCop merges configurations through two entry points that answer this differently, and
261
+ /// nothing else about the two merges differs, so the whole difference lives in this choice.
262
+ enum RootMode {
263
+ /// `ConfigLoaderResolver#merge_with_default` passes `config['inherit_mode'] || {}` -- the
264
+ /// file-level directive -- unchanged for every key it merges.
265
+ File,
266
+ /// `ConfigLoaderResolver#resolve_inheritance` passes `determine_inherit_mode(hash, cop)`,
267
+ /// which is `local_inherit || hash['inherit_mode'] || {}`: a cop's own directive *replaces*
268
+ /// the file-level one, and since an empty Hash is truthy in Ruby it replaces it even when it
269
+ /// selects nothing.
270
+ PerCop,
271
+ }
272
+
273
+ /// Merges a user configuration over the `default.yml`-derived one, mirroring
274
+ /// `ConfigLoaderResolver#merge_with_default`.
169
275
  pub(super) fn merge_config(base: Value, overlay: Value) -> Value {
170
- let global_merge = inherit_merge_keys(&overlay);
276
+ merge_mappings(base, overlay, &RootMode::File)
277
+ }
278
+
279
+ /// Merges an inheriting configuration over the one it inherits from, mirroring
280
+ /// `ConfigLoaderResolver#resolve_inheritance`.
281
+ fn merge_inherited_config(base: Value, overlay: Value) -> Value {
282
+ merge_mappings(base, overlay, &RootMode::PerCop)
283
+ }
284
+
285
+ fn merge_mappings(base: Value, overlay: Value, root: &RootMode) -> Value {
171
286
  match (base, overlay) {
172
287
  (Value::Mapping(mut base), Value::Mapping(overlay)) => {
288
+ let file_mode = inherit_mode(&overlay).unwrap_or_default();
173
289
  for (key, value) in overlay {
174
- let local_merge = inherit_merge_keys(&value);
175
- let merge_keys = if local_merge.is_empty() {
176
- &global_merge
177
- } else {
178
- &local_merge
290
+ let cop_mode = match root {
291
+ RootMode::File => None,
292
+ RootMode::PerCop => value.as_mapping().and_then(inherit_mode),
179
293
  };
294
+ let mode = cop_mode.as_ref().unwrap_or(&file_mode);
180
295
  // `map_or` evaluates its default eagerly, so every key paid for a deep
181
296
  // clone even though most keys are absent from `base`.
182
297
  let merged = match base.remove(&key) {
183
- Some(old) => deep_merge(old, value, merge_keys),
298
+ Some(old) => deep_merge(old, value, mode),
184
299
  None => value,
185
300
  };
186
301
  base.insert(key, merged);
@@ -191,21 +306,25 @@ pub(super) fn merge_config(base: Value, overlay: Value) -> Value {
191
306
  }
192
307
  }
193
308
 
194
- fn deep_merge(base: Value, overlay: Value, merge_keys: &HashSet<String>) -> Value {
309
+ fn deep_merge(base: Value, overlay: Value, root_mode: &InheritMode) -> Value {
195
310
  match (base, overlay) {
196
311
  (Value::Mapping(mut base), Value::Mapping(overlay)) => {
312
+ // `should_union?` reads `inherit_mode` out of *both* hashes at the level it is
313
+ // merging, so an inherited configuration can opt its own parameters into unioning
314
+ // without the inheriting file knowing about it.
315
+ let base_mode = inherit_mode(&base);
316
+ let overlay_mode = inherit_mode(&overlay);
197
317
  for (key, value) in overlay {
198
- let should_merge_sequence =
199
- key.as_str().is_some_and(|name| merge_keys.contains(name));
200
- let merged = match (base.remove(&key), value) {
201
- (Some(Value::Sequence(mut old)), Value::Sequence(new))
202
- if should_merge_sequence =>
203
- {
204
- old.extend(new);
205
- Value::Sequence(old)
206
- }
207
- (Some(old), new) => deep_merge(old, new, merge_keys),
208
- (None, new) => new,
318
+ let merged = match base.remove(&key) {
319
+ Some(old) => merge_entry(
320
+ key.as_str(),
321
+ old,
322
+ value,
323
+ overlay_mode.as_ref(),
324
+ base_mode.as_ref(),
325
+ root_mode,
326
+ ),
327
+ None => value,
209
328
  };
210
329
  base.insert(key, merged);
211
330
  }
@@ -215,42 +334,227 @@ fn deep_merge(base: Value, overlay: Value, merge_keys: &HashSet<String>) -> Valu
215
334
  }
216
335
  }
217
336
 
218
- fn inherit_merge_keys(value: &Value) -> HashSet<String> {
219
- let Some(mode) = value
220
- .as_mapping()
221
- .and_then(|mapping| mapping.get("inherit_mode"))
222
- .and_then(Value::as_mapping)
223
- .and_then(|mapping| mapping.get("merge"))
224
- .and_then(Value::as_sequence)
225
- else {
226
- return HashSet::new();
337
+ /// Merges one key of a mapping, mirroring the `merge_hashes?` / `should_union?` / plain-overwrite
338
+ /// ladder of `ConfigLoaderResolver#merge`.
339
+ fn merge_entry(
340
+ key: Option<&str>,
341
+ base: Value,
342
+ overlay: Value,
343
+ overlay_mode: Option<&InheritMode>,
344
+ base_mode: Option<&InheritMode>,
345
+ root_mode: &InheritMode,
346
+ ) -> Value {
347
+ // `merge_hashes?` is tested before `should_union?`, so two mappings always recurse.
348
+ if base.is_mapping() && overlay.is_mapping() {
349
+ return deep_merge(base, overlay, root_mode);
350
+ }
351
+ if should_union(key, &base, &overlay, overlay_mode, base_mode, root_mode) {
352
+ return union_sequences(base, overlay);
353
+ }
354
+ overlay
355
+ }
356
+
357
+ /// Mirrors `ConfigLoaderResolver#should_union?`.
358
+ ///
359
+ /// The precedence chain is what makes `override` usable at all: the derived side is consulted
360
+ /// first and an `override` entry there stops the search with "replace", which is the only way to
361
+ /// cancel a root-level `merge`. The base side gets its turn next, so an inherited file can request
362
+ /// unioning for its own parameters, and only then does the root mode decide.
363
+ fn should_union(
364
+ key: Option<&str>,
365
+ base: &Value,
366
+ derived: &Value,
367
+ derived_mode: Option<&InheritMode>,
368
+ base_mode: Option<&InheritMode>,
369
+ root_mode: &InheritMode,
370
+ ) -> bool {
371
+ if !base.is_sequence() && !derived.is_sequence() {
372
+ return false;
373
+ }
374
+ let Some(key) = key else {
375
+ return false;
227
376
  };
228
- mode.iter()
229
- .filter_map(Value::as_str)
230
- .map(ToOwned::to_owned)
231
- .collect()
377
+ if let Some(decision) = derived_mode.and_then(|mode| mode.decide(key)) {
378
+ return decision;
379
+ }
380
+ if let Some(decision) = base_mode.and_then(|mode| mode.decide(key)) {
381
+ return decision;
382
+ }
383
+ root_mode.merge_keys.contains(key)
384
+ }
385
+
386
+ /// Mirrors `Array(base_hash[key]) | Array(derived_hash[key])`, the union `ConfigLoaderResolver#merge`
387
+ /// performs once `should_union?` agrees. Ruby's `|` keeps the first occurrence of each value, so a
388
+ /// path listed on both sides ends up in the result once.
389
+ fn union_sequences(base: Value, derived: Value) -> Value {
390
+ let mut union: Vec<Value> = Vec::new();
391
+ for item in to_sequence(base).into_iter().chain(to_sequence(derived)) {
392
+ if !union.contains(&item) {
393
+ union.push(item);
394
+ }
395
+ }
396
+ Value::Sequence(union)
397
+ }
398
+
399
+ /// Mirrors Ruby's `Array()`, which turns `nil` into `[]` and wraps anything that is not already an
400
+ /// array. `should_union?` only requires *one* of the two sides to be an array, so the other one is
401
+ /// allowed to be a bare scalar or absent.
402
+ fn to_sequence(value: Value) -> Vec<Value> {
403
+ match value {
404
+ Value::Null => Vec::new(),
405
+ Value::Sequence(items) => items,
406
+ other => vec![other],
407
+ }
408
+ }
409
+
410
+ /// The `merge` and `override` lists of one `inherit_mode` directive.
411
+ #[derive(Debug, Default)]
412
+ struct InheritMode {
413
+ merge_keys: HashSet<String>,
414
+ override_keys: HashSet<String>,
415
+ }
416
+
417
+ impl InheritMode {
418
+ /// `should_override?` and `should_merge?` collapsed into one lookup: `Some(false)` for
419
+ /// "override wins", `Some(true)` for "merge wins", and `None` when this directive says nothing
420
+ /// about the key and the next mode in `should_union?`'s chain gets its turn.
421
+ fn decide(&self, key: &str) -> Option<bool> {
422
+ if self.override_keys.contains(key) {
423
+ Some(false)
424
+ } else if self.merge_keys.contains(key) {
425
+ Some(true)
426
+ } else {
427
+ None
428
+ }
429
+ }
430
+ }
431
+
432
+ /// Reads the `inherit_mode` directive of a single configuration level.
433
+ ///
434
+ /// Returning `None` only for a missing (or explicitly null) key is what lets `RootMode::PerCop`
435
+ /// implement `local_inherit || hash['inherit_mode'] || {}` faithfully: a present but empty
436
+ /// directive is truthy in Ruby and therefore still replaces the file-level mode.
437
+ fn inherit_mode(mapping: &Mapping) -> Option<InheritMode> {
438
+ let mode = mapping.get("inherit_mode")?;
439
+ if mode.is_null() {
440
+ return None;
441
+ }
442
+ Some(InheritMode {
443
+ merge_keys: mode_keys(mode, "merge"),
444
+ override_keys: mode_keys(mode, "override"),
445
+ })
446
+ }
447
+
448
+ fn mode_keys(mode: &Value, name: &str) -> HashSet<String> {
449
+ mode.as_mapping()
450
+ .and_then(|mapping| mapping.get(name))
451
+ .and_then(Value::as_sequence)
452
+ .map(|keys| {
453
+ keys.iter()
454
+ .filter_map(Value::as_str)
455
+ .map(ToOwned::to_owned)
456
+ .collect()
457
+ })
458
+ .unwrap_or_default()
232
459
  }
233
460
 
234
461
  #[cfg(test)]
235
462
  mod tests {
463
+ use std::collections::HashSet;
236
464
  use std::fs;
465
+ use std::io::{Read, Write};
466
+ use std::net::TcpListener;
467
+ use std::path::Path;
468
+ use std::thread;
237
469
 
238
470
  use serde_yaml_ng::Value;
239
471
  use tempfile::tempdir;
240
472
 
241
- use super::{deep_merge, join_remote_url, merge_config};
473
+ use super::{
474
+ InheritMode, MAX_REMOTE_CONFIG_BYTES, PARSED_CONFIG_COUNT, deep_merge, fetch_remote_config,
475
+ join_remote_url, load_with_inheritance, merge_config, merge_inherited_config,
476
+ };
242
477
  use crate::config::Config;
243
478
 
244
479
  fn yaml(text: &str) -> Value {
245
480
  serde_yaml_ng::from_str(text).unwrap()
246
481
  }
247
482
 
483
+ /// Number of configuration documents parsed since the last call, on this thread.
484
+ fn taken_parse_count() -> usize {
485
+ PARSED_CONFIG_COUNT.with(|count| count.replace(0))
486
+ }
487
+
488
+ fn resolve(path: &Path) -> anyhow::Result<Value> {
489
+ load_with_inheritance(path, &mut HashSet::new())
490
+ }
491
+
492
+ fn serve_once(body: Vec<u8>) -> (String, thread::JoinHandle<()>) {
493
+ let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
494
+ let address = listener.local_addr().unwrap();
495
+ let server = thread::spawn(move || {
496
+ let (mut stream, _) = listener.accept().unwrap();
497
+ let mut request = [0; 1024];
498
+ assert_ne!(stream.read(&mut request).unwrap(), 0);
499
+ write!(
500
+ stream,
501
+ "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
502
+ body.len()
503
+ )
504
+ .unwrap();
505
+ stream.write_all(&body).unwrap();
506
+ });
507
+ (format!("http://{address}/config.yml"), server)
508
+ }
509
+
510
+ fn cop_exclude(merged: &Value, cop: &str) -> Value {
511
+ merged
512
+ .as_mapping()
513
+ .unwrap()
514
+ .get(cop)
515
+ .unwrap()
516
+ .as_mapping()
517
+ .unwrap()
518
+ .get("Exclude")
519
+ .unwrap()
520
+ .clone()
521
+ }
522
+
523
+ /// Writes `.rubocop.yml` plus `f0.yml`..`f{depth}.yml`, where every level names the next one
524
+ /// twice. Resolving that costs 2^depth reads without memoisation and depth + 2 with it.
525
+ fn write_diamond_chain(directory: &Path, depth: usize) {
526
+ fs::write(
527
+ directory.join(".rubocop.yml"),
528
+ "inherit_from:\n - f0.yml\n",
529
+ )
530
+ .unwrap();
531
+ for level in 0..depth {
532
+ fs::write(
533
+ directory.join(format!("f{level}.yml")),
534
+ format!(
535
+ "inherit_from:\n - f{next}.yml\n - f{next}.yml\n",
536
+ next = level + 1
537
+ ),
538
+ )
539
+ .unwrap();
540
+ }
541
+ fs::write(
542
+ directory.join(format!("f{depth}.yml")),
543
+ "Layout/LineLength:\n Max: 42\n",
544
+ )
545
+ .unwrap();
546
+ }
547
+
248
548
  #[test]
249
549
  fn joins_remote_inherit_urls() {
250
550
  assert_eq!(
251
551
  join_remote_url("https://example.com/team/base.yml", "shared.yml").unwrap(),
252
552
  "https://example.com/team/shared.yml"
253
553
  );
554
+ assert_eq!(
555
+ join_remote_url("https://example.com/team/nested/base.yml", "shared.yml").unwrap(),
556
+ "https://example.com/team/nested/shared.yml"
557
+ );
254
558
  assert_eq!(
255
559
  join_remote_url("https://example.com/team/base.yml", "/root.yml").unwrap(),
256
560
  "https://example.com/root.yml"
@@ -259,10 +563,62 @@ mod tests {
259
563
  join_remote_url("https://example.com", "/root.yml").unwrap(),
260
564
  "https://example.com/root.yml"
261
565
  );
566
+ // A base without a path must not have the `//` of its scheme mistaken for a directory.
567
+ assert_eq!(
568
+ join_remote_url("https://example.com", "shared.yml").unwrap(),
569
+ "https://example.com/shared.yml"
570
+ );
571
+ assert_eq!(
572
+ join_remote_url("https://example.com/", "shared.yml").unwrap(),
573
+ "https://example.com/shared.yml"
574
+ );
575
+ assert_eq!(
576
+ join_remote_url("https://example.com/team/", "shared.yml").unwrap(),
577
+ "https://example.com/team/shared.yml"
578
+ );
579
+ // A `/` inside a query string is not a directory separator either.
580
+ assert_eq!(
581
+ join_remote_url(
582
+ "https://example.com/team/base.yml?ref=heads/main",
583
+ "shared.yml"
584
+ )
585
+ .unwrap(),
586
+ "https://example.com/team/shared.yml"
587
+ );
588
+ assert_eq!(
589
+ join_remote_url("https://example.com?ref=heads/main", "shared.yml").unwrap(),
590
+ "https://example.com/shared.yml"
591
+ );
592
+ assert_eq!(
593
+ join_remote_url(
594
+ "https://example.com/team/base.yml?ref=heads/main",
595
+ "/root.yml"
596
+ )
597
+ .unwrap(),
598
+ "https://example.com/root.yml"
599
+ );
262
600
  assert!(join_remote_url("example.com/base.yml", "/root.yml").is_err());
263
601
  assert!(join_remote_url("no-directory", "shared.yml").is_err());
264
602
  }
265
603
 
604
+ #[test]
605
+ fn remote_configuration_enforces_the_response_size_limit() {
606
+ let limit = MAX_REMOTE_CONFIG_BYTES as usize;
607
+
608
+ let (url, server) = serve_once(vec![b'a'; limit]);
609
+ let accepted = fetch_remote_config(&url).unwrap();
610
+ server.join().unwrap();
611
+ assert_eq!(accepted.len(), limit);
612
+
613
+ let (url, server) = serve_once(vec![b'a'; limit + 1]);
614
+ let error = fetch_remote_config(&url).unwrap_err().to_string();
615
+ server.join().unwrap();
616
+ assert!(
617
+ error.starts_with("failed to read remote configuration:"),
618
+ "unexpected error: {error}"
619
+ );
620
+ }
621
+
266
622
  #[test]
267
623
  fn merges_configurations_without_losing_untouched_keys() {
268
624
  let merged = merge_config(
@@ -296,14 +652,63 @@ mod tests {
296
652
  #[test]
297
653
  fn merge_prefers_scalar_overlays_over_mappings() {
298
654
  assert_eq!(merge_config(yaml("A:\n x: 1\n"), yaml("3")), yaml("3"));
299
- let keys = std::collections::HashSet::new();
300
- assert_eq!(deep_merge(yaml("[1, 2]"), yaml("[3]"), &keys), yaml("[3]"));
655
+ let mode = InheritMode::default();
656
+ assert_eq!(deep_merge(yaml("[1, 2]"), yaml("[3]"), &mode), yaml("[3]"));
301
657
  assert_eq!(
302
- deep_merge(yaml("a: 1\n"), yaml("b: 2\n"), &keys),
658
+ deep_merge(yaml("a: 1\n"), yaml("b: 2\n"), &mode),
303
659
  yaml("a: 1\nb: 2\n")
304
660
  );
305
661
  }
306
662
 
663
+ #[test]
664
+ fn per_cop_override_mode_beats_the_root_merge_mode() {
665
+ let merged = merge_inherited_config(
666
+ yaml("Style/A:\n Exclude: [a.rb]\n"),
667
+ yaml(concat!(
668
+ "inherit_mode:\n merge:\n - Exclude\n",
669
+ "Style/A:\n",
670
+ " inherit_mode:\n override:\n - Exclude\n",
671
+ " Exclude: [b.rb]\n",
672
+ )),
673
+ );
674
+ assert_eq!(cop_exclude(&merged, "Style/A"), yaml("[b.rb]"));
675
+ }
676
+
677
+ #[test]
678
+ fn base_side_per_cop_merge_mode_unions_sequences() {
679
+ let merged = merge_inherited_config(
680
+ yaml("Style/A:\n inherit_mode:\n merge:\n - Exclude\n Exclude: [a.rb]\n"),
681
+ yaml("Style/A:\n Exclude: [b.rb]\n"),
682
+ );
683
+ assert_eq!(cop_exclude(&merged, "Style/A"), yaml("[a.rb, b.rb]"));
684
+ }
685
+
686
+ /// The two upstream entry points read a present-but-empty per-cop `inherit_mode` differently,
687
+ /// and both readings are load-bearing: `resolve_inheritance` lets it cancel the file-level
688
+ /// directive, while `merge_with_default` never consults it for the root mode at all.
689
+ #[test]
690
+ fn empty_per_cop_inherit_mode_replaces_the_root_mode_only_when_inheriting() {
691
+ let overlay = concat!(
692
+ "inherit_mode:\n merge:\n - Exclude\n",
693
+ "Style/A:\n inherit_mode: {}\n Exclude: [b.rb]\n",
694
+ );
695
+ let inherited =
696
+ merge_inherited_config(yaml("Style/A:\n Exclude: [a.rb]\n"), yaml(overlay));
697
+ assert_eq!(cop_exclude(&inherited, "Style/A"), yaml("[b.rb]"));
698
+
699
+ let defaulted = merge_config(yaml("Style/A:\n Exclude: [a.rb]\n"), yaml(overlay));
700
+ assert_eq!(cop_exclude(&defaulted, "Style/A"), yaml("[a.rb, b.rb]"));
701
+ }
702
+
703
+ #[test]
704
+ fn union_keeps_each_value_once() {
705
+ let merged = merge_config(
706
+ yaml("Style/A:\n Exclude: [a.rb, b.rb]\n"),
707
+ yaml("inherit_mode:\n merge:\n - Exclude\nStyle/A:\n Exclude: [b.rb, c.rb]\n"),
708
+ );
709
+ assert_eq!(cop_exclude(&merged, "Style/A"), yaml("[a.rb, b.rb, c.rb]"));
710
+ }
711
+
307
712
  #[test]
308
713
  fn loads_inherited_configuration_and_merges_selected_arrays() {
309
714
  let directory = tempdir().unwrap();
@@ -329,4 +734,172 @@ mod tests {
329
734
  );
330
735
  assert!(!config.rule_enabled("Layout/LineLength"));
331
736
  }
737
+
738
+ #[test]
739
+ fn empty_configuration_parses_as_an_empty_mapping() {
740
+ let directory = tempdir().unwrap();
741
+ let path = directory.path().join(".rubocop.yml");
742
+ fs::write(&path, "").unwrap();
743
+ assert_eq!(resolve(&path).unwrap(), yaml("{}"));
744
+
745
+ // A comment-only file is the same story once the comment is stripped.
746
+ fs::write(&path, "# nothing to configure yet\n").unwrap();
747
+ assert_eq!(resolve(&path).unwrap(), yaml("{}"));
748
+ }
749
+
750
+ #[test]
751
+ fn empty_configuration_keeps_the_default_configuration() {
752
+ let directory = tempdir().unwrap();
753
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
754
+ fs::write(directory.path().join(".rubocop.yml"), "").unwrap();
755
+ fs::write(directory.path().join("a.rb"), "x = 1\n").unwrap();
756
+ fs::create_dir_all(directory.path().join("node_modules/pkg")).unwrap();
757
+ fs::write(directory.path().join("node_modules/pkg/v.rb"), "y = 2\n").unwrap();
758
+ let config = Config::load(None, directory.path()).unwrap();
759
+ // `AllCops/Exclude` from `default.yml` has to survive, or `node_modules` gets linted.
760
+ assert!(config.path_excluded(&directory.path().join("node_modules/pkg/v.rb")));
761
+ assert!(!config.path_excluded(&directory.path().join("a.rb")));
762
+ // And so do the per-cop defaults.
763
+ assert_eq!(
764
+ config.cop_value::<usize>("Layout/LineLength", "Max"),
765
+ Some(120)
766
+ );
767
+ }
768
+
769
+ #[test]
770
+ fn non_mapping_configuration_is_rejected_as_malformed() {
771
+ let directory = tempdir().unwrap();
772
+ let path = directory.path().join(".rubocop.yml");
773
+ for contents in ["hello\n", "- Style/A\n", "42\n"] {
774
+ fs::write(&path, contents).unwrap();
775
+ let error = resolve(&path).unwrap_err().to_string();
776
+ assert!(
777
+ error.starts_with("Malformed configuration in"),
778
+ "unexpected error for {contents:?}: {error}"
779
+ );
780
+ }
781
+ }
782
+
783
+ #[test]
784
+ fn diamond_inheritance_reads_every_file_once() {
785
+ let directory = tempdir().unwrap();
786
+ write_diamond_chain(directory.path(), 8);
787
+ taken_parse_count();
788
+
789
+ let resolved = resolve(&directory.path().join(".rubocop.yml")).unwrap();
790
+
791
+ // `.rubocop.yml` plus `f0.yml`..`f8.yml`; without memoisation this would be 2^9 - 1 + 1.
792
+ assert_eq!(taken_parse_count(), 10);
793
+ assert_eq!(
794
+ resolved
795
+ .as_mapping()
796
+ .unwrap()
797
+ .get("Layout/LineLength")
798
+ .unwrap(),
799
+ &yaml("Max: 42\n")
800
+ );
801
+ }
802
+
803
+ #[test]
804
+ fn detects_circular_inheritance() {
805
+ let directory = tempdir().unwrap();
806
+ fs::write(
807
+ directory.path().join(".rubocop.yml"),
808
+ "inherit_from: a.yml\n",
809
+ )
810
+ .unwrap();
811
+ fs::write(directory.path().join("a.yml"), "inherit_from: b.yml\n").unwrap();
812
+ fs::write(directory.path().join("b.yml"), "inherit_from: a.yml\n").unwrap();
813
+ let error = resolve(&directory.path().join(".rubocop.yml"))
814
+ .unwrap_err()
815
+ .to_string();
816
+ assert!(
817
+ error.starts_with("circular inherit_from detected at"),
818
+ "unexpected error: {error}"
819
+ );
820
+
821
+ // A file inheriting from itself is the degenerate case of the same check.
822
+ fs::write(directory.path().join("b.yml"), "inherit_from: b.yml\n").unwrap();
823
+ let error = resolve(&directory.path().join(".rubocop.yml"))
824
+ .unwrap_err()
825
+ .to_string();
826
+ assert!(
827
+ error.starts_with("circular inherit_from detected at"),
828
+ "unexpected error: {error}"
829
+ );
830
+ }
831
+
832
+ #[test]
833
+ fn override_mode_wins_over_an_inherited_merge_mode_end_to_end() {
834
+ let directory = tempdir().unwrap();
835
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
836
+ fs::write(
837
+ directory.path().join("base.yml"),
838
+ "Layout/LineLength:\n Exclude: [a.rb]\n",
839
+ )
840
+ .unwrap();
841
+ fs::write(
842
+ directory.path().join(".rubocop.yml"),
843
+ concat!(
844
+ "inherit_from: base.yml\n",
845
+ "inherit_mode:\n merge:\n - Exclude\n",
846
+ "Layout/LineLength:\n",
847
+ " inherit_mode:\n override:\n - Exclude\n",
848
+ " Exclude: [b.rb]\n",
849
+ ),
850
+ )
851
+ .unwrap();
852
+ let config = Config::load(None, directory.path()).unwrap();
853
+ assert_eq!(
854
+ config.cop_value::<Vec<String>>("Layout/LineLength", "Exclude"),
855
+ Some(vec!["b.rb".to_owned()])
856
+ );
857
+ }
858
+
859
+ #[test]
860
+ fn inherited_per_cop_merge_mode_wins_end_to_end() {
861
+ let directory = tempdir().unwrap();
862
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
863
+ fs::write(
864
+ directory.path().join("base.yml"),
865
+ "Layout/LineLength:\n inherit_mode:\n merge:\n - Exclude\n Exclude: [a.rb]\n",
866
+ )
867
+ .unwrap();
868
+ fs::write(
869
+ directory.path().join(".rubocop.yml"),
870
+ "inherit_from: base.yml\nLayout/LineLength:\n Exclude: [b.rb]\n",
871
+ )
872
+ .unwrap();
873
+ let config = Config::load(None, directory.path()).unwrap();
874
+ assert_eq!(
875
+ config.cop_value::<Vec<String>>("Layout/LineLength", "Exclude"),
876
+ Some(vec!["a.rb".to_owned(), "b.rb".to_owned()])
877
+ );
878
+ }
879
+
880
+ /// `merge_with_default` never derives its root mode per cop, so an empty per-cop directive
881
+ /// does not stop a file-level `merge` from unioning with `default.yml` -- and `node_modules`
882
+ /// stays excluded.
883
+ #[test]
884
+ fn empty_per_cop_inherit_mode_still_unions_with_the_default_configuration() {
885
+ let directory = tempdir().unwrap();
886
+ fs::write(directory.path().join("Gemfile"), "").unwrap();
887
+ fs::write(
888
+ directory.path().join(".rubocop.yml"),
889
+ concat!(
890
+ "inherit_mode:\n merge:\n - Exclude\n",
891
+ "AllCops:\n inherit_mode: {}\n Exclude:\n - custom/**/*\n",
892
+ ),
893
+ )
894
+ .unwrap();
895
+ fs::create_dir_all(directory.path().join("node_modules/pkg")).unwrap();
896
+ fs::write(directory.path().join("node_modules/pkg/v.rb"), "y = 2\n").unwrap();
897
+ fs::create_dir_all(directory.path().join("custom")).unwrap();
898
+ fs::write(directory.path().join("custom/c.rb"), "z = 3\n").unwrap();
899
+ fs::write(directory.path().join("a.rb"), "x = 1\n").unwrap();
900
+ let config = Config::load(None, directory.path()).unwrap();
901
+ assert!(config.path_excluded(&directory.path().join("node_modules/pkg/v.rb")));
902
+ assert!(config.path_excluded(&directory.path().join("custom/c.rb")));
903
+ assert!(!config.path_excluded(&directory.path().join("a.rb")));
904
+ }
332
905
  }