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,644 @@
1
+ //! `Layout/FirstArrayElementIndentation`.
2
+ //!
3
+ //! Checks the indentation of the first element of an array literal whose
4
+ //! opening bracket and first element are on separate lines, and of a hanging
5
+ //! right bracket. Same `AlignmentCorrector` division of labour as the other
6
+ //! alignment cops: Rust computes the offense range (the first element, or the
7
+ //! `]` token), the `column_delta` and the message; Ruby applies the
8
+ //! realignment via `AlignmentCorrector`.
9
+ //!
10
+ //! Columns are parser-gem columns (character counts from the line start), not
11
+ //! display width: the `MultilineElementIndentation` mixin reads
12
+ //! `source_range.column` / `=~ /\S/` directly instead of `display_column`.
13
+ //!
14
+ //! The trickiest part is replicating which left parenthesis "claims" an array
15
+ //! (stock's `each_argument_node` + `ignore_node` dance): `on_send` fires in
16
+ //! pre-order and scans the call's arguments with `on_node(:array, arg, :send)`,
17
+ //! which descends into everything *except* nested plain `send`s — parser's
18
+ //! `:block` node keeps a call's block body scannable, `:csend` is never
19
+ //! excluded, and an index call (`foo[...]`) is a `send` with `loc.begin` `nil`
20
+ //! so it blocks but can never claim. The ancestor-stack walk reproduces this:
21
+ //! the deepest plain-call ancestor (path not entering through its block) is a
22
+ //! blocking boundary, and the outermost candidate call at or below it whose
23
+ //! `(` is on the array's opening line claims the array.
24
+
25
+ use std::rc::Rc;
26
+
27
+ use ruby_prism::{Location, Node};
28
+
29
+ use super::line_index::LineIndex;
30
+
31
+ /// One misindented first element or right bracket. `[start_offset,
32
+ /// end_offset)` is the offense range, which Ruby both reports and realigns by
33
+ /// `column_delta` via `AlignmentCorrector` (resolving the element's AST node
34
+ /// for the string-taboo handling; the `]` range stays a range, like stock).
35
+ pub struct FirstArrayElemIndentOffense {
36
+ pub start_offset: usize,
37
+ pub end_offset: usize,
38
+ pub column_delta: isize,
39
+ pub message: String,
40
+ }
41
+
42
+ #[derive(Clone, Copy, PartialEq, Eq)]
43
+ enum Style {
44
+ /// `special_inside_parentheses` (default).
45
+ SpecialInsideParens,
46
+ Consistent,
47
+ AlignBrackets,
48
+ }
49
+
50
+ impl Style {
51
+ fn from_u8(v: u8) -> Self {
52
+ match v {
53
+ 1 => Style::Consistent,
54
+ 2 => Style::AlignBrackets,
55
+ _ => Style::SpecialInsideParens,
56
+ }
57
+ }
58
+ }
59
+
60
+ /// `indent_base`'s second return value: what the expected column is based on.
61
+ #[derive(Clone, Copy)]
62
+ enum BaseType {
63
+ /// `:left_brace_or_bracket` (`align_brackets` style).
64
+ LeftBracket,
65
+ /// `:first_column_after_left_parenthesis`.
66
+ AfterParen,
67
+ /// `:parent_hash_key`.
68
+ ParentHashKey,
69
+ /// `:start_of_line`.
70
+ StartOfLine,
71
+ }
72
+
73
+ pub fn check_first_array_element_indentation(
74
+ source: &[u8],
75
+ style: u8,
76
+ indent_width: usize,
77
+ enforce_fixed_indentation: bool,
78
+ ) -> Vec<FirstArrayElemIndentOffense> {
79
+ let Some(mut rule) = build_rule(source, style, indent_width, enforce_fixed_indentation) else {
80
+ return Vec::new();
81
+ };
82
+ super::dispatch::run(source, &mut [&mut rule]);
83
+ rule.offenses
84
+ }
85
+
86
+ /// Build the rule for use standalone or in a shared-walk bundle. `None` when
87
+ /// the cop is disabled outright: `Layout/ArrayAlignment` enforcing
88
+ /// `with_fixed_indentation` gates both `on_array` and `on_send` unless the
89
+ /// style is `consistent`.
90
+ pub(crate) fn build_rule(
91
+ source: &[u8],
92
+ style: u8,
93
+ indent_width: usize,
94
+ enforce_fixed_indentation: bool,
95
+ ) -> Option<Visitor<'_>> {
96
+ let style = Style::from_u8(style);
97
+ if enforce_fixed_indentation && style != Style::Consistent {
98
+ return None;
99
+ }
100
+ Some(Visitor {
101
+ source,
102
+ line_index: super::line_index::with_line_index(source, |li| li.clone()),
103
+ style,
104
+ indent: indent_width,
105
+ stack: Vec::new(),
106
+ offenses: Vec::new(),
107
+ })
108
+ }
109
+
110
+ /// Lightweight ancestor frame kind, carrying exactly what the claiming logic
111
+ /// and the parent-hash-key base need.
112
+ ///
113
+ /// NOTE: `ArgumentsNode` (like `StatementsNode`) is reached through its
114
+ /// parent's concretely-typed field (`visit_arguments_node`), which bypasses
115
+ /// the generic branch hooks — it never appears on the stack. "The path enters
116
+ /// through the call's arguments" is therefore an offset test against the
117
+ /// `ArgumentsNode`'s range stored on the call's own frame; skipped transparent
118
+ /// levels cannot change a containment test because node ranges nest.
119
+ enum FrameKind {
120
+ /// A parser `send` / `csend`. `paren_start` is the begin offset of a
121
+ /// literal `(` (an index call's `[` is `loc.begin == nil` in parser and
122
+ /// can never claim). `args_range` spans the `ArgumentsNode`, which in
123
+ /// parser is the send's own extent minus receiver/selector/block.
124
+ Call {
125
+ csend: bool,
126
+ paren_start: Option<usize>,
127
+ args_range: Option<(usize, usize)>,
128
+ },
129
+ /// A `&blk` block-pass (part of `node.arguments` in parser, so a path
130
+ /// through it counts as entering the call's arguments).
131
+ BlockArgument,
132
+ /// A `{}`/`do` block attached to a call: parser materialises a separate
133
+ /// `:block` node *containing* the send, so a path through the block is not
134
+ /// excluded by the send.
135
+ Block,
136
+ /// A hash `pair`, for `pair.loc.column` and `pair.last_line` (the frame's
137
+ /// own `start`/`end` span the pair).
138
+ Assoc {
139
+ key_start: usize,
140
+ value_start: usize,
141
+ },
142
+ /// A hash (braced or keyword). `elements` are the children's ranges, for
143
+ /// `pair.right_sibling`.
144
+ Hash {
145
+ elements: Vec<(usize, usize)>,
146
+ },
147
+ /// An attribute / index op-assign (`a.b += x`, `a[i] ||= x`): parser nests
148
+ /// a plain `send` covering everything up to `block_end` (`a.b` / `a[i]`)
149
+ /// inside the assign node, blocking `on_node` scans into that range, while
150
+ /// the assigned value sits outside it and stays scannable.
151
+ AsgnTarget {
152
+ block_end: usize,
153
+ },
154
+ Other,
155
+ }
156
+
157
+ /// Ancestor frame: the node's range plus its classified kind.
158
+ struct Frame {
159
+ start: usize,
160
+ end: usize,
161
+ kind: FrameKind,
162
+ }
163
+
164
+ pub(crate) struct Visitor<'a> {
165
+ source: &'a [u8],
166
+ line_index: Rc<LineIndex>,
167
+ style: Style,
168
+ indent: usize,
169
+ stack: Vec<Frame>,
170
+ pub(crate) offenses: Vec<FirstArrayElemIndentOffense>,
171
+ }
172
+
173
+ fn loc(l: &Location<'_>) -> (usize, usize) {
174
+ (l.start_offset(), l.end_offset())
175
+ }
176
+
177
+ /// Ruby regex `\s` (the line-local subset; `\n` cannot occur inside a line).
178
+ fn is_ws(b: u8) -> bool {
179
+ matches!(b, b' ' | b'\t' | b'\x0b' | b'\x0c' | b'\r')
180
+ }
181
+
182
+ impl super::dispatch::Rule for Visitor<'_> {
183
+ fn enter(&mut self, node: &Node<'_>) {
184
+ if let Some(a) = node.as_array_node() {
185
+ self.process_array(&a);
186
+ }
187
+ self.stack.push(self.make_frame(node));
188
+ }
189
+
190
+ fn leave(&mut self) {
191
+ self.stack.pop();
192
+ }
193
+ }
194
+
195
+ impl<'a> Visitor<'a> {
196
+ fn make_frame(&self, node: &Node<'_>) -> Frame {
197
+ let (start, end) = loc(&node.location());
198
+ let kind = if let Some(c) = node.as_call_node() {
199
+ let csend = c
200
+ .call_operator_loc()
201
+ .map(|l| l.as_slice() == b"&.")
202
+ .unwrap_or(false);
203
+ let paren_start = c
204
+ .opening_loc()
205
+ .filter(|o| o.as_slice() == b"(")
206
+ .map(|o| o.start_offset());
207
+ FrameKind::Call {
208
+ csend,
209
+ paren_start,
210
+ args_range: c.arguments().map(|a| loc(&a.as_node().location())),
211
+ }
212
+ } else if node.as_block_argument_node().is_some() {
213
+ FrameKind::BlockArgument
214
+ } else if node.as_block_node().is_some() {
215
+ FrameKind::Block
216
+ } else if let Some(a) = node.as_assoc_node() {
217
+ FrameKind::Assoc {
218
+ key_start: a.key().location().start_offset(),
219
+ value_start: a.value().location().start_offset(),
220
+ }
221
+ } else if let Some(h) = node.as_hash_node() {
222
+ FrameKind::Hash {
223
+ elements: h.elements().iter().map(|e| loc(&e.location())).collect(),
224
+ }
225
+ } else if let Some(h) = node.as_keyword_hash_node() {
226
+ FrameKind::Hash {
227
+ elements: h.elements().iter().map(|e| loc(&e.location())).collect(),
228
+ }
229
+ } else if let Some(k) = asgn_target_kind(node) {
230
+ k
231
+ } else {
232
+ FrameKind::Other
233
+ };
234
+ Frame { start, end, kind }
235
+ }
236
+
237
+ /// 1-based line number of `off`.
238
+ fn line_of(&self, off: usize) -> usize {
239
+ self.line_index.line_of(off)
240
+ }
241
+
242
+ /// parser-gem `Source::Range#column`: character column within the line.
243
+ fn column(&self, off: usize) -> usize {
244
+ self.line_index.column(self.source, off)
245
+ }
246
+
247
+ /// `left_brace.source_line =~ /\S/`: the column of the first non-blank
248
+ /// character of `off`'s line (the leading run is ASCII, so bytes == chars;
249
+ /// the line cannot be all-blank — `off` points at a token on it).
250
+ fn line_first_nonws_column(&self, off: usize) -> usize {
251
+ let ls = self.line_index.line_start(off);
252
+ self.source[ls..].iter().take_while(|&&b| is_ws(b)).count()
253
+ }
254
+
255
+ /// The `(` that claims this array via stock's `on_send` path, if any: the
256
+ /// outermost eligible ancestor call not separated from the array by a
257
+ /// plain `send` (`on_node(:array, arg, :send)` stops at nested sends), with
258
+ /// parentheses on the same line as the array's opening bracket. Pre-order
259
+ /// `on_send` + `ignore_node` make the outermost match win.
260
+ fn claimed_paren(&self, array_open_start: usize) -> Option<usize> {
261
+ let array_line = self.line_of(array_open_start);
262
+ let n = self.stack.len();
263
+ // Start offset of the next node on the path from frame `i` towards the
264
+ // array (the array itself when `i` is the direct parent).
265
+ let next_start =
266
+ |i: usize| -> usize { self.stack.get(i + 1).map_or(array_open_start, |f| f.start) };
267
+ // The deepest blocking ancestor: a plain (non-`&.`) call whose path
268
+ // towards the array does not enter through its block, or an op-assign
269
+ // target whose blocked range (parser's nested plain `send`) the path
270
+ // enters. Calls *below* the boundary can still claim.
271
+ let mut start = 0;
272
+ for i in 0..n {
273
+ match self.stack[i].kind {
274
+ FrameKind::Call { csend: false, .. } => {
275
+ let child_is_block =
276
+ i + 1 < n && matches!(self.stack[i + 1].kind, FrameKind::Block);
277
+ if !child_is_block {
278
+ start = i;
279
+ }
280
+ }
281
+ FrameKind::AsgnTarget { block_end } if next_start(i) < block_end => {
282
+ start = i;
283
+ }
284
+ _ => {}
285
+ }
286
+ }
287
+ for i in start..n {
288
+ let FrameKind::Call {
289
+ paren_start: Some(p),
290
+ args_range,
291
+ ..
292
+ } = self.stack[i].kind
293
+ else {
294
+ continue;
295
+ };
296
+ // `each_argument_node` scans only the call's own arguments (which
297
+ // in parser include a `&blk` block-pass — Prism's separate
298
+ // `BlockArgumentNode` field), never its receiver.
299
+ let ns = next_start(i);
300
+ let via_arguments = args_range.is_some_and(|(s, e)| s <= ns && ns < e)
301
+ || (i + 1 < n && matches!(self.stack[i + 1].kind, FrameKind::BlockArgument));
302
+ if via_arguments && self.line_of(p) == array_line {
303
+ return Some(p);
304
+ }
305
+ }
306
+ None
307
+ }
308
+
309
+ /// `check(array_node, left_parenthesis)`.
310
+ fn process_array(&mut self, a: &ruby_prism::ArrayNode<'_>) {
311
+ let Some(open) = a.opening_loc() else { return };
312
+ let open_start = open.start_offset();
313
+ let paren = self.claimed_paren(open_start);
314
+ let first = a.elements().iter().next().map(|e| loc(&e.location()));
315
+
316
+ if let Some(f) = first {
317
+ if self.line_of(f.0) == self.line_of(open_start) {
318
+ return;
319
+ }
320
+ self.check_first(f, open_start, paren);
321
+ }
322
+ if let Some(close) = a.closing_loc() {
323
+ self.check_right_bracket(loc(&close), first, open_start, paren);
324
+ }
325
+ }
326
+
327
+ /// `check_first(first, left_bracket, left_parenthesis, 0)`.
328
+ fn check_first(&mut self, first: (usize, usize), open_start: usize, paren: Option<usize>) {
329
+ let actual_column = self.column(first.0);
330
+ let (base_column, base_type) = self.indent_base(open_start, Some(first), paren);
331
+ let expected_column = base_column + self.indent;
332
+ let column_delta = expected_column as isize - actual_column as isize;
333
+ if column_delta == 0 {
334
+ return;
335
+ }
336
+ let message = format!(
337
+ "Use {} spaces for indentation in an array, relative to {}.",
338
+ self.indent,
339
+ base_description(base_type)
340
+ );
341
+ self.offenses.push(FirstArrayElemIndentOffense {
342
+ start_offset: first.0,
343
+ end_offset: first.1,
344
+ column_delta,
345
+ message,
346
+ });
347
+ }
348
+
349
+ /// `check_right_bracket(right_bracket, first_elem, left_bracket,
350
+ /// left_parenthesis)`.
351
+ fn check_right_bracket(
352
+ &mut self,
353
+ close: (usize, usize),
354
+ first: Option<(usize, usize)>,
355
+ open_start: usize,
356
+ paren: Option<usize>,
357
+ ) {
358
+ // Accept a right bracket that does not begin its line.
359
+ let ls = self.line_index.line_start(close.0);
360
+ if self.source[ls..close.0].iter().any(|&b| !is_ws(b)) {
361
+ return;
362
+ }
363
+ let (expected_column, base_type) = self.indent_base(open_start, first, paren);
364
+ let column_delta = expected_column as isize - self.column(close.0) as isize;
365
+ if column_delta == 0 {
366
+ return;
367
+ }
368
+ let message = match base_type {
369
+ BaseType::LeftBracket => "Indent the right bracket the same as the left bracket.",
370
+ BaseType::AfterParen => {
371
+ "Indent the right bracket the same as the first position \
372
+ after the preceding left parenthesis."
373
+ }
374
+ BaseType::ParentHashKey => "Indent the right bracket the same as the parent hash key.",
375
+ BaseType::StartOfLine => {
376
+ "Indent the right bracket the same as the start of the line \
377
+ where the left bracket is."
378
+ }
379
+ };
380
+ self.offenses.push(FirstArrayElemIndentOffense {
381
+ start_offset: close.0,
382
+ end_offset: close.1,
383
+ column_delta,
384
+ message: message.to_string(),
385
+ });
386
+ }
387
+
388
+ /// `indent_base(left_brace, first, left_parenthesis)`.
389
+ fn indent_base(
390
+ &self,
391
+ open_start: usize,
392
+ first: Option<(usize, usize)>,
393
+ paren: Option<usize>,
394
+ ) -> (usize, BaseType) {
395
+ if self.style == Style::AlignBrackets {
396
+ return (self.column(open_start), BaseType::LeftBracket);
397
+ }
398
+ if first.is_some()
399
+ && let Some(col) = self.parent_hash_key_column()
400
+ {
401
+ return (col, BaseType::ParentHashKey);
402
+ }
403
+ if let Some(p) = paren
404
+ && self.style == Style::SpecialInsideParens
405
+ {
406
+ return (self.column(p) + 1, BaseType::AfterParen);
407
+ }
408
+ (
409
+ self.line_first_nonws_column(open_start),
410
+ BaseType::StartOfLine,
411
+ )
412
+ }
413
+
414
+ /// `hash_pair_where_value_beginning_with` + the two pair conditions: the
415
+ /// array's direct parent is a hash pair whose key and value begin on the
416
+ /// same line and whose right sibling begins on a subsequent line. Returns
417
+ /// `pair.loc.column`.
418
+ fn parent_hash_key_column(&self) -> Option<usize> {
419
+ let n = self.stack.len();
420
+ let pair = self.stack.last()?;
421
+ let FrameKind::Assoc {
422
+ key_start,
423
+ value_start,
424
+ } = pair.kind
425
+ else {
426
+ return None;
427
+ };
428
+ let (start, end) = (pair.start, pair.end);
429
+ // `key_and_value_begin_on_same_line?(pair)`.
430
+ if self.line_of(key_start) != self.line_of(value_start) {
431
+ return None;
432
+ }
433
+ // `right_sibling_begins_on_subsequent_line?(pair)`.
434
+ let FrameKind::Hash { elements } = &self.stack[n.checked_sub(2)?].kind else {
435
+ return None;
436
+ };
437
+ let idx = elements.iter().position(|&(s, _)| s == start)?;
438
+ let sibling = elements.get(idx + 1)?;
439
+ if self.line_of(end - 1) >= self.line_of(sibling.0) {
440
+ return None;
441
+ }
442
+ Some(self.column(start))
443
+ }
444
+ }
445
+
446
+ /// Classifies `a.b op= x` / `a[i] op= x` as an [`FrameKind::AsgnTarget`]:
447
+ /// parser nests a plain `send` spanning everything before the assignment
448
+ /// operator (`a.b` / `a[i]`), which blocks `on_node` scans into that range.
449
+ /// Safe-navigation forms nest a `csend` instead, which `on_node` does not
450
+ /// exclude, so they stay transparent (`None`).
451
+ fn asgn_target_kind(node: &Node<'_>) -> Option<FrameKind> {
452
+ let (safe, op_start) = if let Some(n) = node.as_call_operator_write_node() {
453
+ (
454
+ n.is_safe_navigation(),
455
+ n.binary_operator_loc().start_offset(),
456
+ )
457
+ } else if let Some(n) = node.as_call_and_write_node() {
458
+ (n.is_safe_navigation(), n.operator_loc().start_offset())
459
+ } else if let Some(n) = node.as_call_or_write_node() {
460
+ (n.is_safe_navigation(), n.operator_loc().start_offset())
461
+ } else if let Some(n) = node.as_index_operator_write_node() {
462
+ (
463
+ n.is_safe_navigation(),
464
+ n.binary_operator_loc().start_offset(),
465
+ )
466
+ } else if let Some(n) = node.as_index_and_write_node() {
467
+ (n.is_safe_navigation(), n.operator_loc().start_offset())
468
+ } else if let Some(n) = node.as_index_or_write_node() {
469
+ (n.is_safe_navigation(), n.operator_loc().start_offset())
470
+ } else {
471
+ return None;
472
+ };
473
+ (!safe).then_some(FrameKind::AsgnTarget {
474
+ block_end: op_start,
475
+ })
476
+ }
477
+
478
+ /// `base_description(indent_base_type)`.
479
+ fn base_description(base_type: BaseType) -> &'static str {
480
+ match base_type {
481
+ BaseType::LeftBracket => "the position of the opening bracket",
482
+ BaseType::AfterParen => "the first position after the preceding left parenthesis",
483
+ BaseType::ParentHashKey => "the parent hash key",
484
+ BaseType::StartOfLine => "the start of the line where the left square bracket is",
485
+ }
486
+ }
487
+
488
+ #[cfg(test)]
489
+ mod tests {
490
+ use super::*;
491
+
492
+ fn run(source: &str, style: u8) -> Vec<(usize, usize, isize, String)> {
493
+ check_first_array_element_indentation(source.as_bytes(), style, 2, false)
494
+ .into_iter()
495
+ .map(|o| (o.start_offset, o.end_offset, o.column_delta, o.message))
496
+ .collect()
497
+ }
498
+
499
+ #[test]
500
+ fn operand_array_first_element_and_right_bracket() {
501
+ let got = run("a << [\n 1\n ]\n", 0);
502
+ assert_eq!(got.len(), 2);
503
+ assert_eq!(
504
+ (got[0].2, got[0].3.as_str()),
505
+ (
506
+ 1,
507
+ "Use 2 spaces for indentation in an array, relative to the \
508
+ start of the line where the left square bracket is."
509
+ )
510
+ );
511
+ assert_eq!(
512
+ (got[1].2, got[1].3.as_str()),
513
+ (
514
+ -2,
515
+ "Indent the right bracket the same as the start of the line \
516
+ where the left bracket is."
517
+ )
518
+ );
519
+ }
520
+
521
+ #[test]
522
+ fn accepts_correct_indentation_and_same_line_first_element() {
523
+ assert!(run("a = [\n 1\n]\n", 0).is_empty());
524
+ assert!(run("a = [1,\n 2]\n", 0).is_empty());
525
+ assert!(run("a = [1, 2]\n", 0).is_empty());
526
+ assert!(run("a = []\n", 0).is_empty());
527
+ assert!(run("a, b = b, a\n", 0).is_empty());
528
+ }
529
+
530
+ #[test]
531
+ fn special_inside_parentheses_claims_via_send() {
532
+ let got = run("func([\n 1\n])\n", 0);
533
+ assert_eq!(got.len(), 2);
534
+ assert!(
535
+ got[0]
536
+ .3
537
+ .contains("the first position after the preceding left parenthesis"),
538
+ "{}",
539
+ got[0].3
540
+ );
541
+ assert_eq!(got[0].2, 5);
542
+ assert_eq!(got[1].2, 5);
543
+ }
544
+
545
+ #[test]
546
+ fn consistent_style_ignores_parentheses() {
547
+ let got = run("func([\n 1\n ])\n", 1);
548
+ assert_eq!(got.len(), 2);
549
+ assert!(got[0].3.contains("the start of the line"), "{}", got[0].3);
550
+ assert_eq!(got[0].2, -5);
551
+ }
552
+
553
+ #[test]
554
+ fn align_brackets_uses_bracket_column() {
555
+ let got = run("var = [\n 1\n]\n", 2);
556
+ assert_eq!(got.len(), 2);
557
+ assert!(
558
+ got[0].3.contains("the position of the opening bracket"),
559
+ "{}",
560
+ got[0].3
561
+ );
562
+ assert_eq!(got[0].2, 6); // expected 6 + 2 = 8, actual 2
563
+ assert_eq!(got[1].2, 6);
564
+ }
565
+
566
+ #[test]
567
+ fn parent_hash_key_base_for_multi_pair_hash() {
568
+ let got = run(
569
+ "func(x: [\n :a,\n :b\n],\n y: [\n :c\n ])\n",
570
+ 0,
571
+ );
572
+ assert_eq!(got.len(), 2);
573
+ assert!(got[0].3.contains("the parent hash key"), "{}", got[0].3);
574
+ assert_eq!(got[0].2, 5); // pair col 5 + 2 = 7, actual 2
575
+ assert!(got[1].3.contains("parent hash key"), "{}", got[1].3);
576
+ assert_eq!(got[1].2, 5);
577
+ }
578
+
579
+ #[test]
580
+ fn index_send_never_claims() {
581
+ // `foo[...]` has no `(`; the inner array falls back to start-of-line.
582
+ assert!(run("foo[[\n 1]]\n", 0).is_empty());
583
+ }
584
+
585
+ #[test]
586
+ fn block_body_is_scannable_through_the_send() {
587
+ // parser's `:block` node contains the send, so `func`'s scan reaches
588
+ // the array inside the block body.
589
+ let got = run("func(foo { [\n 1\n] })\n", 0);
590
+ assert_eq!(got.len(), 2);
591
+ assert!(
592
+ got[0].3.contains("preceding left parenthesis"),
593
+ "{}",
594
+ got[0].3
595
+ );
596
+ assert_eq!(got[0].2, 5);
597
+ }
598
+
599
+ #[test]
600
+ fn nested_plain_send_blocks_outer_claim() {
601
+ // `inner` (a plain send) stops `outer`'s scan; `inner` itself claims:
602
+ // base is inner's `(` col 11 + 1, expected 14, not outer's 5 + 1 + 2.
603
+ let got = run("outer(inner([\n 1\n]))\n", 0);
604
+ assert_eq!(got.len(), 2);
605
+ assert_eq!(got[0].2, 12); // 14 - actual col 2
606
+ assert_eq!(got[1].2, 12); // `]` at col 0
607
+ }
608
+
609
+ #[test]
610
+ fn op_assign_value_is_claimable_but_target_is_not() {
611
+ // The assigned value sits outside parser's nested `a.b` send, so the
612
+ // outer `(` claims it (after-paren base, col 5 + 1 + 2 = 8).
613
+ let got = run("func(a.b += [\n 1\n])\n", 0);
614
+ assert_eq!(got.len(), 2);
615
+ assert!(
616
+ got[0].3.contains("preceding left parenthesis"),
617
+ "{}",
618
+ got[0].3
619
+ );
620
+ assert_eq!(got[0].2, 5);
621
+ assert_eq!(got[1].2, 5);
622
+ // An array inside the target (the receiver) is blocked from the outer
623
+ // `(` and falls back to start-of-line (col 0 + 2 = 2: no offense).
624
+ assert!(run("func([\n 1\n].b += x)\n", 0).is_empty());
625
+ }
626
+
627
+ #[test]
628
+ fn disabled_when_array_alignment_enforces_fixed_indentation() {
629
+ assert!(
630
+ check_first_array_element_indentation("func([\n 1\n])\n".as_bytes(), 0, 2, true)
631
+ .is_empty()
632
+ );
633
+ // ...unless the style is `consistent`.
634
+ assert!(
635
+ !check_first_array_element_indentation(
636
+ "func([\n 1\n ])\n".as_bytes(),
637
+ 1,
638
+ 2,
639
+ true
640
+ )
641
+ .is_empty()
642
+ );
643
+ }
644
+ }