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/build.rs ADDED
@@ -0,0 +1,155 @@
1
+ //! Records which commit the binary next to this build was made from.
2
+ //!
3
+ //! The fingerprint a measurement prints -- the binary's own hash -- answers "which binary", not
4
+ //! "whose fixes are in it". Those are different questions, and on 2026-08-17 the gap cost the team
5
+ //! five re-runs of a sixty-minute gate and two reports of bugs that had already been fixed on a
6
+ //! branch nobody's tree had merged. Each side was rigorous on its own: one printed the fingerprint,
7
+ //! the other reported how far its branch was ahead. Neither answers the question in between.
8
+ //!
9
+ //! Writing the commit beside the binary closes it without touching the CLI. `--version` stays
10
+ //! byte-identical to what RuboCop prints, which the drop-in replacement depends on, and nothing
11
+ //! reads this file at run time -- it exists for the measurement scripts, which can now say
12
+ //! `git merge-base --is-ancestor <fix> <sha>` instead of asking a person.
13
+ //!
14
+ //! The file lands next to the executable rather than inside `OUT_DIR` so that the scripts can find
15
+ //! it from the binary path alone; they are handed a path to `sonicop`, not a Cargo layout. A build
16
+ //! that cannot reach git writes `unknown` rather than failing: this is a convenience for measuring,
17
+ //! and a release built from a source tarball has no repository to ask.
18
+
19
+ use std::ffi::OsString;
20
+ use std::fs;
21
+ use std::io;
22
+ use std::path::{Path, PathBuf};
23
+ use std::process::Command;
24
+
25
+ fn main() {
26
+ // The commit marker changes only with git. The embedded cache fingerprint, however, must move
27
+ // with every input that can change diagnostics so a development build never accepts reports
28
+ // from different code merely because the package version stayed put.
29
+ println!("cargo:rerun-if-changed=.git/HEAD");
30
+ println!("cargo:rerun-if-changed=.git/refs");
31
+ for input in [
32
+ "Cargo.toml",
33
+ "Cargo.lock",
34
+ "build.rs",
35
+ "config/default.yml",
36
+ "src",
37
+ ] {
38
+ println!("cargo:rerun-if-changed={input}");
39
+ }
40
+ let fingerprint = build_fingerprint().expect("failed to fingerprint Sonicop build inputs");
41
+ println!("cargo:rustc-env=SONICOP_BUILD_FINGERPRINT={fingerprint}");
42
+
43
+ let Some(profile_dir) = profile_dir() else {
44
+ return;
45
+ };
46
+ let sha = head_sha().unwrap_or_else(|| "unknown".to_owned());
47
+ let dirty = match working_tree_is_dirty() {
48
+ true => " dirty",
49
+ false => "",
50
+ };
51
+ let _ = std::fs::write(
52
+ profile_dir.join(".sonicop-build-sha"),
53
+ format!("{sha}{dirty}\n"),
54
+ );
55
+ }
56
+
57
+ /// Stable identity of everything that can affect the executable's lint result.
58
+ ///
59
+ /// Hashing this once while compiling replaces reading and hashing the entire executable at every
60
+ /// startup. Besides source and embedded configuration, the lockfile covers dependency revisions;
61
+ /// compiler/target/cfg inputs cover builds whose source tree is byte-identical but semantics are
62
+ /// not. Test and documentation files are intentionally absent because they never enter the binary.
63
+ fn build_fingerprint() -> io::Result<blake3::Hash> {
64
+ let mut fingerprint = blake3::Hasher::new();
65
+ for input in [
66
+ "Cargo.toml",
67
+ "Cargo.lock",
68
+ "build.rs",
69
+ "config/default.yml",
70
+ "src",
71
+ ] {
72
+ hash_path(Path::new(input), &mut fingerprint)?;
73
+ }
74
+
75
+ let mut environment = std::env::vars_os()
76
+ .filter(|(name, _)| {
77
+ let name = name.to_string_lossy();
78
+ matches!(
79
+ name.as_ref(),
80
+ "TARGET" | "PROFILE" | "OPT_LEVEL" | "DEBUG" | "CARGO_ENCODED_RUSTFLAGS"
81
+ ) || name.starts_with("CARGO_CFG_")
82
+ || name.starts_with("CARGO_FEATURE_")
83
+ })
84
+ .collect::<Vec<(OsString, OsString)>>();
85
+ environment.sort_unstable();
86
+ for (name, value) in environment {
87
+ hash_part(&mut fingerprint, name.as_encoded_bytes());
88
+ hash_part(&mut fingerprint, value.as_encoded_bytes());
89
+ }
90
+
91
+ let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
92
+ let version = Command::new(rustc).arg("-vV").output()?;
93
+ if !version.status.success() {
94
+ return Err(io::Error::other("rustc -vV failed"));
95
+ }
96
+ hash_part(&mut fingerprint, &version.stdout);
97
+ Ok(fingerprint.finalize())
98
+ }
99
+
100
+ fn hash_path(path: &Path, fingerprint: &mut blake3::Hasher) -> io::Result<()> {
101
+ hash_part(fingerprint, path.as_os_str().as_encoded_bytes());
102
+ if path.is_dir() {
103
+ hash_part(fingerprint, b"directory");
104
+ let mut children = fs::read_dir(path)?
105
+ .collect::<Result<Vec<_>, _>>()?
106
+ .into_iter()
107
+ .map(|entry| entry.path())
108
+ .collect::<Vec<_>>();
109
+ children.sort_unstable();
110
+ for child in children {
111
+ hash_path(&child, fingerprint)?;
112
+ }
113
+ } else {
114
+ hash_part(fingerprint, b"file");
115
+ hash_part(fingerprint, &fs::read(path)?);
116
+ }
117
+ Ok(())
118
+ }
119
+
120
+ fn hash_part(fingerprint: &mut blake3::Hasher, bytes: &[u8]) {
121
+ fingerprint.update(&(bytes.len() as u64).to_le_bytes());
122
+ fingerprint.update(bytes);
123
+ }
124
+
125
+ /// `target/<profile>`, derived from where Cargo put this script's scratch space.
126
+ ///
127
+ /// `OUT_DIR` is `target/<profile>/build/<pkg>-<hash>/out`, so the profile directory is three levels
128
+ /// up. Cargo has no environment variable that names it directly.
129
+ fn profile_dir() -> Option<PathBuf> {
130
+ let out_dir = PathBuf::from(std::env::var_os("OUT_DIR")?);
131
+ Some(out_dir.ancestors().nth(3)?.to_path_buf())
132
+ }
133
+
134
+ fn head_sha() -> Option<String> {
135
+ let done = Command::new("git")
136
+ .args(["rev-parse", "--short", "HEAD"])
137
+ .output()
138
+ .ok()?;
139
+ match done.status.success() {
140
+ true => Some(String::from_utf8(done.stdout).ok()?.trim().to_owned()),
141
+ false => None,
142
+ }
143
+ }
144
+
145
+ /// Whether the tree the binary was built from carries uncommitted changes.
146
+ ///
147
+ /// **A commit alone would overstate what the binary contains.** Half of the re-runs on 2026-08-17
148
+ /// were of trees whose commit was right and whose working copy had moved on, so the scripts need to
149
+ /// see the difference between "built from this commit" and "built from this commit plus edits".
150
+ fn working_tree_is_dirty() -> bool {
151
+ Command::new("git")
152
+ .args(["status", "--porcelain"])
153
+ .output()
154
+ .is_ok_and(|done| done.status.success() && !done.stdout.is_empty())
155
+ }
@@ -15,7 +15,13 @@ module Sonicop
15
15
  MESSAGE
