shirobai 2026.0709.0000

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 (221) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +420 -0
  3. data/Cargo.toml +13 -0
  4. data/LICENSE.txt +21 -0
  5. data/crates/shirobai-core/Cargo.toml +20 -0
  6. data/crates/shirobai-core/src/bin/analysis_profile.rs +447 -0
  7. data/crates/shirobai-core/src/bin/bundle_profile.rs +656 -0
  8. data/crates/shirobai-core/src/bin/hotspot_profile.rs +596 -0
  9. data/crates/shirobai-core/src/bin/walk_profile.rs +191 -0
  10. data/crates/shirobai-core/src/lib.rs +1 -0
  11. data/crates/shirobai-core/src/rules/abc_size.rs +1475 -0
  12. data/crates/shirobai-core/src/rules/access_modifier_indentation.rs +450 -0
  13. data/crates/shirobai-core/src/rules/ambiguous_block_association.rs +528 -0
  14. data/crates/shirobai-core/src/rules/argument_alignment.rs +300 -0
  15. data/crates/shirobai-core/src/rules/arguments_forwarding.rs +1125 -0
  16. data/crates/shirobai-core/src/rules/array_alignment.rs +398 -0
  17. data/crates/shirobai-core/src/rules/assignment_indentation.rs +502 -0
  18. data/crates/shirobai-core/src/rules/block_alignment.rs +869 -0
  19. data/crates/shirobai-core/src/rules/block_delimiters.rs +1876 -0
  20. data/crates/shirobai-core/src/rules/block_length.rs +631 -0
  21. data/crates/shirobai-core/src/rules/block_nesting.rs +390 -0
  22. data/crates/shirobai-core/src/rules/bundle.rs +4433 -0
  23. data/crates/shirobai-core/src/rules/class_length.rs +531 -0
  24. data/crates/shirobai-core/src/rules/closing_parenthesis_indentation.rs +425 -0
  25. data/crates/shirobai-core/src/rules/code_length.rs +801 -0
  26. data/crates/shirobai-core/src/rules/colon_method_call.rs +285 -0
  27. data/crates/shirobai-core/src/rules/complexity.rs +585 -0
  28. data/crates/shirobai-core/src/rules/debugger.rs +438 -0
  29. data/crates/shirobai-core/src/rules/def_end_alignment.rs +485 -0
  30. data/crates/shirobai-core/src/rules/dispatch.rs +276 -0
  31. data/crates/shirobai-core/src/rules/dot_position.rs +276 -0
  32. data/crates/shirobai-core/src/rules/duplicate_magic_comment.rs +1001 -0
  33. data/crates/shirobai-core/src/rules/duplicate_methods.rs +1725 -0
  34. data/crates/shirobai-core/src/rules/else_alignment.rs +837 -0
  35. data/crates/shirobai-core/src/rules/empty_comment.rs +452 -0
  36. data/crates/shirobai-core/src/rules/empty_line_after_guard_clause.rs +1210 -0
  37. data/crates/shirobai-core/src/rules/empty_line_after_magic_comment.rs +166 -0
  38. data/crates/shirobai-core/src/rules/empty_line_between_defs.rs +731 -0
  39. data/crates/shirobai-core/src/rules/empty_lines.rs +432 -0
  40. data/crates/shirobai-core/src/rules/empty_lines_around_arguments.rs +298 -0
  41. data/crates/shirobai-core/src/rules/empty_lines_around_body.rs +1320 -0
  42. data/crates/shirobai-core/src/rules/end_alignment.rs +798 -0
  43. data/crates/shirobai-core/src/rules/file_null.rs +320 -0
  44. data/crates/shirobai-core/src/rules/first_argument_indentation.rs +643 -0
  45. data/crates/shirobai-core/src/rules/first_array_element_indentation.rs +644 -0
  46. data/crates/shirobai-core/src/rules/first_hash_element_indentation.rs +699 -0
  47. data/crates/shirobai-core/src/rules/frozen_string_literal_comment.rs +414 -0
  48. data/crates/shirobai-core/src/rules/hash_alignment.rs +1030 -0
  49. data/crates/shirobai-core/src/rules/hash_each_methods.rs +744 -0
  50. data/crates/shirobai-core/src/rules/hash_syntax.rs +1304 -0
  51. data/crates/shirobai-core/src/rules/hash_transform_keys.rs +909 -0
  52. data/crates/shirobai-core/src/rules/if_unless_modifier.rs +1494 -0
  53. data/crates/shirobai-core/src/rules/indentation_consistency.rs +585 -0
  54. data/crates/shirobai-core/src/rules/indentation_width.rs +1231 -0
  55. data/crates/shirobai-core/src/rules/leading_empty_lines.rs +365 -0
  56. data/crates/shirobai-core/src/rules/line_end_concatenation.rs +330 -0
  57. data/crates/shirobai-core/src/rules/line_index.rs +227 -0
  58. data/crates/shirobai-core/src/rules/line_length.rs +434 -0
  59. data/crates/shirobai-core/src/rules/line_length_breakable.rs +1454 -0
  60. data/crates/shirobai-core/src/rules/method_length.rs +438 -0
  61. data/crates/shirobai-core/src/rules/method_name.rs +565 -0
  62. data/crates/shirobai-core/src/rules/mod.rs +104 -0
  63. data/crates/shirobai-core/src/rules/module_length.rs +283 -0
  64. data/crates/shirobai-core/src/rules/multiline_method_call_brace_layout.rs +594 -0
  65. data/crates/shirobai-core/src/rules/multiline_method_call_indentation.rs +1444 -0
  66. data/crates/shirobai-core/src/rules/multiline_operation_indentation.rs +720 -0
  67. data/crates/shirobai-core/src/rules/nested_parenthesized_calls.rs +486 -0
  68. data/crates/shirobai-core/src/rules/opaque_mask.rs +164 -0
  69. data/crates/shirobai-core/src/rules/parentheses_as_grouped_expression.rs +410 -0
  70. data/crates/shirobai-core/src/rules/parse_cache.rs +116 -0
  71. data/crates/shirobai-core/src/rules/percent_literal_delimiters.rs +835 -0
  72. data/crates/shirobai-core/src/rules/perf_detect.rs +524 -0
  73. data/crates/shirobai-core/src/rules/perf_end_with.rs +368 -0
  74. data/crates/shirobai-core/src/rules/perf_start_with.rs +333 -0
  75. data/crates/shirobai-core/src/rules/perf_string_include.rs +399 -0
  76. data/crates/shirobai-core/src/rules/perf_times_map.rs +570 -0
  77. data/crates/shirobai-core/src/rules/predicate_prefix.rs +397 -0
  78. data/crates/shirobai-core/src/rules/punctuation_spacing.rs +725 -0
  79. data/crates/shirobai-core/src/rules/rails_app.rs +765 -0
  80. data/crates/shirobai-core/src/rules/rails_config.rs +80 -0
  81. data/crates/shirobai-core/src/rules/rails_dynamic_find_by.rs +447 -0
  82. data/crates/shirobai-core/src/rules/rails_pluck.rs +470 -0
  83. data/crates/shirobai-core/src/rules/rails_unknown_env.rs +362 -0
  84. data/crates/shirobai-core/src/rules/redundant_freeze.rs +550 -0
  85. data/crates/shirobai-core/src/rules/redundant_self.rs +691 -0
  86. data/crates/shirobai-core/src/rules/redundant_self_assignment.rs +530 -0
  87. data/crates/shirobai-core/src/rules/require_parentheses.rs +237 -0
  88. data/crates/shirobai-core/src/rules/rspec_dispatcher.rs +2010 -0
  89. data/crates/shirobai-core/src/rules/rspec_empty_line.rs +791 -0
  90. data/crates/shirobai-core/src/rules/rspec_language.rs +233 -0
  91. data/crates/shirobai-core/src/rules/safe_navigation_chain.rs +376 -0
  92. data/crates/shirobai-core/src/rules/self_assignment.rs +976 -0
  93. data/crates/shirobai-core/src/rules/semicolon.rs +492 -0
  94. data/crates/shirobai-core/src/rules/space_around_keyword.rs +881 -0
  95. data/crates/shirobai-core/src/rules/space_around_method_call_operator.rs +398 -0
  96. data/crates/shirobai-core/src/rules/space_before_block_braces.rs +380 -0
  97. data/crates/shirobai-core/src/rules/space_before_first_arg.rs +627 -0
  98. data/crates/shirobai-core/src/rules/space_inside_array_literal_brackets.rs +977 -0
  99. data/crates/shirobai-core/src/rules/space_inside_block_braces.rs +669 -0
  100. data/crates/shirobai-core/src/rules/space_inside_hash_literal_braces.rs +498 -0
  101. data/crates/shirobai-core/src/rules/space_inside_parens.rs +538 -0
  102. data/crates/shirobai-core/src/rules/space_inside_reference_brackets.rs +513 -0
  103. data/crates/shirobai-core/src/rules/space_scan.rs +136 -0
  104. data/crates/shirobai-core/src/rules/stabby_lambda_parentheses.rs +326 -0
  105. data/crates/shirobai-core/src/rules/string_literals.rs +611 -0
  106. data/crates/shirobai-core/src/rules/string_literals_in_interpolation.rs +244 -0
  107. data/crates/shirobai-core/src/rules/trailing_comma.rs +428 -0
  108. data/crates/shirobai-core/src/rules/trailing_comma_in_arguments.rs +502 -0
  109. data/crates/shirobai-core/src/rules/trailing_comma_in_array_literal.rs +210 -0
  110. data/crates/shirobai-core/src/rules/trailing_comma_in_hash_literal.rs +174 -0
  111. data/crates/shirobai-core/src/rules/trailing_empty_lines.rs +307 -0
  112. data/crates/shirobai-core/src/rules/unreachable_code.rs +719 -0
  113. data/crates/shirobai-core/src/rules/useless_access_modifier.rs +1087 -0
  114. data/crates/shirobai-core/src/rules/variable_number.rs +318 -0
  115. data/crates/shirobai-core/src/rules/void.rs +1328 -0
  116. data/ext/shirobai/Cargo.toml +16 -0
  117. data/ext/shirobai/src/lib.rs +4501 -0
  118. data/lib/shirobai/cop/base.rb +45 -0
  119. data/lib/shirobai/cop/layout/access_modifier_indentation.rb +117 -0
  120. data/lib/shirobai/cop/layout/argument_alignment.rb +86 -0
  121. data/lib/shirobai/cop/layout/array_alignment.rb +109 -0
  122. data/lib/shirobai/cop/layout/assignment_indentation.rb +73 -0
  123. data/lib/shirobai/cop/layout/block_alignment.rb +79 -0
  124. data/lib/shirobai/cop/layout/closing_parenthesis_indentation.rb +51 -0
  125. data/lib/shirobai/cop/layout/def_end_alignment.rb +114 -0
  126. data/lib/shirobai/cop/layout/dot_position.rb +63 -0
  127. data/lib/shirobai/cop/layout/else_alignment.rb +86 -0
  128. data/lib/shirobai/cop/layout/empty_comment.rb +75 -0
  129. data/lib/shirobai/cop/layout/empty_line_after_guard_clause.rb +116 -0
  130. data/lib/shirobai/cop/layout/empty_line_after_magic_comment.rb +82 -0
  131. data/lib/shirobai/cop/layout/empty_line_between_defs.rb +78 -0
  132. data/lib/shirobai/cop/layout/empty_lines.rb +70 -0
  133. data/lib/shirobai/cop/layout/empty_lines_around_arguments.rb +55 -0
  134. data/lib/shirobai/cop/layout/empty_lines_around_begin_body.rb +22 -0
  135. data/lib/shirobai/cop/layout/empty_lines_around_block_body.rb +28 -0
  136. data/lib/shirobai/cop/layout/empty_lines_around_body_shared.rb +58 -0
  137. data/lib/shirobai/cop/layout/empty_lines_around_class_body.rb +27 -0
  138. data/lib/shirobai/cop/layout/empty_lines_around_exception_handling_keywords.rb +24 -0
  139. data/lib/shirobai/cop/layout/empty_lines_around_method_body.rb +22 -0
  140. data/lib/shirobai/cop/layout/empty_lines_around_module_body.rb +26 -0
  141. data/lib/shirobai/cop/layout/end_alignment.rb +101 -0
  142. data/lib/shirobai/cop/layout/first_argument_indentation.rb +76 -0
  143. data/lib/shirobai/cop/layout/first_array_element_indentation.rb +89 -0
  144. data/lib/shirobai/cop/layout/first_hash_element_indentation.rb +104 -0
  145. data/lib/shirobai/cop/layout/hash_alignment.rb +135 -0
  146. data/lib/shirobai/cop/layout/indentation_consistency.rb +95 -0
  147. data/lib/shirobai/cop/layout/indentation_width.rb +141 -0
  148. data/lib/shirobai/cop/layout/leading_empty_lines.rb +72 -0
  149. data/lib/shirobai/cop/layout/line_length.rb +227 -0
  150. data/lib/shirobai/cop/layout/multiline_method_call_brace_layout.rb +126 -0
  151. data/lib/shirobai/cop/layout/multiline_method_call_indentation.rb +87 -0
  152. data/lib/shirobai/cop/layout/multiline_operation_indentation.rb +61 -0
  153. data/lib/shirobai/cop/layout/space_after_colon.rb +58 -0
  154. data/lib/shirobai/cop/layout/space_after_comma.rb +64 -0
  155. data/lib/shirobai/cop/layout/space_after_semicolon.rb +57 -0
  156. data/lib/shirobai/cop/layout/space_around_keyword.rb +69 -0
  157. data/lib/shirobai/cop/layout/space_around_method_call_operator.rb +55 -0
  158. data/lib/shirobai/cop/layout/space_before_block_braces.rb +150 -0
  159. data/lib/shirobai/cop/layout/space_before_comma.rb +63 -0
  160. data/lib/shirobai/cop/layout/space_before_comment.rb +56 -0
  161. data/lib/shirobai/cop/layout/space_before_first_arg.rb +65 -0
  162. data/lib/shirobai/cop/layout/space_before_semicolon.rb +57 -0
  163. data/lib/shirobai/cop/layout/space_inside_array_literal_brackets.rb +109 -0
  164. data/lib/shirobai/cop/layout/space_inside_block_braces.rb +116 -0
  165. data/lib/shirobai/cop/layout/space_inside_hash_literal_braces.rb +93 -0
  166. data/lib/shirobai/cop/layout/space_inside_parens.rb +67 -0
  167. data/lib/shirobai/cop/layout/space_inside_reference_brackets.rb +104 -0
  168. data/lib/shirobai/cop/layout/trailing_empty_lines.rb +104 -0
  169. data/lib/shirobai/cop/lint/ambiguous_block_association.rb +117 -0
  170. data/lib/shirobai/cop/lint/debugger.rb +44 -0
  171. data/lib/shirobai/cop/lint/duplicate_magic_comment.rb +67 -0
  172. data/lib/shirobai/cop/lint/duplicate_methods.rb +109 -0
  173. data/lib/shirobai/cop/lint/parentheses_as_grouped_expression.rb +54 -0
  174. data/lib/shirobai/cop/lint/require_parentheses.rb +35 -0
  175. data/lib/shirobai/cop/lint/safe_navigation_chain.rb +52 -0
  176. data/lib/shirobai/cop/lint/self_assignment.rb +105 -0
  177. data/lib/shirobai/cop/lint/unreachable_code.rb +48 -0
  178. data/lib/shirobai/cop/lint/useless_access_modifier.rb +61 -0
  179. data/lib/shirobai/cop/lint/void.rb +60 -0
  180. data/lib/shirobai/cop/metrics/abc_size.rb +61 -0
  181. data/lib/shirobai/cop/metrics/block_length.rb +139 -0
  182. data/lib/shirobai/cop/metrics/block_nesting.rb +62 -0
  183. data/lib/shirobai/cop/metrics/class_length.rb +88 -0
  184. data/lib/shirobai/cop/metrics/complexity_base.rb +35 -0
  185. data/lib/shirobai/cop/metrics/complexity_shared.rb +45 -0
  186. data/lib/shirobai/cop/metrics/cyclomatic_complexity.rb +30 -0
  187. data/lib/shirobai/cop/metrics/method_length.rb +70 -0
  188. data/lib/shirobai/cop/metrics/module_length.rb +83 -0
  189. data/lib/shirobai/cop/metrics/perceived_complexity.rb +30 -0
  190. data/lib/shirobai/cop/naming/method_name.rb +100 -0
  191. data/lib/shirobai/cop/naming/predicate_prefix.rb +107 -0
  192. data/lib/shirobai/cop/naming/variable_number.rb +72 -0
  193. data/lib/shirobai/cop/style/arguments_forwarding.rb +108 -0
  194. data/lib/shirobai/cop/style/block_delimiters.rb +164 -0
  195. data/lib/shirobai/cop/style/colon_method_call.rb +60 -0
  196. data/lib/shirobai/cop/style/file_null.rb +55 -0
  197. data/lib/shirobai/cop/style/frozen_string_literal_comment.rb +118 -0
  198. data/lib/shirobai/cop/style/hash_each_methods.rb +50 -0
  199. data/lib/shirobai/cop/style/hash_syntax.rb +147 -0
  200. data/lib/shirobai/cop/style/hash_transform_keys.rb +46 -0
  201. data/lib/shirobai/cop/style/if_unless_modifier.rb +207 -0
  202. data/lib/shirobai/cop/style/line_end_concatenation.rb +42 -0
  203. data/lib/shirobai/cop/style/nested_parenthesized_calls.rb +88 -0
  204. data/lib/shirobai/cop/style/percent_literal_delimiters.rb +113 -0
  205. data/lib/shirobai/cop/style/redundant_freeze.rb +75 -0
  206. data/lib/shirobai/cop/style/redundant_self.rb +50 -0
  207. data/lib/shirobai/cop/style/redundant_self_assignment.rb +62 -0
  208. data/lib/shirobai/cop/style/semicolon.rb +190 -0
  209. data/lib/shirobai/cop/style/stabby_lambda_parentheses.rb +75 -0
  210. data/lib/shirobai/cop/style/string_literals.rb +135 -0
  211. data/lib/shirobai/cop/style/string_literals_in_interpolation.rb +130 -0
  212. data/lib/shirobai/cop/style/trailing_comma_in_arguments.rb +127 -0
  213. data/lib/shirobai/cop/style/trailing_comma_in_array_literal.rb +127 -0
  214. data/lib/shirobai/cop/style/trailing_comma_in_hash_literal.rb +127 -0
  215. data/lib/shirobai/dispatch.rb +481 -0
  216. data/lib/shirobai/inject.rb +123 -0
  217. data/lib/shirobai/node_locator.rb +124 -0
  218. data/lib/shirobai/source_offsets.rb +43 -0
  219. data/lib/shirobai/version.rb +5 -0
  220. data/lib/shirobai.rb +5 -0
  221. metadata +271 -0
