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
data/src/config/paths.rs CHANGED
@@ -1,8 +1,10 @@
1
+ use std::borrow::Cow;
1
2
  use std::collections::HashMap;
2
3
  use std::fs;
3
- use std::path::{Path, PathBuf};
4
+ use std::path::{Component, Path, PathBuf};
5
+ use std::sync::OnceLock;
4
6
 
5
- use globset::{Glob, GlobMatcher};
7
+ use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
6
8
  use serde_yaml_ng::{Mapping, Value};
7
9
 
8
10
  pub(super) fn compile_excludes(raw: &Value) -> HashMap<String, PathPatterns> {
@@ -43,7 +45,19 @@ pub(super) fn cop_patterns(raw: &Value, name: &str, key: &str) -> Option<PathPat
43
45
  }
44
46
 
45
47
  fn mapping_patterns(mapping: &Mapping, key: &str) -> Option<PathPatterns> {
46
- let patterns: Vec<String> = serde_yaml_ng::from_value(mapping.get(key)?.clone()).ok()?;
48
+ let value = mapping.get(key)?;
49
+ // `Array(...)`: a lone scalar counts as a one-element list. `Exclude: tmp/**/*`, written
50
+ // without the list dashes, is a common enough slip that it has to mean something, and Ruby's
51
+ // own idiom for reading these lists says what. Upstream does not survive the shape at all --
52
+ // `Config#make_excludes_absolute` raises `undefined method 'map!' for an instance of String` --
53
+ // and this project answers a malformed configuration rather than reproducing a crash. What it
54
+ // must not do is the third thing, which is what deserializing straight into `Vec<String>` and
55
+ // dropping the error did: forgetting the entry leaves an `Exclude` silently unenforced, so the
56
+ // files the user ruled out get inspected and nothing says why.
57
+ let patterns: Vec<String> = match value.as_str() {
58
+ Some(pattern) => vec![pattern.to_owned()],
59
+ None => serde_yaml_ng::from_value(value.clone()).ok()?,
60
+ };
47
61
  Some(PathPatterns::compile(&patterns))
48
62
  }
49
63
 
@@ -58,6 +72,91 @@ pub(super) struct PathPatterns {
58
72
  /// degrading into "no list configured".
59
73
  configured: usize,
60
74
  patterns: Vec<CompiledPattern>,
75
+ /// Every pattern, asked in one go.
76
+ all: PatternSet,
77
+ /// The patterns written with a capital, which is the subset
78
+ /// `matches_uppercase_includes` walks.
79
+ uppercase: PatternSet,
80
+ /// Whether any pattern is absolute. Only those are ever held against the absolute spelling of a
81
+ /// path, so a list without one -- which is every list RuboCop ships and nearly every list a
82
+ /// project writes -- skips that second pass and the `PathBuf` it would build per file.
83
+ has_absolute: bool,
84
+ }
85
+
86
+ /// One list of globs folded into `globset`'s set form so a path can be tested against all of them
87
+ /// at once.
88
+ ///
89
+ /// `globset` sorts a set into literal, basename, extension and prefix tables and only falls back to
90
+ /// a regex for the patterns none of those cover, so asking the 46 default `Include` globs together
91
+ /// costs about what asking one of them separately did. Walking `patterns` one at a time is kept for
92
+ /// the cases the sets cannot answer -- a hidden path, where each glob has its own say about whether
93
+ /// it reaches the dot -- so the sets are an accelerator, never the authority.
94
+ #[derive(Clone, Debug, Default)]
95
+ struct PatternSet {
96
+ /// Matched against the project-relative path.
97
+ full: GlobSet,
98
+ /// The separator-free patterns, matched against the basename as well.
99
+ basename: GlobSet,
100
+ /// The `dir` half of a `dir/**/*` pattern, matched against each ancestor of the path.
101
+ ancestor: GlobSet,
102
+ /// Whether `ancestor` holds anything, so the ancestor walk is skipped when it does not.
103
+ has_ancestor: bool,
104
+ /// Whether every builder produced a set. A failure only costs speed: the caller falls back to
105
+ /// walking the patterns one at a time, which is what it did before the sets existed.
106
+ usable: bool,
107
+ }
108
+
109
+ impl PatternSet {
110
+ fn build<'a>(patterns: impl Iterator<Item = &'a CompiledPattern>) -> Self {
111
+ let mut full = GlobSetBuilder::new();
112
+ let mut basename = GlobSetBuilder::new();
113
+ let mut ancestor = GlobSetBuilder::new();
114
+ let mut has_ancestor = false;
115
+ for pattern in patterns {
116
+ full.add(pattern.matcher.glob().clone());
117
+ if pattern.basename {
118
+ basename.add(pattern.matcher.glob().clone());
119
+ }
120
+ if let Some(prefix) = pattern.ancestor.as_ref() {
121
+ ancestor.add(prefix.glob().clone());
122
+ has_ancestor = true;
123
+ }
124
+ }
125
+ match (full.build(), basename.build(), ancestor.build()) {
126
+ (Ok(full), Ok(basename), Ok(ancestor)) => Self {
127
+ full,
128
+ basename,
129
+ ancestor,
130
+ has_ancestor,
131
+ usable: true,
132
+ },
133
+ _ => Self::default(),
134
+ }
135
+ }
136
+
137
+ /// The same question `CompiledPattern::matches` answers for one pattern, asked of the whole set.
138
+ /// Only reachable where every pattern would have been asked with `hidden` false, so the
139
+ /// `skips_hidden` shortcut has no say here.
140
+ fn matches(&self, normalized: &str, basename: &str) -> bool {
141
+ self.full.is_match(normalized)
142
+ || (self.has_ancestor
143
+ && Path::new(normalized)
144
+ .ancestors()
145
+ .skip(1)
146
+ .any(|path| self.ancestor.is_match(slashed(path).as_ref())))
147
+ || self.basename.is_match(basename)
148
+ }
149
+ }
150
+
151
+ /// `path` as text with Windows separators folded to `/`, borrowing whenever there is nothing to
152
+ /// fold -- which is every path on a Unix checkout.
153
+ fn slashed(path: &Path) -> Cow<'_, str> {
154
+ match path.to_string_lossy() {
155
+ Cow::Borrowed(text) if !text.contains('\\') => Cow::Borrowed(text),
156
+ Cow::Borrowed(text) => Cow::Owned(text.replace('\\', "/")),
157
+ Cow::Owned(text) if !text.contains('\\') => Cow::Owned(text),
158
+ Cow::Owned(text) => Cow::Owned(text.replace('\\', "/")),
159
+ }
61
160
  }
