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/rules/mod.rs CHANGED
@@ -1,13 +1,13 @@
1
1
  use std::cell::OnceCell;
2
2
  use std::collections::HashMap;
3
3
  use std::ops::Range;
4
+ use std::sync::LazyLock;
4
5
 
5
6
  use tree_sitter::Node;
6
7
 
7
8
  use crate::rules::lint::variable_force::Analysis;
8
9
  use crate::rules::metrics::fragments::Fragments;
9
10
  use crate::rules::metrics::locals::Locals;
10
- use crate::rules::naming::support::Variables;
11
11
 
12
12
  use crate::config::Config;
13
13
  use crate::diagnostic::{Offense, Severity};
@@ -60,7 +60,7 @@ mod style;
60
60
  pub(crate) mod support;
61
61
  mod visibility;
62
62
 
63
- pub(crate) use support::{push_named_children, walk_named};
63
+ pub(crate) use support::{push_named_children, push_named_children_in, walk_named};
64
64
 
65
65
  /// A cop: its qualified name, the severity it reports at by default, and the function that
66
66
  /// inspects one file.
@@ -135,15 +135,14 @@ pub(crate) struct RuleContext<'a> {
135
135
  /// The context is reused across every cop of a file for this reason; see
136
136
  /// [`Self::inspecting_with`].
137
137
  analysis: OnceCell<Analysis<'a>>,
138
- /// The Naming department's own reading of which names are variables, run at most once per
139
- /// file. Five cops ask for it, and like [`Self::variable_analysis`] it depends on nothing but
140
- /// the tree and the source.
141
- variables: OnceCell<Variables>,
142
138
  /// The code the grammar swallowed, recovered once per file. The three complexity cops each
143
139
  /// used to recover it for themselves.
144
140
  fragments: OnceCell<Fragments>,
145
141
  /// Which identifiers the Metrics cops read as local variables, replayed once per file.
146
142
  metric_locals: OnceCell<Locals>,
143
+ /// The lexer token stream reconstructed from the tree, shared by the Layout and Style cops
144
+ /// that inspect neighbouring tokens rather than syntax nodes.
145
+ layout_tokens: OnceCell<Vec<layout::tokens::Token>>,
147
146
  /// The cops the run switches off through the configuration, which is what an `enable` directive
148
147
  /// has to undo.
149
148
  ///
@@ -185,9 +184,9 @@ impl<'a> RuleContext<'a> {
185
184
  correcting,
186
185
  directive_review: None,
187
186
  analysis: OnceCell::new(),
188
- variables: OnceCell::new(),
189
187
  fragments: OnceCell::new(),
190
188
  metric_locals: OnceCell::new(),
189
+ layout_tokens: OnceCell::new(),
191
190
  disabled_cops: &[],
192
191
  }
193
192
  }
@@ -214,25 +213,37 @@ impl<'a> RuleContext<'a> {
214
213
 
215
214
  /// RuboCop's `VariableForce` result for this file, computed on the first cop that asks.
216
215
  pub(in crate::rules) fn variable_analysis(&self) -> &Analysis<'a> {
217
- self.analysis
218
- .get_or_init(|| Analysis::run(self.ast.root, self.source))
219
- }
220
-
221
- /// Which names in the file are variables, as the Naming cops read them.
222
- pub(in crate::rules) fn variable_roles(&self) -> &Variables {
223
- self.variables
224
- .get_or_init(|| Variables::resolve(self.ast.root, self.source))
216
+ self.analysis.get_or_init(|| {
217
+ crate::profile::phase(crate::profile::Phase::Variables, || {
218
+ Analysis::run(self.ast, self.source)
219
+ })
220
+ })
225
221
  }
226
222
 
227
223
  /// The code the grammar read as something other than code, recovered once per file.
228
224
  pub(in crate::rules) fn fragments(&self) -> &Fragments {
229
- self.fragments.get_or_init(|| Fragments::new(self))
225
+ self.fragments.get_or_init(|| {
226
+ crate::profile::phase(crate::profile::Phase::Fragments, || Fragments::new(self))
227
+ })
230
228
  }
231
229
 
232
230
  /// Which identifiers the Metrics cops read as local variables.
233
231
  pub(in crate::rules) fn metric_locals(&self) -> &Locals {
234
- self.metric_locals
235
- .get_or_init(|| Locals::new(self, self.fragments()))
232
+ self.metric_locals.get_or_init(|| {
233
+ crate::profile::phase(crate::profile::Phase::MetricLocals, || {
234
+ Locals::new(self, self.fragments())
235
+ })
236
+ })
237
+ }
238
+
239
+ /// RuboCop's lexer token stream for this file, reconstructed at most once however many cops
240
+ /// inspect it.
241
+ pub(in crate::rules) fn layout_tokens(&self) -> &[layout::tokens::Token] {
242
+ self.layout_tokens.get_or_init(|| {
243
+ crate::profile::phase(crate::profile::Phase::LayoutTokens, || {
244
+ layout::tokens::build(self)
245
+ })
246
+ })
236
247
  }