16
16
 
17
17
  begin
18
- exec(binary, *arguments)
18
+ # `[binary, binary]` = exec 形式の強制。`exec(binary, *arguments)` は arguments が空、
19
+ # つまり「カレントディレクトリを見る素の `sonicop`」という最も普通の呼び出しのとき
20
+ # `exec(String)` = commandline 形式に落ちる。この形式は空白で単語分割し、メタ文字が
21
+ # あればまるごと /bin/sh に渡すため、binary のパス次第で ENOENT や exit 127 になる。
22
+ # パスの出所は SONICOP_BINARY と gem の install 先で、どちらもメタ文字を含まない保証がない。
23
+ # argv0 は multi-arg 形式が既定で入れるものと同じ値を明示している。
24
+ exec([binary, binary], *arguments)
19
25
  rescue SystemCallError => error
20
26
  # 実行できない典型は「platform gem は入ったが実行環境と ABI が違う」ケース
21
27
  # (musl 環境に glibc ビルドが入るなど)。素の errno だけでは原因が読めない。
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Generated from Cargo.toml by `rake version:sync`. Do not edit by hand.
4
4
  module Sonicop
5
- VERSION = '26.8.111'
5
+ VERSION = '26.8.113'
6
6
  end
data/src/cli.rs CHANGED
@@ -11,8 +11,9 @@ use crate::config::{Config, ConfigStore};
11
11
  use crate::cop_name::{self, selector_matches};