62
161
 
63
162
  #[derive(Clone, Debug)]
@@ -73,16 +172,26 @@ struct CompiledPattern {
73
172
  /// The pattern's dot-prefixed segments, such as `.git` in `.git/**/*`, compiled on their own so
74
173
  /// they can be asked whether they reach a hidden part of a path.
75
174
  dot_segments: Vec<GlobMatcher>,
175
+ /// `/[A-Z]/.match?(pattern)`: whether the pattern names a file by a capitalised name of its
176
+ /// own, which is what `allowed_camel_case_file?` selects on.
177
+ has_uppercase: bool,
76
178
  }
77
179
 
78
180
  impl PathPatterns {
79
181
  fn compile(patterns: &[String]) -> Self {
182
+ let compiled: Vec<CompiledPattern> = patterns
183
+ .iter()
184
+ .filter_map(|pattern| CompiledPattern::compile(pattern))
185
+ .collect();
186
+ let all = PatternSet::build(compiled.iter());
187
+ let uppercase = PatternSet::build(compiled.iter().filter(|pattern| pattern.has_uppercase));
188
+ let has_absolute = compiled.iter().any(|pattern| pattern.absolute);
80
189
  Self {
81
190
  configured: patterns.len(),
82
- patterns: patterns
83
- .iter()
84
- .filter_map(|pattern| CompiledPattern::compile(pattern))
85
- .collect(),
191
+ patterns: compiled,
192
+ all,
193
+ uppercase,
194
+ has_absolute,
86
195
  }
87
196
  }
88
197
 
@@ -97,28 +206,90 @@ impl PathPatterns {
97
206
  }
98
207
 
99
208
  pub(super) fn matches_includes(&self, path: &Path, root: &Path) -> bool {
209
+ self.matches_includes_inner(path, root, false)
210
+ }
211
+
212
+ /// `file_to_include?(file) { |pattern| /[A-Z]/.match?(pattern.to_s) }`: the same walk over the
213
+ /// `Include` list, restricted to the patterns written with a capital. A file that only reaches
214
+ /// the run through one of those -- `Rakefile`, `Gemfile` -- is named after the pattern rather
215
+ /// than after Ruby's conventions, so `Naming/FileName` lets it be.
216
+ pub(super) fn matches_uppercase_includes(&self, path: &Path, root: &Path) -> bool {
217
+ self.matches_includes_inner(path, root, true)
218
+ }
219
+
220
+ fn matches_includes_inner(&self, path: &Path, root: &Path, uppercase_only: bool) -> bool {
100
221
  if self.patterns.is_empty() {
101
222
  return false;
102
223
  }
103
224
  let relative = project_relative(path, root).unwrap_or_else(|| path.to_path_buf());
104
225
  let relative = relative.to_string_lossy().replace('\\', "/");
105
- let normalized = relative.trim_start_matches("./");
106
226
  let basename = path
107
227
  .file_name()
108
228
  .and_then(|name| name.to_str())
109
229
  .unwrap_or("");
230
+ if self.matches_includes_spelling(
231
+ relative.trim_start_matches("./"),
232
+ basename,
233
+ uppercase_only,
234
+ false,
235
+ ) {
236
+ return true;
237
+ }
238
+ // `match_relative_or_absolute_path?`: an `Include` pattern written absolutely is held
239
+ // against the absolute spelling of the file, which is the only spelling it can ever match.
240
+ // Only the absolute patterns are asked -- upstream picks the spelling per pattern, and
241
+ // handing a relative pattern the absolute path would let `**/vendor/**/*` match by way of a
242
+ // directory above the project root that the user never named.
243
+ if !self.has_absolute {
244
+ return false;
245
+ }
246
+ let Some(absolute) = absolute_form(path) else {
247
+ return false;
248
+ };
249
+ self.matches_includes_spelling(slashed(&absolute).as_ref(), basename, uppercase_only, true)
250
+ }
251
+
252
+ /// One spelling of a path put to the `Include` list. `absolute_only` restricts the question to
253
+ /// the absolute patterns and takes the slow walk, because the sets are built over the whole
254
+ /// list and there is no set for that subset.
255
+ fn matches_includes_spelling(
256
+ &self,
257
+ normalized: &str,
258
+ basename: &str,
259
+ uppercase_only: bool,
260
+ absolute_only: bool,
261
+ ) -> bool {
110
262
  // RuboCop's `match_path?` matches without `FNM_DOTMATCH`, so a wildcard never reaches a dot
111
263
  // component -- only a segment the pattern spells out literally does. On top of that,
112
264
  // `hidden_file_in_not_hidden_dir?` lets a dot *file* through as long as it sits in a real,
113
265
  // non-hidden directory. So a dot directory is invisible unless named, and a top-level dot
114
266
  // file needs naming too.
115
267
  let (directories, file) = normalized.rsplit_once('/').unwrap_or(("", normalized));
268
+ let hidden_file = hidden_segment(file);
269
+ let hidden_directory = directories.split('/').any(hidden_segment);
270
+ // With nothing hidden about the path, every pattern would clear the two dot tests below on
271
+ // its own, and what is left is the plain glob question the set can answer in one go.
272
+ if !absolute_only {
273
+ let set = if uppercase_only {
274
+ &self.uppercase
275
+ } else {
276
+ &self.all
277
+ };
278
+ if set.usable && !hidden_directory && !(hidden_file && directories.is_empty()) {
279
+ return set.matches(normalized, basename);
280
+ }
281
+ }
116
282
  let hidden_directories: Vec<&str> = directories
117
283
  .split('/')
118
- .filter(|part| part.starts_with('.'))
284
+ .filter(|part| hidden_segment(part))
119
285
  .collect();
120
- let hidden_file = file.starts_with('.');
121
286
  self.patterns.iter().any(|pattern| {
287
+ if uppercase_only && !pattern.has_uppercase {
288
+ return false;
289
+ }
290
+ if absolute_only && !pattern.absolute {
291
+ return false;
292
+ }
122
293
  if !hidden_directories
123
294
  .iter()
124
295
  .all(|part| pattern.reaches_hidden(part))
@@ -144,10 +315,34 @@ impl PathPatterns {
144
315
  .and_then(|name| name.to_str())
145
316
  .unwrap_or("");
146
317
  let hidden = respect_hidden && has_hidden_component(Path::new(normalized));
318
+ // `hidden` and `absolute_only` are the only things that put a pattern out of the running on
319
+ // its own; without them the set answers for the whole list at once.
320
+ let matched = if self.all.usable && !hidden && !absolute_only {
321
+ self.all.matches(normalized, basename)
322
+ } else {
323
+ self.patterns
324
+ .iter()
325
+ .filter(|pattern| !absolute_only || pattern.absolute)
326
+ .any(|pattern| pattern.matches(normalized, basename, hidden))
327
+ };
328
+ if matched {
329
+ return true;
330
+ }
331
+ // `file_to_exclude?` expands the file before matching, so an `Exclude` written absolutely
332
+ // reaches project files upstream. The same restriction as on the `Include` side applies:
333
+ // only the absolute patterns get to see the absolute spelling.
334
+ if !self.has_absolute {
335
+ return false;
336
+ }
337
+ let Some(absolute) = absolute_form(path) else {
338
+ return false;
339
+ };
340
+ let absolute = slashed(&absolute);
341
+ let hidden = respect_hidden && has_hidden_component(Path::new(absolute.as_ref()));
147
342
  self.patterns
148
343
  .iter()
149
- .filter(|pattern| !absolute_only || pattern.absolute)
150
- .any(|pattern| pattern.matches(normalized, basename, hidden))
344
+ .filter(|pattern| pattern.absolute)
345
+ .any(|pattern| pattern.matches(absolute.as_ref(), basename, hidden))
151
346
  }
152
347
  }
153
348
 
@@ -170,6 +365,9 @@ impl CompiledPattern {
170
365
  .filter_map(|segment| Glob::new(segment).ok())
171
366
  .map(|glob| glob.compile_matcher())
172
367
  .collect(),
368
+ has_uppercase: pattern
369
+ .chars()
370
+ .any(|character| character.is_ascii_uppercase()),
173
371
  })
174
372
  }
175
373
 
@@ -191,7 +389,7 @@ impl CompiledPattern {
191
389
  Path::new(normalized)
192
390
  .ancestors()
193
391
  .skip(1)
194
- .any(|path| ancestor.is_match(path.to_string_lossy().replace('\\', "/")))
392
+ .any(|path| ancestor.is_match(slashed(path).as_ref()))
195
393
  })