237
248
  }
238
249
 
@@ -309,6 +320,38 @@ impl<'a> RuleContext<'a> {
309
320
  self.ast.root
310
321
  }
311
322
 
323
+ /// The file's node index, for the helpers that answer structural questions -- a parent above
324
+ /// all -- outside a cop's own `check`.
325
+ pub(in crate::rules) fn ast_index(&self) -> &'a AstIndex<'a> {
326
+ self.ast
327
+ }
328
+
329
+ /// `Node::children` for a node of this file, answered from the index. See
330
+ /// [`AstIndex::children_of`].
331
+ pub(in crate::rules) fn children<'node>(
332
+ &'node self,
333
+ node: Node<'node>,
334
+ ) -> Option<Children<'node>> {
335
+ self.ast.children_of(node)
336
+ }
337
+
338
+ /// `Node::named_children` for a node of this file, answered from the index. See
339
+ /// [`AstIndex::named_children_of`].
340
+ pub(in crate::rules) fn named_children<'node>(
341
+ &'node self,
342
+ node: Node<'node>,
343
+ ) -> Option<&'node [Node<'node>]> {
344
+ self.ast.named_children_of(node)
345
+ }
346
+
347
+ /// Every named node of `node`'s subtree, `node` first. See [`AstIndex::named_descendants`].
348
+ pub(in crate::rules) fn named_descendants<'node>(
349
+ &'node self,
350
+ node: Node<'node>,
351
+ ) -> Option<&'node [Node<'node>]> {
352
+ self.ast.named_descendants(node)
353
+ }
354
+
312
355
  pub fn nodes(&self) -> impl Iterator<Item = Node<'a>> + '_ {
313
356
  self.ast.nodes.iter().copied()
314
357
  }
@@ -325,13 +368,30 @@ impl<'a> RuleContext<'a> {
325
368
  /// The named nodes of any of `kinds`, in source order. The kinds are indexed separately, so
326
369
  /// their positions have to be merged to put the nodes back in the order a cop that scans the
327
370
  /// whole file would have seen them in.
371
+ ///
372
+ /// A single kind is by far the common case and is handed straight through: the per-kind lists
373
+ /// are already in source order, so there is nothing to merge and **the list is borrowed**.
374
+ /// Copying it out cost an allocation and a memcpy of the whole list on every cop of every
375
+ /// file, and the list is thousands of entries wide for a kind as ordinary as `call`.
328
376
  pub fn nodes_of_any(&self, kinds: &[&str]) -> impl Iterator<Item = Node<'a>> + '_ {
329
- let mut indices: Vec<u32> = kinds
330
- .iter()
331
- .flat_map(|kind| self.ast.of_kind(kind))
332
- .collect();
333
- indices.sort_unstable();
334
- indices.into_iter().map(|index| self.ast.named_node(index))
377
+ let positions: std::borrow::Cow<'_, [u32]> = match kinds {
378
+ [only] => std::borrow::Cow::Borrowed(self.ast.slice_of_kind(only)),
379
+ _ => {
380
+ // One allocation of the right size. `collect` from a `flat_map` cannot see the
381
+ // total ahead of time and grows the vector as it goes.
382
+ let total: usize = kinds
383
+ .iter()
384
+ .map(|kind| self.ast.slice_of_kind(kind).len())
385
+ .sum();
386
+ let mut indices = Vec::with_capacity(total);
387
+ for kind in kinds {
388
+ indices.extend_from_slice(self.ast.slice_of_kind(kind));
389
+ }
390
+ indices.sort_unstable();
391
+ std::borrow::Cow::Owned(indices)
392
+ }
393
+ };
394
+ (0..positions.len()).map(move |at| self.ast.named_node(positions[at]))
335
395
  }
336
396
 