@@ -0,0 +1,596 @@
1
+ //! Hotspot decomposition profiler (MEASUREMENT ONLY — does not change real cops).
2
+ //!
3
+ //! Breaks the heavy cops (first_argument_indentation, indentation_width,
4
+ //! multiline_bundle, dot_position) into internal phases and measures each in
5
+ //! CPU time over the corpus, so we can attribute the pure-analysis cost to
6
+ //! specific helpers/structures rather than treating it as opaque.
7
+ //!
8
+ //! Phases measured per relevant cop:
9
+ //! * `parse` — parse-only baseline (P)
10
+ //! * `walk_noop` — dispatch::run with an empty rule (shared traversal cost:
11
+ //! parse-cache hit + tree walk + per-node virtual dispatch).
12
+ //! This is the *shared* walk cost 16 cops duplicate.
13
+ //! * `frame_<cop>` — walk + replicate that cop's per-node frame extraction
14
+ //! (prism accessors + the same Vec allocations) but NO
15
+ //! offense analysis. (full − frame) ≈ analysis body;
16
+ //! (frame − walk_noop) ≈ frame-construction overhead.
17
+ //! * `full_<cop>` — the real check_* (delegates to rules; unchanged logic).
18
+ //!
19
+ //! Microbenchmarks (`micro_*`) isolate the cost of the O(n)-prefix line/column
20
+ //! helpers as currently written vs. an O(1) precomputed line-index, by calling
21
+ //! them on the *real* offsets every CallNode in the corpus produces.
22
+ //!
23
+ //! Usage: hotspot_profile <mode> <corpus_dir> [iterations]
24
+
25
+ use std::path::PathBuf;
26
+
27
+ use ruby_prism::{Node, Visit, parse};
28
+ use shirobai_core::rules;
29
+ use shirobai_core::rules::dispatch::{self, Rule};
30
+
31
+ // ----------------- timing -----------------
32
+
33
+ #[cfg(unix)]
34
+ fn cpu_time_secs() -> f64 {
35
+ let mut ts = libc::timespec {
36
+ tv_sec: 0,
37
+ tv_nsec: 0,
38
+ };
39
+ // SAFETY: ts is a valid timespec.
40
+ unsafe { libc::clock_gettime(libc::CLOCK_PROCESS_CPUTIME_ID, &mut ts) };
41
+ ts.tv_sec as f64 + ts.tv_nsec as f64 / 1e9
42
+ }
43
+ #[cfg(not(unix))]
44
+ fn cpu_time_secs() -> f64 {
45
+ 0.0
46
+ }
47
+
48
+ fn collect_rb_files(dir: &str) -> Vec<Vec<u8>> {
49
+ let mut out = Vec::new();
50
+ let mut stack = vec![PathBuf::from(dir)];
51
+ while let Some(p) = stack.pop() {
52
+ let Ok(rd) = std::fs::read_dir(&p) else {
53
+ continue;
54
+ };
55
+ for entry in rd.flatten() {
56
+ let path = entry.path();
57
+ if path.is_dir() {
58
+ stack.push(path);
59
+ } else if path.extension().is_some_and(|e| e == "rb")
60
+ && let Ok(bytes) = std::fs::read(&path)
61
+ {
62
+ out.push(bytes);
63
+ }
64
+ }
65
+ }
66
+ out
67
+ }
68
+
69
+ // ----------------- a line index (the proposed O(1) replacement) -----------------
70
+
71
+ /// Precomputed sorted byte offsets of each line start. Built once per source
72
+ /// in O(n); answers line_of / line_start in O(log n) (or O(1) amortised).
73
+ struct LineIndex {
74
+ starts: Vec<usize>,
75
+ }
76
+ impl LineIndex {
77
+ fn build(src: &[u8]) -> Self {
78
+ let mut starts = Vec::with_capacity(src.len() / 24 + 1);
79
+ starts.push(0);
80
+ for (i, &b) in src.iter().enumerate() {
81
+ if b == b'\n' {
82
+ starts.push(i + 1);
83
+ }
84
+ }
85
+ LineIndex { starts }
86
+ }
87
+ /// 1-based line number of `off`.
88
+ fn line_of(&self, off: usize) -> usize {
89
+ // partition_point: index of first start > off == line number (1-based).
90
+ self.starts.partition_point(|&s| s <= off)
91
+ }
92
+ }
93
+
94
+ // ----------------- current O(n)-prefix helpers (copied verbatim) -----------------
95
+
96
+ fn cur_line_of(src: &[u8], off: usize) -> usize {
97
+ src[..off].iter().filter(|&&b| b == b'\n').count() + 1
98
+ }
99
+ // ----------------- no-op rule (shared walk cost) -----------------
100
+
101
+ struct NoopRule;
102
+ impl Rule for NoopRule {
103
+ fn enter(&mut self, _node: &Node<'_>) {}
104
+ fn leave(&mut self) {}
105
+ }
106
+
107
+ // ----------------- frame-only rules (walk + frame extraction, no analysis) -----
108
+
109
+ fn loc(l: &ruby_prism::Location<'_>) -> (usize, usize) {
110
+ (l.start_offset(), l.end_offset())
111
+ }
112
+
113
+ /// Replicates first_argument_indentation::make_frame work (incl. name.to_vec()).
114
+ struct FrameFirstArg {
115
+ sink: usize,
116
+ stack: Vec<(usize, usize, Vec<u8>)>,
117
+ }
118
+ impl Rule for FrameFirstArg {
119
+ fn enter(&mut self, node: &Node<'_>) {
120
+ let l = node.location();
121
+ let name = if let Some(c) = node.as_call_node() {
122
+ let _paren = c
123
+ .opening_loc()
124
+ .map(|o| o.as_slice() == b"(")
125
+ .unwrap_or(false);
126
+ let _recv = c.receiver().map(|r| loc(&r.location()));
127
+ let _end = c.closing_loc().map(|cl| cl.start_offset());
128
+ c.name().as_slice().to_vec() // <-- per-call heap alloc in real code
129
+ } else if let Some(s) = node.as_super_node() {
130
+ let _ = s.lparen_loc().is_some();
131
+ let _ = s.rparen_loc().map(|cl| cl.start_offset());
132
+ Vec::new()
133
+ } else if node.as_splat_node().is_some() || node.as_assoc_splat_node().is_some() {
134
+ Vec::new()
135
+ } else {
136
+ let _ = node.as_arguments_node().is_some()
137
+ || node.as_statements_node().is_some()
138
+ || node.as_keyword_hash_node().is_some();
139
+ Vec::new()
140
+ };
141
+ self.sink = self.sink.wrapping_add(name.len());
142
+ self.stack.push((l.start_offset(), l.end_offset(), name));
143
+ }
144
+ fn leave(&mut self) {
145
+ if let Some((s, _, _)) = self.stack.pop() {
146
+ self.sink = self.sink.wrapping_add(s);
147
+ }
148
+ }
149
+ }
150
+
151
+ /// Replicates indentation_width::enter frame work, incl. the per-call
152
+ /// `line_of(receiver_end)` O(n) scan done DURING frame build.
153
+ struct FrameIndent<'s> {
154
+ src: &'s [u8],
155
+ sink: usize,
156
+ depth: usize,
157
+ }
158
+ impl<'s> Rule for FrameIndent<'s> {
159
+ fn enter(&mut self, node: &Node<'_>) {
160
+ if node.as_arguments_node().is_some() {
161
+ self.sink = self.sink.wrapping_add(1);
162
+ } else if let Some(c) = node.as_call_node() {
163
+ if c.receiver().is_none() {
164
+ self.sink = self.sink.wrapping_add(node.location().start_offset());
165
+ } else {
166
+ let _dot = c.call_operator_loc().map(|l| l.start_offset());
167
+ let _sel = c.message_loc().map(|l| l.start_offset());
168
+ // The real code does exactly this O(n) scan per call frame:
169
+ let rll = c
170
+ .receiver()
171
+ .map(|r| cur_line_of(self.src, r.location().end_offset().saturating_sub(1)));
172
+ self.sink = self.sink.wrapping_add(rll.unwrap_or(0));
173
+ }
174
+ }
175
+ self.depth += 1;
176
+ }
177
+ fn leave(&mut self) {
178
+ self.depth = self.depth.saturating_sub(1);
179
+ }
180
+ }
181
+
182
+ /// Same as FrameIndent but uses a precomputed LineIndex for receiver_last_line.
183
+ struct FrameIndentIdx<'s> {
184
+ idx: &'s LineIndex,
185
+ sink: usize,
186
+ depth: usize,
187
+ }
188
+ impl<'s> Rule for FrameIndentIdx<'s> {
189
+ fn enter(&mut self, node: &Node<'_>) {
190
+ if node.as_arguments_node().is_some() {
191
+ self.sink = self.sink.wrapping_add(1);
192
+ } else if let Some(c) = node.as_call_node() {
193
+ if c.receiver().is_none() {
194
+ self.sink = self.sink.wrapping_add(node.location().start_offset());
195
+ } else {
196
+ let _dot = c.call_operator_loc().map(|l| l.start_offset());
197
+ let _sel = c.message_loc().map(|l| l.start_offset());
198
+ let rll = c.receiver().map(|r| {
199
+ self.idx
200
+ .line_of(r.location().end_offset().saturating_sub(1))
201
+ });
202
+ self.sink = self.sink.wrapping_add(rll.unwrap_or(0));
203
+ }
204
+ }
205
+ self.depth += 1;
206
+ }
207
+ fn leave(&mut self) {
208
+ self.depth = self.depth.saturating_sub(1);
209
+ }
210
+ }
211
+
212
+ /// Replicates multiline_method_call_indentation::frame_for, incl. per-call
213
+ /// args Vec allocation.
214
+ struct FrameMc {
215
+ sink: usize,
216
+ stack: Vec<usize>,
217
+ }
218
+ impl Rule for FrameMc {
219
+ fn enter(&mut self, node: &Node<'_>) {
220
+ if let Some(c) = node.as_call_node() {
221
+ let _paren = match c.closing_loc() {
222
+ Some(close) if close.as_slice() == b")" => c
223
+ .opening_loc()
224
+ .map(|o| (o.start_offset(), close.end_offset())),
225
+ _ => None,
226
+ };
227
+ let args: Vec<(usize, usize)> = c
228
+ .arguments()
229
+ .map(|a| {
230
+ a.arguments()
231
+ .iter()
232
+ .map(|arg| loc(&arg.location()))
233
+ .collect()
234
+ })
235
+ .unwrap_or_default();
236
+ let _dot = c.call_operator_loc().map(|d| d.start_offset());
237
+ let _sel = c.message_loc().as_ref().map(loc);
238
+ self.sink = self.sink.wrapping_add(args.len());
239
+ }
240
+ self.stack.push(node.location().start_offset());
241
+ }
242
+ fn leave(&mut self) {
243
+ if let Some(s) = self.stack.pop() {
244
+ self.sink = self.sink.wrapping_add(s);
245
+ }
246
+ }
247
+ }
248
+
249
+ // dot_position has its own (non-dispatch) walk; replicate the bare traversal +
250
+ // the per-dotted-call work UP TO the early-bail, both with current O(n) helpers
251
+ // and with the line-index, to show the saving.
252
+
253
+ struct DotWalkCur<'s> {
254
+ src: &'s [u8],
255
+ sink: usize,
256
+ }
257
+ impl<'pr, 's> Visit<'pr> for DotWalkCur<'s> {
258
+ fn visit_call_node(&mut self, node: &ruby_prism::CallNode<'pr>) {
259
+ if let Some(dot) = node.call_operator_loc() {
260
+ let dt = &self.src[dot.start_offset()..dot.end_offset()];
261
+ if (dt == b"." || dt == b"&.")
262
+ && node.receiver().is_some()
263
+ && let Some(sel) = node
264
+ .message_loc()
265
+ .map(|m| m.start_offset())
266
+ .or_else(|| node.opening_loc().map(|o| o.start_offset()))
267
+ {
268
+ let recv_end = node.receiver().unwrap().location().end_offset();
269
+ // The real is_offense() O(n) prefix scans (two of them) before bail:
270
+ let a = cur_line_of(self.src, sel);
271
+ let b = cur_line_of(self.src, recv_end);
272
+ self.sink = self.sink.wrapping_add(a ^ b);
273
+ }
274
+ }
275
+ ruby_prism::visit_call_node(self, node);
276
+ }
277
+ }
278
+
279
+ struct DotWalkIdx<'s> {
280
+ idx: &'s LineIndex,
281
+ src: &'s [u8],
282
+ sink: usize,
283
+ }
284
+ impl<'pr, 's> Visit<'pr> for DotWalkIdx<'s> {
285
+ fn visit_call_node(&mut self, node: &ruby_prism::CallNode<'pr>) {
286
+ if let Some(dot) = node.call_operator_loc() {
287
+ let dt = &self.src[dot.start_offset()..dot.end_offset()];
288
+ if (dt == b"." || dt == b"&.")
289
+ && node.receiver().is_some()
290
+ && let Some(sel) = node
291
+ .message_loc()
292
+ .map(|m| m.start_offset())
293
+ .or_else(|| node.opening_loc().map(|o| o.start_offset()))
294
+ {
295
+ let recv_end = node.receiver().unwrap().location().end_offset();
296
+ let a = self.idx.line_of(sel);
297
+ let b = self.idx.line_of(recv_end);
298
+ self.sink = self.sink.wrapping_add(a ^ b);
299
+ }
300
+ }
301
+ ruby_prism::visit_call_node(self, node);
302
+ }
303
+ }
304
+
305
+ // ----------------- phase runners -----------------
306
+
307
+ fn run_parse(sources: &[Vec<u8>]) -> usize {
308
+ let mut sink = 0usize;
309
+ for s in sources {
310
+ let r = parse(s);
311
+ sink = sink.wrapping_add(r.node().location().end_offset());
312
+ }
313
+ sink
314
+ }
315
+
316
+ fn run_walk_noop(sources: &[Vec<u8>]) -> usize {
317
+ let mut sink = 0usize;
318
+ for s in sources {
319
+ let mut r = NoopRule;
320
+ dispatch::run(s, &mut [&mut r]);
321
+ sink = sink.wrapping_add(s.len());
322
+ }
323
+ sink
324
+ }
325
+
326
+ fn run_frame_firstarg(sources: &[Vec<u8>]) -> usize {
327
+ let mut sink = 0usize;
328
+ for s in sources {
329
+ let mut r = FrameFirstArg {
330
+ sink: 0,
331
+ stack: Vec::new(),
332
+ };
333
+ dispatch::run(s, &mut [&mut r]);
334
+ sink = sink.wrapping_add(r.sink);
335
+ }
336
+ sink
337
+ }
338
+
339
+ fn run_frame_indent(sources: &[Vec<u8>]) -> usize {
340
+ let mut sink = 0usize;
341
+ for s in sources {
342
+ let mut r = FrameIndent {
343
+ src: s,
344
+ sink: 0,
345
+ depth: 0,
346
+ };
347
+ dispatch::run(s, &mut [&mut r]);
348
+ sink = sink.wrapping_add(r.sink);
349
+ }
350
+ sink
351
+ }
352
+
353
+ fn run_frame_indent_idx(sources: &[Vec<u8>]) -> usize {
354
+ let mut sink = 0usize;
355
+ for s in sources {
356
+ let idx = LineIndex::build(s);
357
+ let mut r = FrameIndentIdx {
358
+ idx: &idx,
359
+ sink: 0,
360
+ depth: 0,
361
+ };
362
+ dispatch::run(s, &mut [&mut r]);
363
+ sink = sink.wrapping_add(r.sink);
364
+ }
365
+ sink
366
+ }
367
+
368
+ fn run_frame_mc(sources: &[Vec<u8>]) -> usize {
369
+ let mut sink = 0usize;
370
+ for s in sources {
371
+ let mut r = FrameMc {
372
+ sink: 0,
373
+ stack: Vec::new(),
374
+ };
375
+ dispatch::run(s, &mut [&mut r]);
376
+ sink = sink.wrapping_add(r.sink);
377
+ }
378
+ sink
379
+ }
380
+
381
+ fn run_dot_walk_cur(sources: &[Vec<u8>]) -> usize {
382
+ let mut sink = 0usize;
383
+ for s in sources {
384
+ let res = parse(s);
385
+ let mut v = DotWalkCur { src: s, sink: 0 };
386
+ v.visit(&res.node());
387
+ sink = sink.wrapping_add(v.sink);
388
+ }
389
+ sink
390
+ }
391
+
392
+ fn run_dot_walk_idx(sources: &[Vec<u8>]) -> usize {
393
+ let mut sink = 0usize;
394
+ for s in sources {
395
+ let res = parse(s);
396
+ let idx = LineIndex::build(s);
397
+ let mut v = DotWalkIdx {
398
+ idx: &idx,
399
+ src: s,
400
+ sink: 0,
401
+ };
402
+ v.visit(&res.node());
403
+ sink = sink.wrapping_add(v.sink);
404
+ }
405
+ sink
406
+ }
407
+
408
+ // --- full real cops (unchanged logic) ---
409
+
410
+ fn run_full_firstarg(sources: &[Vec<u8>]) -> usize {
411
+ let mut sink = 0usize;
412
+ for s in sources {
413
+ sink = sink.wrapping_add(
414
+ rules::first_argument_indentation::check_first_argument_indentation(s, 0, 2, false)
415
+ .len(),
416
+ );
417
+ }
418
+ sink
419
+ }
420
+
421
+ fn run_full_indent(sources: &[Vec<u8>]) -> usize {
422
+ let cfg = rules::indentation_width::Config {
423
+ width: 2,
424
+ relative_to_receiver: false,
425
+ access_modifier_outdent: false,
426
+ indented_internal_methods: false,
427
+ end_align: 2,
428
+ def_end_align_def: false,
429
+ use_tabs: false,
430
+ };
431
+ let allowed: Vec<usize> = Vec::new();
432
+ let prior: Vec<(usize, usize)> = Vec::new();
433
+ let mut sink = 0usize;
434
+ for s in sources {
435
+ sink = sink.wrapping_add(
436
+ rules::indentation_width::check_indentation_width(s, cfg, &allowed, &prior).len(),
437
+ );
438
+ }
439
+ sink
440
+ }
441
+
442
+ fn run_full_bundle(sources: &[Vec<u8>]) -> usize {
443
+ let mut sink = 0usize;
444
+ for s in sources {
445
+ let (op, mc) = rules::bundle::check_multiline_bundle(s, 0, 2, 2, 0, 2, 2);
446
+ sink = sink.wrapping_add(op.len()).wrapping_add(mc.len());
447
+ }
448
+ sink
449
+ }
450
+
451
+ fn run_full_dot(sources: &[Vec<u8>]) -> usize {
452
+ let mut sink = 0usize;
453
+ for s in sources {
454
+ sink = sink.wrapping_add(rules::dot_position::check_dot_position(s, 0).len());
455
+ }
456
+ sink
457
+ }
458
+
459
+ // Microbench: count how many CallNode offsets a corpus produces and the
460
+ // total cost of cur_line_of vs index.line_of over ALL those offsets.
461
+
462
+ fn collect_call_offsets(sources: &[Vec<u8>]) -> Vec<(usize, Vec<usize>)> {
463
+ struct Collect {
464
+ offs: Vec<usize>,
465
+ }
466
+ impl<'pr> Visit<'pr> for Collect {
467
+ fn visit_call_node(&mut self, node: &ruby_prism::CallNode<'pr>) {
468
+ self.offs.push(node.as_node().location().start_offset());
469
+ if let Some(r) = node.receiver() {
470
+ self.offs.push(r.location().end_offset());
471
+ }
472
+ ruby_prism::visit_call_node(self, node);
473
+ }
474
+ }
475
+ sources
476
+ .iter()
477
+ .map(|s| {
478
+ let res = parse(s);
479
+ let mut c = Collect { offs: Vec::new() };
480
+ c.visit(&res.node());
481
+ (s.len(), c.offs)
482
+ })
483
+ .collect()
484
+ }
485
+
486
+ fn run_mode(mode: &str, sources: &[Vec<u8>]) -> usize {
487
+ match mode {
488
+ "parse" => run_parse(sources),
489
+ "walk_noop" => run_walk_noop(sources),
490
+ "frame_firstarg" => run_frame_firstarg(sources),
491
+ "frame_indent" => run_frame_indent(sources),
492
+ "frame_indent_idx" => run_frame_indent_idx(sources),
493
+ "frame_mc" => run_frame_mc(sources),
494
+ "full_firstarg" => run_full_firstarg(sources),
495
+ "full_indent" => run_full_indent(sources),
496
+ "full_bundle" => run_full_bundle(sources),
497
+ "full_dot" => run_full_dot(sources),
498
+ "dot_walk_cur" => run_dot_walk_cur(sources),
499
+ "dot_walk_idx" => run_dot_walk_idx(sources),
500
+ other => {
501
+ eprintln!("unknown mode: {other}");
502
+ std::process::exit(2);
503
+ }
504
+ }
505
+ }
506
+
507
+ fn time_mode(mode: &str, sources: &[Vec<u8>], iters: usize) -> f64 {
508
+ let mut times = Vec::with_capacity(iters);
509
+ let mut sink = 0usize;
510
+ for _ in 0..iters {
511
+ let t0 = cpu_time_secs();
512
+ sink = sink.wrapping_add(run_mode(mode, sources));
513
+ let t1 = cpu_time_secs();
514
+ times.push(t1 - t0);
515
+ }
516
+ times.sort_by(|a, b| a.partial_cmp(b).unwrap());
517
+ let median = times[times.len() / 2];
518
+ eprintln!("mode={mode} median={median:.6} sink={sink}");
519
+ median
520
+ }
521
+
522
+ fn main() {
523
+ let args: Vec<String> = std::env::args().collect();
524
+ if args.len() < 3 {
525
+ eprintln!("usage: hotspot_profile <mode|micro> <corpus_dir> [iterations]");
526
+ std::process::exit(2);
527
+ }
528
+ let mode = args[1].clone();
529
+ let dir = &args[2];
530
+ let iters: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(1);
531
+ let sources = collect_rb_files(dir);
532
+ if sources.is_empty() {
533
+ eprintln!("no .rb files under {dir}");
534
+ std::process::exit(1);
535
+ }
536
+ eprintln!("files={} iters={iters}", sources.len());
537
+
538
+ if mode == "micro" {
539
+ // Cost of line_of over EVERY callnode offset: current O(n) vs index O(log n).
540
+ let data = collect_call_offsets(&sources);
541
+ let total_offs: usize = data.iter().map(|(_, o)| o.len()).sum();
542
+ eprintln!("total callnode offsets={total_offs}");
543
+
544
+ // current
545
+ let mut sink = 0usize;
546
+ let t0 = cpu_time_secs();
547
+ for (_len, offs) in &data {
548
+ // We need the source; recompute via index of data position.
549
+ // (sources and data are parallel.)
550
+ let _ = offs;
551
+ }
552
+ // Re-pair with sources for correctness.
553
+ let t_cur = {
554
+ let mut s = 0usize;
555
+ let t0 = cpu_time_secs();
556
+ for (src, (_l, offs)) in sources.iter().zip(&data) {
557
+ for &o in offs {
558
+ s = s.wrapping_add(cur_line_of(src, o));
559
+ }
560
+ }
561
+ let t1 = cpu_time_secs();
562
+ sink = sink.wrapping_add(s);
563
+ t1 - t0
564
+ };
565
+ let _ = t0;
566
+
567
+ // index: build once per source then query
568
+ let t_idx_build;
569
+ let t_idx_query;
570
+ {
571
+ let mut s = 0usize;
572
+ let t0 = cpu_time_secs();
573
+ let indices: Vec<LineIndex> = sources.iter().map(|src| LineIndex::build(src)).collect();
574
+ let t1 = cpu_time_secs();
575
+ t_idx_build = t1 - t0;
576
+ let t2 = cpu_time_secs();
577
+ for (idx, (_l, offs)) in indices.iter().zip(&data) {
578
+ for &o in offs {
579
+ s = s.wrapping_add(idx.line_of(o));
580
+ }
581
+ }
582
+ let t3 = cpu_time_secs();
583
+ t_idx_query = t3 - t2;
584
+ sink = sink.wrapping_add(s);
585
+ }
586
+ eprintln!(
587
+ "micro line_of: current_Onprefix={t_cur:.6}s index_build={t_idx_build:.6}s index_query={t_idx_query:.6}s index_total={:.6}s sink={sink}",
588
+ t_idx_build + t_idx_query
589
+ );
590
+ println!("{t_cur:.6}\t{:.6}", t_idx_build + t_idx_query);
591
+ return;
592
+ }
593
+
594
+ let m = time_mode(&mode, &sources, iters);
595
+ println!("{m:.6}");
596
+ }