196
394
  || (self.basename && self.matcher.is_match(basename))
197
395
  }
@@ -208,12 +406,38 @@ impl CompiledPattern {
208
406
  /// treated as hidden. The text comparisons are tried first because they cover every normal case;
209
407
  /// only when they disagree is it worth asking the filesystem.
210
408
  pub(super) fn project_relative(path: &Path, root: &Path) -> Option<PathBuf> {
409
+ // `strip_prefix` compares component by component, so a `..` left in the path survives into the
410
+ // result: `sonicop ../lib` run from a subdirectory produced `sub/../lib/a.rb` where upstream
411
+ // produces `lib/a.rb`, and every wildcard pattern then refused the file over the dot segment.
412
+ // Upstream never sees one because `PathUtil.relative_path` runs the path through
413
+ // `File.expand_path` first, so the fold happens here too.
414
+ let folded = without_parent_segments(path);
415
+ let path = folded.as_ref();
211
416
  if let Ok(relative) = path.strip_prefix(root) {
212
417
  return Some(relative.to_path_buf());
213
418
  }
214
419
  if let Ok(relative) = strip_verbatim(path).strip_prefix(strip_verbatim(root)) {
215
420
  return Some(relative.to_path_buf());
216
421
  }
422
+ // A path spelled relative to the working directory -- which is every path a run started with
423
+ // `sonicop .` walks -- shares no text with an absolute root, so both comparisons above miss and
424
+ // the file falls through to `canonicalize`. That is two syscalls per question and three
425
+ // questions per file, which cost more than inspecting the file did. Joining the working
426
+ // directory answers the same question without asking the filesystem anything; `Components`
427
+ // drops the `.` segments a walk introduces, and the join is folded again because a leading `..`
428
+ // only has something to cancel once the working directory is in front of it.
429
+ if path.is_relative() {
430
+ if let Some(joined) = working_directory().map(|cwd| cwd.join(path)) {
431
+ let joined = without_parent_segments(&joined);
432
+ let joined = joined.as_ref();
433
+ if let Ok(relative) = joined.strip_prefix(root) {
434
+ return Some(relative.to_path_buf());
435
+ }
436
+ if let Ok(relative) = strip_verbatim(joined).strip_prefix(strip_verbatim(root)) {
437
+ return Some(relative.to_path_buf());
438
+ }
439
+ }
440
+ }
217
441
  let resolved = fs::canonicalize(path).ok()?;
218
442
  let root = fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
219
443
  resolved
@@ -227,6 +451,80 @@ pub(super) fn project_relative(path: &Path, root: &Path) -> Option<PathBuf> {
227
451
  .map(Path::to_path_buf)
228
452
  }
229
453
 
454
+ /// `path` with its `..` segments folded away, borrowed whenever it has none -- which is every path
455
+ /// a `sonicop .` run walks.
456
+ ///
457
+ /// This is for `project_relative`, whose comparisons are all `strip_prefix` and therefore run over
458
+ /// `Components`, which already hides the `.` segments a walk introduces. Only `..` needs settling
459
+ /// there, and settling it is lexical, exactly as `File.expand_path` is: a `..` that crosses a
460
+ /// symlink lands where the text says rather than where the link points. That is upstream's answer
461
+ /// as well, and it is what keeps this from costing a `readlink` per file. Matching against the
462
+ /// *text* of a path needs more than this -- see `folded_components`.
463
+ fn without_parent_segments(path: &Path) -> Cow<'_, Path> {
464
+ if !path
465
+ .components()
466
+ .any(|component| component == Component::ParentDir)
467
+ {
468
+ // `strip_prefix` -- the only thing the fold is for here -- compares `Components`, and those
469
+ // already drop the `.` segments a walk introduces. A path with no `..` in it is therefore
470
+ // its own folded form as far as `project_relative` is concerned, and can be borrowed.
471
+ return Cow::Borrowed(path);
472
+ }
473
+ Cow::Owned(folded_components(path))
474
+ }
475
+
476
+ /// `path` rebuilt out of its components, with `.` and `..` folded away.
477
+ ///
478
+ /// Unlike `without_parent_segments` this always rebuilds, because a glob is matched against the
479
+ /// *text* of a path: `Components` hides the `./` a `sonicop .` walk leaves in the middle of
480
+ /// `<cwd>/./lib/a.rb`, but `to_string_lossy` does not, and no pattern spelled `<root>/lib/**/*.rb`
481
+ /// can match a string with that `./` still in it.
482
+ fn folded_components(path: &Path) -> PathBuf {
483
+ let mut kept: Vec<Component<'_>> = Vec::new();
484
+ for component in path.components() {
485
+ match component {
486
+ Component::CurDir => {}
487
+ Component::ParentDir => match kept.last() {
488
+ // `a/../b` is `b`.
489
+ Some(Component::Normal(_)) => {
490
+ kept.pop();
491
+ }
492
+ // `/..` is `/`: a path cannot climb past its own root.
493
+ Some(Component::RootDir) => {}
494
+ // A leading `..`, or one following another, has nothing to cancel and has to
495
+ // survive -- `File.expand_path` keeps those too, once a relative path has run out
496
+ // of segments to undo.
497
+ _ => kept.push(component),
498
+ },
499
+ other => kept.push(other),
500
+ }
501
+ }
502
+ kept.iter().collect()
503
+ }
504
+
505
+ /// `File.expand_path(path)`: the absolute spelling of `path`, folded.
506
+ ///
507
+ /// `Config#file_to_exclude?` matches the absolute path and `Config#file_to_include?` matches the
508
+ /// relative *or* the absolute one, so a pattern written absolutely reaches project files upstream.
509
+ /// The working directory is what a relative path is joined to, because that is what
510
+ /// `File.expand_path` uses -- not the project root, which a run started outside the project does
511
+ /// not sit in.
512
+ fn absolute_form(path: &Path) -> Option<PathBuf> {
513
+ if path.is_absolute() {
514
+ return Some(folded_components(path));
515
+ }
516
+ Some(folded_components(&working_directory()?.join(path)))
517
+ }
518
+
519
+ /// The working directory, asked of the kernel once.
520
+ ///
521
+ /// Nothing in the program changes it, and `project_relative` needs it for every file a relative
522
+ /// argument expands to, so the answer is kept rather than fetched again.
523
+ fn working_directory() -> Option<&'static Path> {
524
+ static CWD: OnceLock<Option<PathBuf>> = OnceLock::new();
525
+ CWD.get_or_init(|| std::env::current_dir().ok()).as_deref()
526
+ }
527
+
230
528
  /// Drops Windows' `\\?\` extended-length marker so that two spellings of one path can be compared.