337
397
  pub fn protected_ranges(&self) -> &[Range<usize>] {
@@ -372,45 +432,184 @@ const PROTECTED_LITERAL_KINDS: &[&str] = &[
372
432
  "bare_string",
373
433
  ];
374
434
 
435
+ /// FNV-1a over the node kind names.
436
+ ///
437
+ /// The keys are tree-sitter's kind strings -- short, ASCII, and looked up once per cop per file,
438
+ /// which is hundreds of thousands of times in a run. SipHash's resistance to collision attacks
439
+ /// buys nothing against a fixed set of names the grammar chose, and its setup costs more than the
440
+ /// hash of a ten-character string.
441
+ #[derive(Default)]
442
+ pub(crate) struct KindHasher(u64);
443
+
444
+ impl std::hash::Hasher for KindHasher {
445
+ fn finish(&self) -> u64 {
446
+ self.0
447
+ }
448
+
449
+ fn write(&mut self, bytes: &[u8]) {
450
+ const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
451
+ const PRIME: u64 = 0x0000_0100_0000_01b3;
452
+ let mut hash = if self.0 == 0 { OFFSET } else { self.0 };
453
+ for byte in bytes {
454
+ hash ^= u64::from(*byte);
455
+ hash = hash.wrapping_mul(PRIME);
456
+ }
457
+ self.0 = hash;
458
+ }
459
+ }
460
+
461
+ type KindMap = HashMap<&'static str, Vec<u32>, std::hash::BuildHasherDefault<KindHasher>>;
462
+
463
+ /// The hash for a node id, which is the address tree-sitter stored the subtree at.
464
+ ///
465
+ /// The keys are pointers into one arena: already well spread above the alignment, and never
466
+ /// chosen by anything but the parser. Multiplying by the 64-bit golden ratio moves the low bits
467
+ /// that alignment fixes into the top of the word, which is where the table reads from -- SipHash's
468
+ /// four rounds buy nothing here and are asked for on every node of every file.
469
+ #[derive(Default)]
470
+ pub(crate) struct IdHasher(u64);
471
+
472
+ impl std::hash::Hasher for IdHasher {
473
+ fn finish(&self) -> u64 {
474
+ self.0
475
+ }
476
+
477
+ fn write(&mut self, bytes: &[u8]) {
478
+ for byte in bytes {
479
+ self.0 = (self.0 ^ u64::from(*byte)).wrapping_mul(0x9e37_79b9_7f4a_7c15);
480
+ }
481
+ }
482
+
483
+ fn write_usize(&mut self, value: usize) {
484
+ self.0 = (value as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
485
+ }
486
+ }
487
+
488
+ type IdMap = HashMap<usize, u32, std::hash::BuildHasherDefault<IdHasher>>;
489
+
490
+ /// The hasher every id-keyed table should be built with. See [`IdHasher`].
491
+ pub(crate) type IdHash = std::hash::BuildHasherDefault<IdHasher>;
492
+
493
+ /// A set of node ids.
494
+ pub(crate) type IdSet = std::collections::HashSet<usize, IdHash>;
495
+
496
+ /// A map from a node id.
497
+ pub(crate) type IdKeyed<V> = HashMap<usize, V, IdHash>;
498
+
375
499
  pub(crate) struct AstIndex<'tree> {
376
500
  root: Node<'tree>,
377
501
  nodes: Vec<Node<'tree>>,
378
502
  named_nodes: Vec<Node<'tree>>,
379
503
  /// Positions in `named_nodes` grouped by node kind, each list in source order. Indices rather
380
504
  /// than nodes because a `Node` is eight times the size of the `u32` that finds it.
381
- by_kind: HashMap<&'static str, Vec<u32>>,
505
+ by_kind: KindMap,
382
506
  protected_ranges: Vec<Range<usize>>,
383
507
  heredoc_ranges: Vec<Range<usize>>,
384
508
  comment_ranges: Vec<Range<usize>>,
385
- /// Each node's parent, as its position in `nodes`, keyed by the node's own id. [`NO_PARENT`]
386
- /// stands for the root, which has none.
509
+ /// Where each node sits in `nodes`, keyed by the node's own id. Both structural answers below
510
+ /// start here.
511
+ positions: IdMap,
512
+ /// Each node's parent, as its position in `nodes`. [`NO_PARENT`] stands for the root.
387
513
  ///
388
514
  /// `Node::parent` walks down from the root of the tree comparing byte ranges, which costs a
389
515
  /// pass over the children of every ancestor -- 43% of a run over RuboCop's own tree once the
390
516
  /// cheaper accessors were dealt with. The walk that builds this index already knows every
391
517
  /// node's parent, so recording it turns the question into one hash lookup.
518
+ parent_of: Vec<u32>,
519
+ /// Every node's named children, laid end to end, with `child_start[position]` naming where a
520
+ /// node's run begins and `child_start[position + 1]` where it ends.
521
+ ///
522
+ /// `Node::named_children` opens a tree cursor and collects into a fresh `Vec` on every call.
523
+ /// Both showed at the top of a sampling profile: the cursor iteration was the largest single
524
+ /// cost of a run and the collect the largest allocation. The walk that fills `nodes` already
525
+ /// visits every child, so the list is recorded rather than rebuilt.
526
+ named_children: Vec<Node<'tree>>,
527
+ child_start: Vec<u32>,
528
+ /// Where each node sits in `named_nodes`, or [`NOT_NAMED`] for one that is not named.
529
+ named_index: Vec<u32>,
530
+ /// The field each node fills in its parent, as the grammar's field id, or 0 for a node that
531
+ /// fills none.
392
532
  ///
393
- /// Deferred because a run narrowed to a handful of cops may never ask. The map holds no
394
- /// borrows, so it does not make `AstIndex` invariant the way a cache of nodes would.
395
- parents: OnceCell<HashMap<usize, u32>>,
533
+ /// Answering "which field is this child" by walking the parent's children with a cursor costs
534
+ /// one walk per question, and the cops that classify identifiers ask on every identifier of
535
+ /// every file. The walk that builds the index passes each node once with a cursor that already
536
+ /// knows the answer.
537
+ field_of: Vec<u32>,
538
+ /// How many nodes each node's subtree holds, itself included. `nodes` is in pre-order, so a
539
+ /// node's own children are found by stepping over one subtree at a time from the node after
540
+ /// it -- which is what lets [`Self::children_of`] answer without a tree cursor.
541
+ subtree_len: Vec<u32>,
542
+ /// How many named nodes each node's subtree holds, itself included.
543
+ ///
544
+ /// `named_nodes` is in pre-order, so a node's descendants are the run that begins where the
545
+ /// node itself sits and is this long -- which is what turns "every named node below this one"
546
+ /// from a stack-driven walk into a slice.
547
+ named_subtree: Vec<u32>,
548
+ }
549
+
550
+ /// One node's children, walked through the index's pre-order table. See
551
+ /// [`AstIndex::children_of`].
552
+ pub(in crate::rules) struct Children<'a> {
553
+ nodes: &'a [Node<'a>],
554
+ subtree_len: &'a [u32],
555
+ next: usize,
556
+ end: usize,
396
557
  }