12
12
  use crate::diagnostic::{FileReport, Offense, Severity};
13
13
  use crate::engine::{
14
- CorrectMode, NO_SYNTAX_GUARD, Selection, correct_file, discover_targets_with_store,
15
- inspect_files_with_store, inspect_source, is_mandatory_cop, offense_count, write_corrected,
14
+ CorrectMode, NO_SYNTAX_GUARD, ResultCache, Selection, correct_file,
15
+ discover_targets_with_store, inspect_files_with_store_cached, inspect_stdin, is_mandatory_cop,
16
+ offense_count, write_corrected,
16
17
  };
17
18
  use crate::formatter::{
18
19
  Format, FormatOptions, offenses_by_cop, render, smart_path, yaml_single_quoted,
@@ -238,9 +239,11 @@ impl Cli {
238
239
  }
239
240
 
240
241
  pub fn run() -> i32 {
241
- crate::profile::set_enabled(std::env::var_os("SONICOP_PROFILE").is_some());
242
+ // Sized from the registry: a fixed table silently drops every cop past its end.
243
+ let names: Vec<&'static str> = crate::rules::rule_names().collect();
244
+ crate::profile::set_enabled(std::env::var_os("SONICOP_PROFILE").is_some(), names.len());
242
245
  let code = run_inner();
243
- crate::profile::report(&crate::rules::rule_names().collect::<Vec<_>>());
246
+ crate::profile::report(&names);
244
247
  code
245
248
  }
246
249
 
@@ -304,10 +307,17 @@ fn output_paths_by_format(matches: &ArgMatches) -> Vec<Option<PathBuf>> {
304
307
 
305
308
  let mut slots = vec![None; format_positions.len().max(1)];
306
309
  for (out_position, path) in out_positions.into_iter().zip(out_paths) {
310
+ // `Options#parse`: `-o` attaches to the `-f` it follows. One written before any `-f` lands
311
+ // in `output_path` instead, and the only thing that reads it is `apply_default_formatter`'s
312
+ // `@options[:formatters] ||= [[format, output_path]]` -- a no-op once a `-f` was given. So
313
+ // with a `-f` present such a path is dropped, and the formatter writes to stdout; with none,
314
+ // it is where the default formatter writes.
307
315
  let slot = format_positions
308
316
  .iter()
309
- .rposition(|format_position| *format_position < out_position)
310
- .unwrap_or(0);
317
+ .rposition(|format_position| *format_position < out_position);
318
+ let Some(slot) = slot.or_else(|| format_positions.is_empty().then_some(0)) else {
319
+ continue;
320
+ };
311
321
  slots[slot].get_or_insert_with(|| path.clone());
312
322
  }
313
323
  slots
@@ -392,13 +402,28 @@ fn try_run(cli: Cli, outputs: &[Option<PathBuf>]) -> Result<i32> {
392
402
 
393
403
  let correct_mode = cli.correct_mode();
394
404
  let selection = build_selection(&cli, &config, correct_mode)?;
405
+ let result_cache = result_cache(&cli, &cwd, &selection, &config)?;
395
406
  if !cli.list_target_files {
396
407
  warn_unimplemented_enabled(&config, &selection);
397
408
  }
398
409
 
399
410
  let parallel = !cli.no_parallel && (cli.parallel || cli.stdin.is_none());
400
- let mut reports = match inspect_inputs(&cli, &cwd, &configs, &selection, parallel)? {
401
- Inspection::Reports(reports) => reports,
411
+ let inspection = inspect_inputs(
412
+ &cli,
413
+ &cwd,
414
+ &configs,
415
+ &selection,
416
+ parallel,
417
+ result_cache.as_ref(),
418
+ )?;
419
+ if let Some(cache) = &result_cache {
420
+ cache.prune();
421
+ }
422
+ let (mut reports, target_count) = match inspection {
423
+ Inspection::Reports {
424
+ reports,
425
+ target_count,
426
+ } => (reports, target_count),
402
427
  Inspection::ListedTargets => return Ok(0),
403
428
  };
404
429
 
@@ -426,22 +451,22 @@ fn try_run(cli: Cli, outputs: &[Option<PathBuf>]) -> Result<i32> {
426
451
  // A file RuboCop could not finish counts as a failed run even when nothing else offended.
427
452
  let failing = fail_level.failing(&reports) || had_errors;
428
453
 
454
+ filter_displayed_offenses(&mut reports, &cli, fail_level);
455
+ render_outputs(
456
+ &RenderRequest {
457
+ cli: &cli,
458
+ config: &config,
459
+ cwd: &cwd,
460
+ fail_level,
461
+ outputs,
462
+ corrected_count,
463
+ target_count,
464
+ elapsed: started.elapsed().as_secs_f64(),
465
+ },
466
+ &reports,
467
+ )?;
429
468
  if let Some(corrected) = stdin_corrected {
430
- print!("{corrected}");
431
- } else {
432
- filter_displayed_offenses(&mut reports, &cli, fail_level);
433
- render_outputs(
434
- &RenderRequest {
435
- cli: &cli,
436
- config: &config,
437
- cwd: &cwd,
438
- fail_level,
439
- outputs,
440
- corrected_count,
441
- elapsed: started.elapsed().as_secs_f64(),
442
- },
443
- &reports,
444
- )?;
469
+ print_corrected_stdin(&cli, &corrected)?;
445
470
  }
446
471
 
447
472
  Ok(i32::from(failing))
@@ -471,14 +496,69 @@ fn build_selection(cli: &Cli, config: &Config, correct_mode: CorrectMode) -> Res
471
496
  ignore_disable_comments: cli.ignore_disable_comments,
472
497
  display_suppressed: cli.display_suppressed,
473
498
  correcting: correct_mode != CorrectMode::None,
499
+ // `LSP.enabled?`. `--lsp` sets it too, but that mode returns before anything is inspected,
500
+ // so `--editor-mode` is the only way it reaches a cop from here.
501
+ editor_mode: cli.editor_mode,
474
502
  // **No flag turns this on from the command line.** The guard is switched off for tests
475
503
  // only, and even then per case; a run reaches the environment variable or nothing.
476
504
  skip_syntax_guard: false,
477
505
  })
478
506
  }
479
507
 
508
+ fn result_cache(
509
+ cli: &Cli,
510
+ cwd: &Path,
511
+ selection: &Selection,
512
+ config: &Config,
513
+ ) -> Result<Option<ResultCache>> {
514
+ // Profiling reports what inspecting the files costs, so it serves cached reports to nobody --
515
+ // unless the run asks for the cache in as many words, which is how the cost of a cache *hit*
516
+ // gets measured at all.
517
+ let profiling = std::env::var_os("SONICOP_PROFILE").is_some();
518
+ let enabled = cli.cache.as_deref() != Some("false")
519
+ && !selection.correcting
520
+ && cli.stdin.is_none()
521
+ && !cli.profile
522
+ && !cli.memory
523
+ && (!profiling || cli.cache.as_deref() == Some("true"));
524
+ if !enabled {
525
+ return Ok(None);
526
+ }
527
+ let root = cli
528
+ .cache_root
529
+ .clone()
530
+ .unwrap_or_else(|| default_cache_root(cwd));
531
+ let max_files = config.all_cops_value("MaxFilesInCache").unwrap_or(20_000);
532
+ if max_files == 0 {
533
+ return Ok(None);
534
+ }
535
+ ResultCache::new(root, config.path_base(), selection, max_files).map(Some)
536
+ }
537
+
538
+ fn default_cache_root(cwd: &Path) -> PathBuf {
539
+ if let Some(root) = std::env::var_os("XDG_CACHE_HOME") {
540
+ return PathBuf::from(root).join("sonicop");
541
+ }
542
+ if cfg!(target_os = "macos")
543
+ && let Some(home) = std::env::var_os("HOME")
544
+ {
545
+ return PathBuf::from(home).join("Library/Caches/sonicop");
546
+ }
547
+ if let Some(home) = std::env::var_os("HOME") {
548
+ return PathBuf::from(home).join(".cache/sonicop");
549
+ }
550
+ cwd.join(".sonicop-cache")
551
+ }
552
+
480
553
  enum Inspection {
481
- Reports(Vec<FileReport>),
554
+ Reports {
555
+ reports: Vec<FileReport>,
556
+ /// What discovery produced, which `--fail-fast` leaves larger than the number of reports.
557
+ /// `JSONFormatter` takes `target_file_count` from `started(target_files)` and
558
+ /// `inspected_file_count` from `finished(inspected_files)`, so the two are separate counts
559
+ /// and only a run that inspects everything it found makes them equal.
560
+ target_count: usize,
561
+ },
482
562
  ListedTargets,
483
563
  }
484
564
 
@@ -488,6 +568,7 @@ fn inspect_inputs(
488
568
  configs: &ConfigStore,
489
569
  selection: &Selection,
490
570
  parallel: bool,
571
+ cache: Option<&ResultCache>,
491
572
  ) -> Result<Inspection> {
492
573
  let Some(stdin_path) = &cli.stdin else {
493
574
  let mut targets = discover_targets_with_store(
@@ -503,6 +584,9 @@ fn inspect_inputs(
503
584
  }
504
585
  return Ok(Inspection::ListedTargets);
505
586
  }
587
+ if let Some(cache) = cache {
588
+ cache.prepare(targets.len());
589
+ }
506
590
  let reports = if cli.fail_fast {
507
591
  targets.sort_by_key(|path| {
508
592
  std::cmp::Reverse(
@@ -511,28 +595,40 @@ fn inspect_inputs(
511
595
  .ok(),
512
596
  )
513
597
  });
514
- inspect_fail_fast(&targets, configs, selection)?
598
+ inspect_fail_fast(&targets, configs, selection, cache)?
515
599
  } else {
516
- inspect_files_with_store(&targets, configs, selection, parallel)?
600
+ inspect_files_with_store_cached(&targets, configs, selection, parallel, cache)?
517
601
  };
518
- return Ok(Inspection::Reports(reports));
602
+ return Ok(Inspection::Reports {
603
+ reports,
604
+ target_count: targets.len(),
605
+ });
519
606
  };
520
607
 
521
608
  if !cli.paths.is_empty() {
522
609
  bail!("--stdin requires exactly one path supplied as its argument and no file arguments");
523
610
  }
524
- let mut text = String::new();
611
+ // Read as bytes, not as text: `$stdin.binmode.read` (`options.rb:46`) is what RuboCop reads,
612
+ // and a source that is not valid UTF-8 is an offense to report rather than a run to abort.
613
+ let mut bytes = Vec::new();
525
614
  io::stdin()
526
- .read_to_string(&mut text)
527
- .context("failed to read UTF-8 source from stdin")?;
615
+ .read_to_end(&mut bytes)
616
+ .context("failed to read source from stdin")?;
528
617
  let target_config = configs.for_path(stdin_path)?;
529
- let report = inspect_source(stdin_path.clone(), text, &target_config, selection)?;
530
- Ok(Inspection::Reports(vec![report]))
618
+ let report = inspect_stdin(stdin_path.clone(), bytes, &target_config, selection)?;
619
+ Ok(Inspection::Reports {
620
+ reports: vec![report],
621
+ target_count: 1,
622
+ })
531
623
  }
532
624
 
533
625
  struct CorrectionRun {
534
626
  reports: Vec<FileReport>,
535
627
  corrected_count: usize,
628
+ /// The buffer `--stdin` is left holding, which is `@options[:stdin]` after the run: the
629
+ /// corrected source when a cop rewrote it, and the source that was read when none did.
630
+ /// `Team#autocorrect` (`cop/team.rb:175-178`) only assigns it on a rewrite, so the unchanged
631
+ /// case is not an absence -- it is the original buffer, and upstream prints that too.
536
632
  stdin_corrected: Option<String>,
537
633
  had_errors: bool,
538
634
  }
@@ -566,12 +662,12 @@ fn apply_corrections(
566
662
  eprintln!("{message}");
567
663
  had_errors = true;
568
664
  }
569
- if outcome.rewritten {
570
- if cli.stdin.is_some() {
665
+ if cli.stdin.is_some() {
666
+ if correct_mode != CorrectMode::None {
571
667
  stdin_corrected = Some(outcome.text);
572
- } else {
573
- write_corrected(&path, &outcome.text)?;
574
668
  }
669
+ } else if outcome.rewritten {
670
+ write_corrected(&path, &outcome.text)?;
575
671
  }
576
672
  corrected_reports.push(outcome.report);
577
673
  }
@@ -611,15 +707,26 @@ impl FailLevel {
611
707
  }
612
708
  }
613
709
 
710
+ /// `Runner#considered_failure?` (`runner.rb:561-569`).
711
+ ///
712
+ /// `autocorrect` adds a reason to fail; it does not replace the one every run has. Upstream
713
+ /// returns early for a correctable offense **and then falls through** to the severity
714
+ /// comparison, so an uncorrectable offense at or above the threshold still fails the run.
715
+ /// Reading the pseudo level as "correctable offenses only" instead lets `--fail-level
716
+ /// autocorrect` exit 0 on a `Metrics/MethodLength` no cop can fix -- a green CI on code
717
+ /// RuboCop fails, which is the one direction a linter must never be wrong in.
614
718
  fn failing(self, reports: &[FileReport]) -> bool {
615
- match self {
616
- // A correctable offense fails even once it has been corrected.
617
- Self::Autocorrect => reports
719
+ // `minimum_severity_to_fail` reads `autocorrect` as `:refactor`, which [`Self::severity`]
720
+ // already does, so the threshold applies whichever level was asked for.
721
+ if offense_count(reports, self.severity()) > 0 {
722
+ return true;
723
+ }
724
+ // A correctable offense fails even once it has been corrected.
725
+ self == Self::Autocorrect
726
+ && reports
618
727
  .iter()
619
728
  .flat_map(|report| &report.offenses)
620
- .any(Offense::is_correctable),
621
- Self::Severity(severity) => offense_count(reports, severity) > 0,
622
- }
729
+ .any(Offense::is_correctable)
623
730
  }
624
731
  }
625
732
 
@@ -743,11 +850,17 @@ fn inspect_fail_fast(
743
850
  paths: &[PathBuf],
744
851
  configs: &ConfigStore,
745
852
  selection: &Selection,
853
+ cache: Option<&ResultCache>,
746
854
  ) -> Result<Vec<FileReport>> {
747
855
  let mut reports = Vec::new();
748
856
  for path in paths {
749
- let mut inspected =
750
- inspect_files_with_store(std::slice::from_ref(path), configs, selection, false)?;
857
+ let mut inspected = inspect_files_with_store_cached(
858
+ std::slice::from_ref(path),
859
+ configs,
860
+ selection,
861
+ false,
862
+ cache,
863
+ )?;
751
864
  let has_offense = inspected
752
865
  .first()
753
866
  .is_some_and(|report| !report.offenses.is_empty());
@@ -944,7 +1057,24 @@ fn generate_todo(reports: &[FileReport], cwd: &Path, cli: &Cli) -> Result<()> {
944
1057
  }
945
1058
  fs::write(cwd.join(".rubocop_todo.yml"), output)?;
946
1059
  let root_config = cwd.join(".rubocop.yml");
947
- let existing = fs::read_to_string(&root_config).unwrap_or_default();
1060
+ // `add_inheritance_from_auto_generated_file` reads the file only after `File.exist?` says
1061
+ // there is one, so "absent" and "unreadable" are separate answers and only the first means
1062
+ // "write a new one". Collapsing them -- `unwrap_or_default()` -- turns every read failure into
1063
+ // an empty string, and the prepend below silently becomes a replace: a `.rubocop.yml` that is
1064
+ // not valid UTF-8, or that the process cannot open, is overwritten by the one line this would
1065
+ // have added to it, with a zero exit code and nothing on stderr. Fail instead.
1066
+ let existing = match fs::read_to_string(&root_config) {
1067
+ Ok(existing) => existing,
1068
+ Err(error) if error.kind() == io::ErrorKind::NotFound => String::new(),
1069
+ Err(error) => {
1070
+ return Err(error).with_context(|| {
1071
+ format!(
1072
+ "failed to read {}; refusing to replace it with the generated inheritance",
1073
+ root_config.display()
1074
+ )
1075
+ });
1076
+ }
1077
+ };
948
1078
  if !existing.contains(".rubocop_todo.yml") {
949
1079
  let addition = if existing.is_empty() {
950
1080
  "inherit_from: .rubocop_todo.yml\n".to_owned()
@@ -979,6 +1109,9 @@ struct RenderRequest<'a> {
979
1109
  /// Destination per format, already paired the way RuboCop pairs `--out` with `--format`.
980
1110
  outputs: &'a [Option<PathBuf>],
981
1111
  corrected_count: usize,
1112
+ /// How many files discovery produced, which is not how many were inspected once `--fail-fast`
1113
+ /// stops the run early. See [`Inspection::Reports`].
1114
+ target_count: usize,
982
1115
  elapsed: f64,
983
1116
  }
984
1117
 
@@ -990,6 +1123,7 @@ fn render_outputs(request: &RenderRequest<'_>, reports: &[FileReport]) -> Result
990
1123
  fail_level,
991
1124
  outputs,
992
1125
  corrected_count,
1126
+ target_count,
993
1127
  elapsed,
994
1128
  } = *request;
995
1129
  let formats = if cli.formats.is_empty() {
@@ -1021,6 +1155,7 @@ fn render_outputs(request: &RenderRequest<'_>, reports: &[FileReport]) -> Result
1021
1155
  extra_details: cli.extra_details,
1022
1156
  color,
1023
1157
  corrected_count,
1158
+ target_file_count: target_count,
1024
1159
  fail_level: fail_level.severity(),
1025
1160
  safe_autocorrect: cli.correct_mode() == CorrectMode::Safe,
1026
1161
  display_only_failed: cli.display_only_failed,
@@ -1038,6 +1173,42 @@ fn render_outputs(request: &RenderRequest<'_>, reports: &[FileReport]) -> Result
1038
1173
  Ok(())
1039
1174
  }
1040
1175
 
1176
+ /// `ExecuteRunner::INTEGRATION_FORMATTERS`: the formats whose consumers parse the whole stream, so
1177
+ /// nothing may be appended to them. Both the short and the long spelling of each.
1178
+ const INTEGRATION_FORMATTERS: [&str; 6] = ["h", "html", "j", "json", "ju", "junit"];
1179
+
1180
+ /// Appends the source `--stdin` was left holding to a formatter's report, the way
1181
+ /// `ExecuteRunner#maybe_print_corrected_source` (`cli/command/execute_runner.rb:92-102`) does.
1182
+ ///
1183
+ /// An editor driving `--stdin -a` reads the corrected buffer off the end of the output, which is
1184
+ /// what the twenty `=` mark the start of. The report still comes first: printing only the buffer
1185
+ /// leaves nothing to say what was corrected, and with `--format json` it puts raw Ruby where a
1186
+ /// caller asked for JSON and will try to parse it.
1187
+ ///
1188
+ /// Two details are upstream's rather than chosen here. The check is against `@options[:format]`,
1189
+ /// which the option parser assigns per `-f`, so it is the **last** format on the command line that
1190
+ /// decides -- `-f json -f simple` appends. And `--stderr` moves the separator but not the source:
1191
+ /// upstream picks the stream for the `puts` and then `print`s to `$stdout` unconditionally, by
1192
+ /// which point the redirect around the runner has already been undone.
1193
+ fn print_corrected_stdin(cli: &Cli, corrected: &str) -> Result<()> {
1194
+ if cli
1195
+ .formats
1196
+ .last()
1197
+ .is_some_and(|format| INTEGRATION_FORMATTERS.contains(&format.as_str()))
1198
+ {
1199
+ return Ok(());
1200
+ }
1201
+ let separator = format!("{}\n", "=".repeat(20));
1202
+ if cli.stderr {
1203
+ io::stderr().write_all(separator.as_bytes())?;
1204
+ } else {
1205
+ io::stdout().write_all(separator.as_bytes())?;
1206
+ }
1207
+ io::stdout().write_all(corrected.as_bytes())?;
1208
+ io::stdout().flush()?;
1209
+ Ok(())
1210
+ }
1211
+
1041
1212
  fn init_config(cwd: &Path) -> Result<i32> {
1042
1213
  let path = cwd.join(".rubocop.yml");
1043
1214
  if path.exists() {