231
529
  /// Other platforms never carry the prefix, so this is the identity there.
232
530
  fn strip_verbatim(path: &Path) -> &Path {
@@ -247,16 +545,30 @@ fn strip_verbatim(path: &Path) -> &Path {
247
545
  /// the same project-relative path, so both walk the components here instead of
248
546
  /// each carrying its own copy of the rule.
249
547
  pub(super) fn has_hidden_component(path: &Path) -> bool {
250
- path.components().any(|component| {
251
- component
252
- .as_os_str()
253
- .to_str()
254
- .is_some_and(|part| part.starts_with('.') && part != "." && part != "..")
255
- })
548
+ path.components()
549
+ .any(|component| component.as_os_str().to_str().is_some_and(hidden_segment))
550
+ }
551
+
552
+ /// Whether one path segment is hidden.
553
+ ///
554
+ /// `.` and `..` begin with a dot without being hidden, and both can reach a matcher: a path outside
555
+ /// the project root keeps its `..` segments, and a walk hands out `./`-prefixed spellings.
556
+ /// Upstream draws the same line -- `file_to_include?` asks
557
+ /// `relative_file_path.start_with?('.') && !relative_file_path.start_with?('..')`. Reading a `..`
558
+ /// as hidden put every wildcard pattern out of the running, which is how `sonicop ../lib` came to
559
+ /// inspect nothing at all.
560
+ fn hidden_segment(segment: &str) -> bool {
561
+ segment.starts_with('.') && segment != "." && segment != ".."
256
562
  }
257
563
 
258
564
  /// Verbatim copy of the pre-compilation matcher, kept so the compiled matcher can be
259
565
  /// proven byte-for-byte equivalent over a cross product of paths and patterns.
566
+ ///
567
+ /// It compiles a `Glob` per pattern per question and knows nothing of `PatternSet`, `has_absolute`
568
+ /// or the dot-segment tables, so the accelerators the real matcher is built out of have somewhere
569
+ /// to be checked against. What it does share is `absolute_form` and `hidden_segment`: those decide
570
+ /// *which path* is being matched rather than *how*, and a second copy of them would only prove the
571
+ /// two copies agree.
260
572
  #[cfg(test)]
261
573
  fn matches_patterns_reference(
262
574
  path: &Path,
@@ -265,22 +577,25 @@ fn matches_patterns_reference(
265
577
  respect_hidden: bool,
266
578
  ) -> bool {
267
579
  let relative = path.strip_prefix(root).unwrap_or(path);
268
- let normalized = relative
580
+ let relative = relative
269
581
  .to_string_lossy()
270
582
  .replace('\\', "/")
271
583
  .trim_start_matches("./")
272
584
  .to_owned();
585
+ let absolute = absolute_form(path).map(|path| slashed(&path).into_owned());
273
586
  let basename = path
274
587
  .file_name()
275
588
  .and_then(|name| name.to_str())
276
589
  .unwrap_or("");
277
- let (directories, file) = normalized.rsplit_once('/').unwrap_or(("", &normalized));
278
- let hidden_directories: Vec<&str> = directories
279
- .split('/')
280
- .filter(|part| part.starts_with('.') && *part != "." && *part != "..")
281
- .collect();
282
- let hidden_file = file.starts_with('.') && file != "." && file != "..";
283
590
  patterns.iter().any(|pattern| {
591
+ // `match_relative_or_absolute_path?`: the absolute spelling of the path is in play for an
592
+ // absolute pattern and for nothing else.
593
+ let mut spellings = vec![relative.as_str()];
594
+ if Path::new(pattern.as_str()).is_absolute()
595
+ && let Some(absolute) = absolute.as_deref()
596
+ {
597
+ spellings.push(absolute);
598
+ }
284
599
  let pattern = pattern.trim_start_matches("./");
285
600
  // Only a dot-prefixed segment of the pattern reaches a dot component, and a dot file at the
286
601
  // top level needs the same. Below the top level a dot file is reached anyway.
@@ -290,28 +605,36 @@ fn matches_patterns_reference(
290
605
  && Glob::new(part).is_ok_and(|glob| glob.compile_matcher().is_match(segment))
291
606
  })
292
607
  };
293
- if respect_hidden
294
- && (!hidden_directories.iter().all(|part| reaches(part))
295
- || (hidden_file && directories.is_empty() && !reaches(file)))
296
- {
297
- return false;
298
- }
299
- Glob::new(pattern)
300
- .map(|glob| {
301
- let matcher = glob.compile_matcher();
302
- matcher.is_match(&normalized)
303
- || pattern.strip_suffix("/**/*").is_some_and(|prefix| {
304
- Glob::new(prefix).is_ok_and(|prefix_glob| {
305
- let prefix_matcher = prefix_glob.compile_matcher();
306
- Path::new(&normalized).ancestors().skip(1).any(|ancestor| {
307
- prefix_matcher
308
- .is_match(ancestor.to_string_lossy().replace('\\', "/"))
608
+ spellings.into_iter().any(|normalized| {
609
+ let (directories, file) = normalized.rsplit_once('/').unwrap_or(("", normalized));
610
+ let hidden_directories: Vec<&str> = directories
611
+ .split('/')
612
+ .filter(|part| hidden_segment(part))
613
+ .collect();
614
+ let hidden_file = hidden_segment(file);
615
+ if respect_hidden
616
+ && (!hidden_directories.iter().all(|part| reaches(part))
617
+ || (hidden_file && directories.is_empty() && !reaches(file)))
618
+ {
619
+ return false;
620
+ }
621
+ Glob::new(pattern)
622
+ .map(|glob| {
623
+ let matcher = glob.compile_matcher();
624
+ matcher.is_match(normalized)
625
+ || pattern.strip_suffix("/**/*").is_some_and(|prefix| {
626
+ Glob::new(prefix).is_ok_and(|prefix_glob| {
627
+ let prefix_matcher = prefix_glob.compile_matcher();
628
+ Path::new(normalized).ancestors().skip(1).any(|ancestor| {
629
+ prefix_matcher
630
+ .is_match(ancestor.to_string_lossy().replace('\\', "/"))
631
+ })
309
632
  })
310
633
  })
311
- })
312
- || (!pattern.contains('/') && matcher.is_match(basename))
313
- })
314
- .unwrap_or(false)
634
+ || (!pattern.contains('/') && matcher.is_match(basename))
635
+ })
636
+ .unwrap_or(false)
637
+ })
315
638
  })
316
639
  }
317
640
 
@@ -332,9 +655,11 @@ fn matches_any_reference(path: &Path, root: &Path, patterns: &[String]) -> bool
332
655
  mod tests {
333
656
  use std::path::{Path, PathBuf};
334
657
 
658
+ use serde_yaml_ng::Value;
659
+
335
660
  use super::{
336
- PathPatterns, matches_any_reference, matches_patterns_reference, project_relative,
337
- strip_verbatim,
661
+ PathPatterns, hidden_segment, mapping_patterns, matches_any_reference,
662
+ matches_patterns_reference, project_relative, slashed, strip_verbatim,
338
663
  };
339
664
 
340
665
  fn owned(patterns: &[&str]) -> Vec<String> {
@@ -344,6 +669,14 @@ mod tests {
344
669
  .collect()
345
670
  }
346
671
 
672
+ /// `AllCops`'s `Include`/`Exclude` read the way the configuration hands it over, so the shape
673
+ /// of the YAML is part of what is under test.
674
+ fn all_cops_patterns(yaml: &str, key: &str) -> Option<PathPatterns> {
675
+ let value: Value = serde_yaml_ng::from_str(yaml).unwrap();
676
+ let all_cops = value.as_mapping().unwrap().get("AllCops").unwrap();
677
+ mapping_patterns(all_cops.as_mapping().unwrap(), key)
678
+ }
679
+
347
680
  fn includes(path: &str, root: &str, patterns: &[&str]) -> bool {
348
681
  PathPatterns::compile(&owned(patterns)).matches_includes(Path::new(path), Path::new(root))
349
682
  }
@@ -390,6 +723,24 @@ mod tests {
390
723
  );
391
724
  }
392
725
 
726
+ /// `sonicop .` が渡すのは `./lib/a.rb` のような相対パスで、絶対のルートとは文字列が
727
+ /// 重ならない。作業ディレクトリを継ぎ足して解決できなければ、ファイルごとに
728
+ /// `canonicalize` へ落ちて探索が検査より高くつく。存在しないパスで確かめているのは、
729
+ /// 継ぎ足しで答えが出たことを `canonicalize` の成功と取り違えないため。
730
+ #[test]
731
+ fn a_relative_path_resolves_against_the_working_directory() {
732
+ let cwd = std::env::current_dir().unwrap();
733
+ assert_eq!(
734
+ project_relative(Path::new("nowhere/lib/a.rb"), &cwd.join("nowhere")),
735
+ Some(PathBuf::from("lib/a.rb"))
736
+ );
737
+ // 走査が挟む `.` は `Components` が落とすので、綴りの違いは結果に出ない。
738
+ assert_eq!(
739
+ project_relative(Path::new("./nowhere/lib/a.rb"), &cwd.join("nowhere")),
740
+ Some(PathBuf::from("lib/a.rb"))
741
+ );
742
+ }
743
+
393
744
  /// プロジェクト外は None のままでなければ、相対パターンが外部へ届いてしまう。
394
745
  #[test]
395
746
  fn a_path_outside_the_root_has_no_project_relative_form() {
@@ -429,6 +780,134 @@ mod tests {
429
780
  assert!(excludes("/p/.git/config.rb", "/p", &["**/*.rb"]));
430
781
  }
431
782
 
783
+ /// `.` and `..` begin with a dot without being hidden, and a matcher meets both: a walk hands
784
+ /// out `./`-prefixed spellings, and a file outside the project root keeps its `..` segments.
785
+ /// Reading either as hidden put every pattern that does not spell a dot -- which is the whole
786
+ /// of the default `Include` list -- out of the running, and `sonicop ../lib` inspected nothing.
787
+ #[test]
788
+ fn a_parent_directory_segment_is_not_hidden() {
789
+ assert!(!hidden_segment("."));
790
+ assert!(!hidden_segment(".."));
791
+ assert!(!hidden_segment("lib"));
792
+ // The exemption is those two names and nothing else, which is where `hidden_dir?` and
793
+ // `hidden_file?` draw it too -- both ask only `start_with?('.')` of a whole segment.
794
+ assert!(hidden_segment(".git"));
795
+ assert!(hidden_segment("..."));
796
+ assert!(hidden_segment(".rubocop.yml"));
797
+ // A path that keeps its `..` -- one outside the project root -- is still reachable by an
798
+ // ordinary wildcard, which is what upstream does once it switches to the absolute spelling.
799
+ assert!(includes("../other/x.rb", "/p", &["**/*.rb"]));
800
+ // A genuinely hidden directory still has to be named by the pattern.
801
+ assert!(!includes("/p/.git/config.rb", "/p", &["**/*.rb"]));
802
+ assert!(includes("/p/.git/config.rb", "/p", &[".git/**/*"]));
803
+ }
804
+
805
+ /// `PathUtil.relative_path` expands the path before comparing it with the base directory, so a
806
+ /// project-relative spelling never keeps a `..`. `strip_prefix` compares components and leaves
807
+ /// one in place, so `sonicop ../lib` run from `sub/` produced `sub/../lib/a.rb` where upstream
808
+ /// produces `lib/a.rb`.
809
+ #[test]
810
+ fn a_parent_directory_target_folds_before_the_root_is_stripped() {
811
+ assert_eq!(
812
+ project_relative(Path::new("/p/sub/../lib/a.rb"), Path::new("/p")),
813
+ Some(PathBuf::from("lib/a.rb"))
814
+ );
815
+ // Folding must not invent a hit: `..` that climbs out of the root still leaves the file
816
+ // outside it.
817
+ assert_eq!(
818
+ project_relative(
819
+ Path::new("/p/../other/x.rb"),
820
+ Path::new("/p/does-not-exist")
821
+ ),
822
+ None
823
+ );
824
+ // The spelling a walk actually produces is relative to the working directory, and a leading
825
+ // `..` only has something to cancel once that directory is in front of it. Non-existent
826
+ // paths again, so that an answer can only have come from the lexical fold.
827
+ let cwd = std::env::current_dir().unwrap();
828
+ let sibling = cwd.parent().unwrap().join("nowhere");
829
+ assert_eq!(
830
+ project_relative(Path::new("../nowhere/lib/a.rb"), &sibling),
831
+ Some(PathBuf::from("lib/a.rb"))
832
+ );
833
+ }
834
+
835
+ /// `file_to_exclude?` matches the absolute path and `file_to_include?` the relative *or* the
836
+ /// absolute one, so a pattern written absolutely reaches project files upstream. Asking only
837
+ /// the project-relative spelling meant an absolute `Exclude` was quietly ignored and an
838
+ /// absolute `Include` matched nothing at all.
839
+ #[test]
840
+ fn an_absolute_pattern_reaches_a_file_inside_the_project() {
841
+ // A root under the working directory, so the relative spellings below resolve against it.
842
+ // Nothing here exists; the answers can only come from the lexical expansion.
843
+ let cwd = std::env::current_dir().unwrap();
844
+ let root = cwd.join("nowhere/deep");
845
+ let root_text = slashed(&root).into_owned();
846
+ let file = root.join("lib/a.rb");
847
+ let temporary = root.join("tmp/t.rb");
848
+
849
+ let include = PathPatterns::compile(&[format!("{root_text}/lib/**/*.rb")]);
850
+ assert!(include.matches_includes(&file, &root));
851
+ assert!(!include.matches_includes(&temporary, &root));
852
+ // The same file spelled the way a walk hands it over. `./` is the spelling `sonicop .`
853
+ // produces, and `Components` hides it from `strip_prefix` while leaving it in the text a
854
+ // glob is matched against, so it needs its own case.
855
+ assert!(include.matches_includes(Path::new("nowhere/deep/lib/a.rb"), &root));
856
+ assert!(include.matches_includes(Path::new("./nowhere/deep/lib/a.rb"), &root));
857
+
858
+ let exclude = PathPatterns::compile(&[format!("{root_text}/tmp/**/*")]);
859
+ assert!(exclude.matches_any(&temporary, &root));
860
+ assert!(exclude.matches_any(Path::new("./nowhere/deep/tmp/t.rb"), &root));
861
+ assert!(!exclude.matches_any(&file, &root));
862
+
863
+ // The other side of the boundary: a *relative* pattern is never held against the absolute
864
+ // spelling. `**/nowhere/**/*` names a directory that only exists above the project root, so
865
+ // matching it there would exclude every file in the project over a name the user never
866
+ // wrote about.
867
+ assert!(!excludes(
868
+ "lib/a.rb",
869
+ root.to_str().unwrap(),
870
+ &["**/nowhere/**/*"]
871
+ ));
872
+ assert!(!includes(
873
+ "lib/a.rb",
874
+ root.to_str().unwrap(),
875
+ &["**/nowhere/**/*"]
876
+ ));
877
+ // And a relative pattern still matches the relative spelling, absolutely as before.
878
+ assert!(excludes("lib/a.rb", root.to_str().unwrap(), &["lib/**/*"]));
879
+ }
880
+
881
+ /// `Array(...)`: a lone scalar is a one-element list. Upstream raises on the shape
882
+ /// (`undefined method 'map!' for an instance of String`), and the answer this project gives
883
+ /// instead must not be "the entry never existed" -- a forgotten `Exclude` inspects the files
884
+ /// the user ruled out and says nothing about it.
885
+ #[test]
886
+ fn a_scalar_pattern_list_is_read_as_one_element() {
887
+ let scalar = all_cops_patterns("AllCops:\n Exclude: tmp/**/*\n", "Exclude").unwrap();
888
+ let list = all_cops_patterns("AllCops:\n Exclude:\n - tmp/**/*\n", "Exclude").unwrap();
889
+ for patterns in [&scalar, &list] {
890
+ assert!(!patterns.is_empty());
891
+ assert!(patterns.matches_any(Path::new("/p/tmp/t.rb"), Path::new("/p")));
892
+ assert!(!patterns.matches_any(Path::new("/p/lib/a.rb"), Path::new("/p")));
893
+ }
894
+
895
+ let scalar = all_cops_patterns("AllCops:\n Include: lib/**/*.rb\n", "Include").unwrap();
896
+ let list =
897
+ all_cops_patterns("AllCops:\n Include:\n - lib/**/*.rb\n", "Include").unwrap();
898
+ for patterns in [&scalar, &list] {
899
+ assert!(patterns.matches_includes(Path::new("/p/lib/a.rb"), Path::new("/p")));
900
+ assert!(!patterns.matches_includes(Path::new("/p/tmp/t.rb"), Path::new("/p")));
901
+ }
902
+
903
+ // A key that is not written at all still has no list, which is a different thing from an
904
+ // empty one: `path_included` reads it as "every file".
905
+ assert!(all_cops_patterns("AllCops:\n Exclude: tmp/**/*\n", "Include").is_none());
906
+ // An empty list stays empty rather than turning into a one-element list of nothing.
907
+ let empty = all_cops_patterns("AllCops:\n Exclude: []\n", "Exclude").unwrap();
908
+ assert!(empty.is_empty());
909
+ }
910
+
432
911
  #[test]
433
912
  fn an_unusable_include_list_still_counts_as_configured() {
434
913
  let patterns = PathPatterns::compile(&owned(&["["]));
@@ -451,6 +930,10 @@ mod tests {
451
930
  "**/.*",
452
931
  "/abs/**/*",
453
932
  "/abs/x.rb",
933
+ // Absolute patterns that reach *into* the project root, which is the shape a hand
934
+ // written `Include`/`Exclude` takes and the one the relative-only matcher never saw.
935
+ "/p/**/*.rb",
936
+ "/p/.git/**/*",
454
937
  "spec/**/*_spec.rb",
455
938
  "a/*/c",
456
939
  "[",
@@ -505,6 +988,25 @@ mod tests {
505
988
  matches_any_reference(path, root, &selected),
506
989
  "excludes {path:?} against {selected:?}"
507
990
  );
991
+ // Selecting on `has_uppercase` has to mean the same thing as handing the
992
+ // matcher only the patterns written with a capital in the first place. The two
993
+ // reach the question by different routes -- one filters a compiled set, the
994
+ // other compiles a filtered list -- so they are worth holding against each
995
+ // other for every window.
996
+ let capitals: Vec<String> = selected
997
+ .iter()
998
+ .filter(|pattern| {
999
+ pattern
1000
+ .chars()
1001
+ .any(|character| character.is_ascii_uppercase())
1002
+ })
1003
+ .cloned()
1004
+ .collect();
1005
+ assert_eq!(
1006
+ compiled.matches_uppercase_includes(path, root),
1007
+ PathPatterns::compile(&capitals).matches_includes(path, root),
1008
+ "uppercase includes {path:?} against {selected:?}"
1009
+ );
508
1010
  }
509
1011
  }
510
1012
  }