397
558
 
398
- /// The value [`AstIndex::parents`] carries for the root.
559
+ impl<'a> Iterator for Children<'a> {
560
+ type Item = Node<'a>;
561
+
562
+ fn next(&mut self) -> Option<Node<'a>> {
563
+ if self.next >= self.end {
564
+ return None;
565
+ }
566
+ let node = self.nodes[self.next];
567
+ self.next += self.subtree_len[self.next] as usize;
568
+ Some(node)
569
+ }
570
+ }
571
+
572
+ /// Every field name the grammar has, by field id, so a recorded id can be turned back into the
573
+ /// `&'static str` the cops compare against.
574
+ static FIELD_NAMES: LazyLock<Vec<Option<&'static str>>> = LazyLock::new(|| {
575
+ let language = node_ext::language();
576
+ (0..=language.field_count() as u16)
577
+ .map(|id| language.field_name_for_id(id))
578
+ .collect()
579
+ });
580
+
581
+ /// The value [`AstIndex::parent_of`] carries for the root.
399
582
  const NO_PARENT: u32 = u32::MAX;
400
583
 
584
+ /// The value [`AstIndex::named_index`] carries for a node that is not named.
585
+ const NOT_NAMED: u32 = u32::MAX;
586
+
401
587
  impl<'tree> AstIndex<'tree> {
402
588
  pub fn new(root: Node<'tree>) -> Self {
589
+ // The tree knows how many nodes it holds, so every per-node table is allocated once at the
590
+ // right size. Growing them by doubling costs a dozen reallocations and copies per file,
591
+ // and eight workers doing that at once is contention on the allocator rather than work.
592
+ let count = root.descendant_count();
403
593
  let mut index = Self {
404
594
  root,
405
- nodes: Vec::new(),
406
- named_nodes: Vec::new(),
407
- by_kind: HashMap::new(),
595
+ nodes: Vec::with_capacity(count),
596
+ named_nodes: Vec::with_capacity(count),
597
+ by_kind: KindMap::default(),
408
598
  protected_ranges: Vec::new(),
409
599
  heredoc_ranges: Vec::new(),
410
600
  comment_ranges: Vec::new(),
411
- parents: OnceCell::new(),
601
+ positions: IdMap::with_capacity_and_hasher(count, Default::default()),
602
+ parent_of: Vec::with_capacity(count),
603
+ named_children: Vec::new(),
604
+ child_start: Vec::new(),
605
+ named_index: Vec::with_capacity(count),
606
+ field_of: Vec::with_capacity(count),
607
+ subtree_len: Vec::new(),
608
+ named_subtree: Vec::new(),
412
609
  };
413
610
  index.collect(root);
611
+ index.index_children();
612
+ index.index_subtrees();
414
613
  index.protected_ranges.sort_by_key(|range| range.start);
415
614
  merge_touching_ranges(&mut index.protected_ranges);
416
615
  index.heredoc_ranges.sort_by_key(|range| range.start);
@@ -422,11 +621,12 @@ impl<'tree> AstIndex<'tree> {
422
621
  }
423
622
 
424
623
  fn of_kind(&self, kind: &str) -> impl Iterator<Item = u32> + '_ {
425
- self.by_kind
426
- .get(kind)
427
- .map_or(&[][..], Vec::as_slice)
428
- .iter()
429
- .copied()
624
+ self.slice_of_kind(kind).iter().copied()
625
+ }
626
+
627
+ /// The positions for one kind, in source order.
628
+ fn slice_of_kind(&self, kind: &str) -> &[u32] {
629
+ self.by_kind.get(kind).map_or(&[][..], Vec::as_slice)
430
630
  }
431
631
 
432
632
  fn named_node(&self, index: u32) -> Node<'tree> {
@@ -439,44 +639,164 @@ impl<'tree> AstIndex<'tree> {
439
639
  /// `Metrics`' recovered fragments are -- is asked of the parser itself, so the answer is the
440
640
  /// one `Node::parent` would have given whatever tree the node came from.
441
641
  fn parent<'node>(&'node self, node: Node<'node>) -> Option<Node<'node>> {
442
- match self
443
- .parents
444
- .get_or_init(|| self.index_parents())
445
- .get(&node.id())
446
- {
447
- Some(&NO_PARENT) => None,
448
- Some(&index) => Some(self.nodes[index as usize]),
642
+ match self.positions.get(&node.id()) {
643
+ Some(&position) => match self.parent_of[position as usize] {
644
+ NO_PARENT => None,
645
+ index => Some(self.nodes[index as usize]),
646
+ },
449
647
  None => node.parent(),
450
648
  }
451
649
  }
452
650
 
453
- /// Walks the tree once more, recording where each node hangs. The walk that filled `nodes`
454
- /// cannot do it: most runs never ask, and a file's nodes outnumber the parents any one cop
455
- /// looks up.
456
- fn index_parents(&self) -> HashMap<usize, u32> {
457
- let mut parents = HashMap::with_capacity(self.nodes.len());
458
- let mut cursor = self.root.walk();
459
- let mut ancestors: Vec<u32> = Vec::new();
460
- let mut position = 0u32;
461
- loop {
462
- parents.insert(
463
- cursor.node().id(),
464
- ancestors.last().copied().unwrap_or(NO_PARENT),
465
- );
466
- let here = position;
467
- position += 1;
468
- if cursor.goto_first_child() {
469
- ancestors.push(here);
651
+ /// The named nodes of one kind, in source order -- the same list [`RuleContext::nodes_of`]
652
+ /// hands a cop, for the helpers that hold an index rather than a context.
653
+ pub(in crate::rules) fn nodes_of_kind<'a>(
654
+ &'a self,
655
+ kind: &str,
656
+ ) -> impl Iterator<Item = Node<'tree>> + 'a {
657
+ self.of_kind(kind).map(|index| self.named_node(index))
658
+ }
659
+
660
+ /// The field `node` fills in its parent, as `TreeCursor::field_name` reports it, or `None`
661
+ /// for a node that fills none.
662
+ ///
663
+ /// A node the index does not know answers `None` as well, so a caller that must tell the two
664
+ /// apart asks [`Self::knows`] first.
665
+ pub(in crate::rules) fn field_name_of(&self, node: Node<'_>) -> Option<&'static str> {
666
+ let position = *self.positions.get(&node.id())? as usize;
667
+ match self.field_of[position] {
668
+ 0 => None,
669
+ id => FIELD_NAMES.get(id as usize).copied().flatten(),
670
+ }
671
+ }
672
+
673
+ /// Whether the node belongs to the tree this index was built from.
674
+ pub(in crate::rules) fn knows(&self, node: Node<'_>) -> bool {
675
+ self.positions.contains_key(&node.id())
676
+ }
677
+
678
+ /// Every child of `node`, named or not, in the order the parser wrote them.
679
+ ///
680
+ /// `Node::children` opens a tree cursor for each node it is asked about, and a sampling
681
+ /// profile of a run over RuboCop's own tree put that cursor's iteration first among every
682
+ /// cost. The pre-order table already holds the children: the first sits right after the node,
683
+ /// and each next one a whole subtree further on.
684
+ pub(in crate::rules) fn children_of<'node>(
685
+ &'node self,
686
+ node: Node<'node>,
687
+ ) -> Option<Children<'node>> {
688
+ let position = *self.positions.get(&node.id())? as usize;
689
+ Some(Children {
690
+ nodes: &self.nodes,
691
+ subtree_len: &self.subtree_len,
692
+ next: position + 1,
693
+ end: position + self.subtree_len[position] as usize,
694
+ })
695
+ }
696
+
697
+ /// Every named node of `node`'s subtree, `node` itself first and the rest in depth-first
698
+ /// pre-order -- the order a stack-driven walk produces.
699
+ ///
700
+ /// `named_nodes` is filled in that order, so the answer is a run of it rather than a walk.
701
+ pub(in crate::rules) fn named_descendants<'node>(
702
+ &'node self,
703
+ node: Node<'node>,
704
+ ) -> Option<&'node [Node<'node>]> {
705
+ let position = *self.positions.get(&node.id())? as usize;
706
+ let start = match self.named_index[position] {
707
+ NOT_NAMED => return None,
708
+ index => index as usize,
709
+ };
710
+ Some(&self.named_nodes[start..start + self.named_subtree[position] as usize])
711
+ }
712
+
713
+ /// The named children of a node of this file's tree, as they were recorded when the index was
714
+ /// built. A node the index does not know is walked with a cursor, which is what
715
+ /// `Node::named_children` would have done for every node.
716
+ pub(in crate::rules) fn named_children_of<'node>(
717
+ &'node self,
718
+ node: Node<'node>,
719
+ ) -> Option<&'node [Node<'node>]> {
720
+ let position = *self.positions.get(&node.id())? as usize;
721
+ let start = self.child_start[position] as usize;
722
+ let end = self.child_start[position + 1] as usize;
723
+ Some(&self.named_children[start..end])
724
+ }
725
+
726
+ /// [`Self::parent`] for a node of the tree this index was built from, answered without tying
727
+ /// the result to the borrow of the index.
728
+ ///
729
+ /// A helper that hands the parent back to its caller, and the variable force, which holds
730
+ /// nodes for as long as the file is inspected, both need the tree's own lifetime rather than
731
+ /// the shorter one `parent` gives.
732
+ /// The tree this index was built from.
733
+ pub(in crate::rules) fn root_node(&self) -> Node<'tree> {
734
+ self.root
735
+ }
736
+
737
+ pub(in crate::rules) fn parent_in_tree(&self, node: Node<'tree>) -> Option<Node<'tree>> {
738
+ match self.positions.get(&node.id()) {
739
+ Some(&position) => match self.parent_of[position as usize] {
740
+ NO_PARENT => None,
741
+ index => Some(self.nodes[index as usize]),
742
+ },
743
+ None => node.parent(),
744
+ }
745
+ }
746
+
747
+ /// Groups the named children of every node into one run each, from the parents the walk
748
+ /// recorded. A counting pass and a placing pass, so no node's list is grown as it fills.
749
+ fn index_children(&mut self) {
750
+ let count = self.nodes.len();
751
+ // One slot past the end, so a node's run is `child_start[i]..child_start[i + 1]` without
752
+ // a bounds check on the last node.
753
+ self.child_start = vec![0u32; count + 1];
754
+ for position in 0..count {
755
+ if self.named_index[position] == NOT_NAMED {
470
756
  continue;
471
757
  }
472
- loop {
473
- if cursor.goto_next_sibling() {
474
- break;
475
- }
476
- if !cursor.goto_parent() {
477
- return parents;
478
- }
479
- ancestors.pop();
758
+ let parent = self.parent_of[position];
759
+ if parent == NO_PARENT {
760
+ continue;
761
+ }
762
+ self.child_start[parent as usize + 1] += 1;
763
+ }
764
+ for index in 1..=count {
765
+ self.child_start[index] += self.child_start[index - 1];
766
+ }
767
+ // `child_start` now holds each node's end; filling backwards from it leaves it holding
768
+ // the start again, which is the usual way to build a compressed adjacency list.
769
+ let total = self.child_start[count] as usize;
770
+ self.named_children = vec![self.root; total];
771
+ let mut cursor = self.child_start.clone();
772
+ for position in 0..count {
773
+ if self.named_index[position] == NOT_NAMED {
774
+ continue;
775
+ }
776
+ let parent = self.parent_of[position];
777
+ if parent == NO_PARENT {
778
+ continue;
779
+ }
780
+ let slot = &mut cursor[parent as usize];
781
+ self.named_children[*slot as usize] = self.nodes[position];
782
+ *slot += 1;
783
+ }
784
+ }
785
+
786
+ /// Counts the named nodes of every subtree. A node always sits before its children in
787
+ /// `nodes`, so one pass from the end folds each count into its parent's.
788
+ fn index_subtrees(&mut self) {
789
+ self.named_subtree = self
790
+ .named_index
791
+ .iter()
792
+ .map(|index| u32::from(*index != NOT_NAMED))
793
+ .collect();
794
+ self.subtree_len = vec![1u32; self.nodes.len()];
795
+ for position in (1..self.nodes.len()).rev() {
796
+ let parent = self.parent_of[position];
797
+ if parent != NO_PARENT {
798
+ self.named_subtree[parent as usize] += self.named_subtree[position];
799
+ self.subtree_len[parent as usize] += self.subtree_len[position];
480
800
  }
481
801
  }
482
802
  }
@@ -486,9 +806,17 @@ impl<'tree> AstIndex<'tree> {
486
806
  /// walk aborts the whole process on deeply nested input.
487
807
  fn collect(&mut self, root: Node<'tree>) {
488
808
  let mut cursor = root.walk();
809
+ let mut ancestors: Vec<u32> = Vec::new();
489
810
  loop {
490
- self.visit(cursor.node());
811
+ let here = self.nodes.len() as u32;
812
+ let field = cursor.field_id().map_or(0, std::num::NonZeroU16::get);
813
+ self.visit(
814
+ cursor.node(),
815
+ ancestors.last().copied().unwrap_or(NO_PARENT),
816
+ field,
817
+ );
491
818
  if cursor.goto_first_child() {
819
+ ancestors.push(here);
492
820
  continue;
493
821
  }
494
822
  loop {
@@ -498,26 +826,38 @@ impl<'tree> AstIndex<'tree> {
498
826
  if !cursor.goto_parent() {
499
827
  return;
500
828
  }
829
+ ancestors.pop();
501
830
  }
502
831
  }
503
832
  }
504
833
 
505
- fn visit(&mut self, node: Node<'tree>) {
834
+ fn visit(&mut self, node: Node<'tree>, parent: u32, field: u16) {
835
+ // Read once. Each call goes through the C API for the node's symbol, and this used to ask
836
+ // four times for every node of every file.
837
+ let kind = node.kind_str();
838
+ let named = node.is_named();
839
+ self.positions.insert(node.id(), self.nodes.len() as u32);
840
+ self.parent_of.push(parent);
841
+ self.field_of.push(u32::from(field));
842
+ self.named_index.push(match named {
843
+ true => self.named_nodes.len() as u32,
844
+ false => NOT_NAMED,
845
+ });
506
846
  self.nodes.push(node);
507
- if node.is_named() {
847
+ if named {
508
848
  // A file with more than u32::MAX named nodes would need tens of gigabytes of source;
509
849
  // the cast cannot lose information for anything a parser will accept.
510
850
  let index = self.named_nodes.len() as u32;
511
851
  self.named_nodes.push(node);
512
- self.by_kind.entry(node.kind_str()).or_default().push(index);
852
+ self.by_kind.entry(kind).or_default().push(index);
513
853
  }
514
- if PROTECTED_LITERAL_KINDS.contains(&node.kind_str()) {
854
+ if PROTECTED_LITERAL_KINDS.contains(&kind) {
515
855
  self.protected_ranges.push(node.byte_range());
516
856
  }
517
- if node.kind_str() == "heredoc_body" {
857
+ if kind == "heredoc_body" {
518
858
  self.heredoc_ranges.push(node.byte_range());
519
859
  }
520
- if node.kind_str() == "comment" && !inside_literal_text(node) {
860
+ if kind == "comment" && !inside_literal_text(node) {
521
861
  self.comment_ranges.push(node.byte_range());
522
862
  }
523
863
  }
@@ -577,8 +917,9 @@ fn merge_touching_ranges(ranges: &mut Vec<Range<usize>>) {
577
917
  mod tests {
578
918
  use std::collections::HashSet;
579
919
 
580
- use super::{merge_touching_ranges, rule_names, rules};
920
+ use super::{AstIndex, merge_touching_ranges, rule_names, rules};
581
921
  use crate::config::Config;
922
+ use crate::rules::node_ext::NodeExt;
582
923
  use crate::source::is_protected;
583
924
 
584
925
  #[test]
@@ -636,6 +977,110 @@ mod tests {
636
977
  }
637
978
  }
638
979
 
980
+ /// The index answers a parent and a child list from its own arrays rather than from the
981
+ /// parser, so the two have to agree for every node of a real file -- a cop reaching for either
982
+ /// would otherwise see a tree the grammar never built.
983
+ #[test]
984
+ fn the_index_answers_what_the_parser_answers() {
985
+ let source = "# frozen_string_literal: true\n class Foo < Bar\n def baz(a = 1, *rest, key:, &block)\n @x ||= a.map { |v| \"#{v}-#{rest.first}\" }\n text = <<~DOC\n hello #{key}\n DOC\n [1, 2].each_with_object({}) { |n, memo| memo[n] = n }\n rescue StandardError => error\n raise error\n ensure\n puts text\n end\n end\n";
986
+ let mut parser = tree_sitter::Parser::new();
987
+ parser
988
+ .set_language(&tree_sitter_ruby::LANGUAGE.into())
989
+ .expect("the Ruby grammar loads");
990
+ let tree = parser.parse(source, None).expect("the source parses");
991
+ let index = AstIndex::new(tree.root_node());
992
+
993
+ let mut seen = 0;
994
+ for node in &index.nodes {
995
+ let mut cursor = node.walk();
996
+ let expected: Vec<tree_sitter::Node<'_>> = node.named_children(&mut cursor).collect();
997
+ let recorded = index
998
+ .named_children_of(*node)
999
+ .expect("every node of this tree is indexed");
1000
+ let mut all = node.walk();
1001
+ let all_expected: Vec<tree_sitter::Node<'_>> = node.children(&mut all).collect();
1002
+ let all_recorded: Vec<tree_sitter::Node<'_>> = index
1003
+ .children_of(*node)
1004
+ .expect("every node of this tree is indexed")
1005
+ .collect();
1006
+ assert_eq!(
1007
+ all_recorded,
1008
+ all_expected,
1009
+ "{} reported different children",
1010
+ node.kind_str()
1011
+ );
1012
+ assert_eq!(
1013
+ recorded,
1014
+ expected,
1015
+ "{} reported different named children",
1016
+ node.kind_str()
1017
+ );
1018
+ assert_eq!(
1019
+ index.parent_in_tree(*node).map(|found| found.id()),
1020
+ node.parent().map(|found| found.id()),
1021
+ "{} reported a different parent",
1022
+ node.kind_str()
1023
+ );
1024
+ // The field a node fills, as a cursor sitting on it would report.
1025
+ let by_cursor = index.parent_in_tree(*node).and_then(|parent| {
1026
+ let mut walk = parent.walk();
1027
+ walk.goto_first_child().then_some(()).and_then(|()| {
1028
+ loop {
1029
+ if walk.node().id() == node.id() {
1030
+ return walk.field_name();
1031
+ }
1032
+ if !walk.goto_next_sibling() {
1033
+ return None;
1034
+ }
1035
+ }
1036
+ })
1037
+ });
1038
+ assert_eq!(
1039
+ index.field_name_of(*node),
1040
+ by_cursor,
1041
+ "{} reported a different field",
1042
+ node.kind_str()
1043
+ );
1044
+ seen += 1;
1045
+ }
1046
+ assert!(
1047
+ seen > 100,
1048
+ "the sample file should exercise more than {seen} nodes"
1049
+ );
1050
+ }
1051
+
1052
+ /// A node from another tree -- the extra parses `Metrics` makes to recover what the grammar
1053
+ /// swallowed -- is not in the index, and both accessors have to fall back to the parser rather
1054
+ /// than answer for a node they never saw.
1055
+ #[test]
1056
+ fn a_node_from_another_tree_falls_back_to_the_parser() {
1057
+ let mut parser = tree_sitter::Parser::new();
1058
+ parser
1059
+ .set_language(&tree_sitter_ruby::LANGUAGE.into())
1060
+ .expect("the Ruby grammar loads");
1061
+ let indexed = parser.parse("foo(1)\n", None).expect("the source parses");
1062
+ let index = AstIndex::new(indexed.root_node());
1063
+
1064
+ let other = parser
1065
+ .parse("bar(2, 3)\n", None)
1066
+ .expect("the source parses");
1067
+ let call = other
1068
+ .root_node()
1069
+ .named_child(0)
1070
+ .expect("the program has a statement");
1071
+ assert!(index.named_children_of(call).is_none());
1072
+ let mut cursor = call.walk();
1073
+ let expected: Vec<tree_sitter::Node<'_>> = call.named_children(&mut cursor).collect();
1074
+ assert_eq!(
1075
+ crate::rules::send_node::named_children_in(call, &index).into_owned(),
1076
+ expected
1077
+ );
1078
+ assert_eq!(
1079
+ index.parent_in_tree(call).map(|found| found.id()),
1080
+ call.parent().map(|found| found.id())
1081
+ );
1082
+ }
1083
+
639
1084
  /// The registry is a static built from the department tables, so iteration order cannot vary
640
1085
  /// between runs; autocorrect ordering and `--debug` output both rely on that.
641
1086
  #[test]