rfmt 1.5.3 → 1.6.1

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.
@@ -0,0 +1,434 @@
1
+ //! BeginRule - Formats Ruby begin/rescue/ensure blocks
2
+ //!
3
+ //! Handles:
4
+ //! - Explicit begin...end blocks
5
+ //! - Implicit begin wrapping method body with rescue/ensure
6
+
7
+ use crate::ast::{Node, NodeType};
8
+ use crate::doc::{concat, hardline, indent, text, Doc};
9
+ use crate::error::Result;
10
+ use crate::format::context::FormatContext;
11
+ use crate::format::registry::RuleRegistry;
12
+ use crate::format::rule::{
13
+ format_child, format_leading_comments, format_statements, format_trailing_comment, FormatRule,
14
+ };
15
+
16
+ /// True when this BeginNode represents an implicit begin (the source does not
17
+ /// start with the `begin` keyword) AND carries at least one rescue/else/ensure
18
+ /// clause. Such bodies need the rescue/else/ensure keywords emitted at the
19
+ /// outer (e.g. `def`) indent level rather than at the body indent level.
20
+ pub(crate) fn is_implicit_begin_with_clauses(node: &Node, ctx: &FormatContext) -> bool {
21
+ if node.node_type != NodeType::BeginNode {
22
+ return false;
23
+ }
24
+ let has_clause = node.children.iter().any(|c| {
25
+ matches!(
26
+ c.node_type,
27
+ NodeType::RescueNode | NodeType::EnsureNode | NodeType::ElseNode
28
+ )
29
+ });
30
+ if !has_clause {
31
+ return false;
32
+ }
33
+ ctx.extract_source(node)
34
+ .map(|s| !s.trim_start().starts_with("begin"))
35
+ .unwrap_or(false)
36
+ }
37
+
38
+ /// Emits the body of a construct (def/class/module/block) whose body is an
39
+ /// implicit BeginNode with rescue/else/ensure clauses.
40
+ ///
41
+ /// The returned Doc is meant to sit between the opening line (e.g. `def foo`)
42
+ /// and a trailing `hardline + text("end")` emitted by the caller. Body
43
+ /// statements are wrapped in `indent`, while rescue/else/ensure clause
44
+ /// keywords are emitted at the caller's current indent level so that they
45
+ /// align with the opener instead of the body.
46
+ pub(crate) fn format_implicit_begin_body(
47
+ node: &Node,
48
+ ctx: &mut FormatContext,
49
+ registry: &RuleRegistry,
50
+ ) -> Result<Doc> {
51
+ let mut body_children: Vec<&Node> = Vec::new();
52
+ let mut clause_children: Vec<&Node> = Vec::new();
53
+
54
+ for child in &node.children {
55
+ match child.node_type {
56
+ NodeType::RescueNode | NodeType::EnsureNode | NodeType::ElseNode => {
57
+ clause_children.push(child);
58
+ }
59
+ _ => {
60
+ body_children.push(child);
61
+ }
62
+ }
63
+ }
64
+
65
+ let mut docs: Vec<Doc> = Vec::with_capacity(clause_children.len() * 2 + 2);
66
+
67
+ if !body_children.is_empty() {
68
+ let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2 + 1);
69
+ body_docs.push(hardline());
70
+ for (i, child) in body_children.iter().enumerate() {
71
+ if i > 0 {
72
+ body_docs.push(hardline());
73
+ }
74
+ body_docs.push(format_child(child, ctx, registry)?);
75
+ }
76
+ docs.push(indent(concat(body_docs)));
77
+ }
78
+
79
+ for clause in clause_children {
80
+ docs.push(hardline());
81
+ docs.push(format_begin_clause(clause, ctx, registry)?);
82
+ }
83
+
84
+ Ok(concat(docs))
85
+ }
86
+
87
+ /// Emits a rescue/else/ensure clause the way the enclosing begin expects.
88
+ ///
89
+ /// `format_child` sends ElseNode to `FallbackRule`, which slices the source
90
+ /// between `else` and the next keyword — but Prism's `ElseNode.location`
91
+ /// stretches into the *following* clause's keyword (`ensure`, or the
92
+ /// begin's `end`). Emitting that slice as-is duplicates whichever keyword
93
+ /// comes after, producing e.g. `else\n y\nensure\nensure\n z\nend` or
94
+ /// `else\n y\nend\nend`. Handle ElseNode explicitly so we emit only
95
+ /// `else\n body` and let the caller (or the subsequent EnsureRule) write
96
+ /// the following keyword.
97
+ fn format_begin_clause(
98
+ clause: &Node,
99
+ ctx: &mut FormatContext,
100
+ registry: &RuleRegistry,
101
+ ) -> Result<Doc> {
102
+ if !matches!(clause.node_type, NodeType::ElseNode) {
103
+ return format_child(clause, ctx, registry);
104
+ }
105
+
106
+ let mut docs: Vec<Doc> = Vec::with_capacity(3);
107
+ docs.push(text("else"));
108
+ for child in &clause.children {
109
+ if matches!(child.node_type, NodeType::StatementsNode) {
110
+ let body_doc = format_statements(child, ctx, registry)?;
111
+ docs.push(indent(concat(vec![hardline(), body_doc])));
112
+ }
113
+ }
114
+ Ok(concat(docs))
115
+ }
116
+
117
+ /// Rule for formatting begin/rescue/ensure blocks.
118
+ pub struct BeginRule;
119
+
120
+ /// Rule for formatting rescue clauses.
121
+ pub struct RescueRule;
122
+
123
+ /// Rule for formatting ensure clauses.
124
+ pub struct EnsureRule;
125
+
126
+ impl FormatRule for BeginRule {
127
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
128
+ format_begin(node, ctx, registry)
129
+ }
130
+ }
131
+
132
+ impl FormatRule for RescueRule {
133
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
134
+ format_rescue(node, ctx, registry, 0)
135
+ }
136
+ }
137
+
138
+ impl FormatRule for EnsureRule {
139
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
140
+ format_ensure(node, ctx, registry, 0)
141
+ }
142
+ }
143
+
144
+ /// Formats begin block
145
+ fn format_begin(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
146
+ // Check if this is an explicit begin block by looking at source
147
+ let is_explicit_begin = ctx
148
+ .extract_source(node)
149
+ .map(|s| s.trim_start().starts_with("begin"))
150
+ .unwrap_or(false);
151
+
152
+ if is_explicit_begin {
153
+ format_explicit_begin(node, ctx, registry)
154
+ } else {
155
+ // Implicit begin - emit children directly
156
+ format_implicit_begin(node, ctx, registry)
157
+ }
158
+ }
159
+
160
+ /// Formats explicit begin...end block
161
+ fn format_explicit_begin(
162
+ node: &Node,
163
+ ctx: &mut FormatContext,
164
+ registry: &RuleRegistry,
165
+ ) -> Result<Doc> {
166
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
167
+
168
+ // Leading comments
169
+ let leading = format_leading_comments(ctx, node.location.start_line);
170
+ if !leading.is_empty() {
171
+ docs.push(leading);
172
+ }
173
+
174
+ docs.push(text("begin"));
175
+
176
+ let mut body_children: Vec<&Node> = Vec::new();
177
+ let mut clause_children: Vec<&Node> = Vec::new();
178
+ for child in &node.children {
179
+ match child.node_type {
180
+ NodeType::RescueNode | NodeType::EnsureNode | NodeType::ElseNode => {
181
+ clause_children.push(child);
182
+ }
183
+ _ => body_children.push(child),
184
+ }
185
+ }
186
+
187
+ if !body_children.is_empty() {
188
+ let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2 + 1);
189
+ body_docs.push(hardline());
190
+ for (i, child) in body_children.iter().enumerate() {
191
+ if i > 0 {
192
+ body_docs.push(hardline());
193
+ }
194
+ body_docs.push(format_child(child, ctx, registry)?);
195
+ }
196
+ docs.push(indent(concat(body_docs)));
197
+ }
198
+
199
+ for clause in &clause_children {
200
+ docs.push(hardline());
201
+ docs.push(format_begin_clause(clause, ctx, registry)?);
202
+ }
203
+
204
+ docs.push(hardline());
205
+ docs.push(text("end"));
206
+
207
+ // Trailing comment on end line
208
+ let trailing = format_trailing_comment(ctx, node.location.end_line);
209
+ if !trailing.is_empty() {
210
+ docs.push(trailing);
211
+ }
212
+
213
+ Ok(concat(docs))
214
+ }
215
+
216
+ /// Formats implicit begin (children only, no begin/end keywords)
217
+ fn format_implicit_begin(
218
+ node: &Node,
219
+ ctx: &mut FormatContext,
220
+ registry: &RuleRegistry,
221
+ ) -> Result<Doc> {
222
+ let mut docs: Vec<Doc> = Vec::with_capacity(node.children.len() * 2);
223
+
224
+ for (i, child) in node.children.iter().enumerate() {
225
+ if i > 0 {
226
+ docs.push(hardline());
227
+ }
228
+ let child_doc = format_child(child, ctx, registry)?;
229
+ docs.push(child_doc);
230
+ }
231
+
232
+ Ok(concat(docs))
233
+ }
234
+
235
+ /// Formats rescue clause
236
+ fn format_rescue(
237
+ node: &Node,
238
+ ctx: &mut FormatContext,
239
+ registry: &RuleRegistry,
240
+ dedent_level: usize,
241
+ ) -> Result<Doc> {
242
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
243
+
244
+ // For rescue within a method, dedent by 1 level
245
+ // But since we're in Doc IR, we handle this at the caller level
246
+ let _ = dedent_level;
247
+
248
+ // Emit any standalone comments that sit immediately before the rescue
249
+ // keyword. Without this they would be picked up as leading comments of
250
+ // the first statement inside the rescue body, which visually moves an
251
+ // annotation like `# retry on flaky errors` *into* the rescue branch.
252
+ let leading = format_leading_comments(ctx, node.location.start_line);
253
+ if !leading.is_empty() {
254
+ docs.push(leading);
255
+ }
256
+
257
+ docs.push(text("rescue"));
258
+
259
+ // Extract exception classes and variable from source
260
+ if let Some(source_text) = ctx.extract_source(node) {
261
+ // Find the rescue declaration part (first line only, unless trailing comma/backslash)
262
+ let mut rescue_decl = String::new();
263
+ let mut expect_continuation = false;
264
+
265
+ for line in source_text.lines() {
266
+ let trimmed = line.trim();
267
+
268
+ if rescue_decl.is_empty() {
269
+ // First line - remove "rescue" prefix
270
+ let after_rescue = trimmed.trim_start_matches("rescue").trim();
271
+ if !after_rescue.is_empty() {
272
+ // Check if line ends with continuation marker
273
+ expect_continuation =
274
+ after_rescue.ends_with(',') || after_rescue.ends_with('\\');
275
+ rescue_decl.push_str(after_rescue.trim_end_matches('\\').trim());
276
+ }
277
+ if !expect_continuation {
278
+ break;
279
+ }
280
+ } else if expect_continuation {
281
+ // Continuation line after trailing comma or backslash
282
+ if !rescue_decl.ends_with(' ') {
283
+ rescue_decl.push(' ');
284
+ }
285
+ let content = trimmed.trim_end_matches('\\').trim();
286
+ rescue_decl.push_str(content);
287
+ expect_continuation = trimmed.ends_with(',') || trimmed.ends_with('\\');
288
+ if !expect_continuation {
289
+ break;
290
+ }
291
+ } else {
292
+ break;
293
+ }
294
+ }
295
+
296
+ if !rescue_decl.is_empty() {
297
+ docs.push(text(" "));
298
+ docs.push(text(rescue_decl));
299
+ }
300
+ }
301
+
302
+ // Emit rescue body (indented under the rescue keyword) and subsequent
303
+ // chained rescue clauses (at the same indent level as this rescue).
304
+ //
305
+ // The hardline lives INSIDE the `indent(...)` wrap so that the body's
306
+ // first statement lands at `caller_indent + indent_width`, matching the
307
+ // indentation applied by hardlines later inside `format_statements`.
308
+ let mut body_stmts: Option<&Node> = None;
309
+ let mut subsequent: Option<&Node> = None;
310
+ for child in &node.children {
311
+ match &child.node_type {
312
+ NodeType::StatementsNode => body_stmts = Some(child),
313
+ NodeType::RescueNode => subsequent = Some(child),
314
+ _ => {}
315
+ }
316
+ }
317
+
318
+ if let Some(stmts) = body_stmts {
319
+ let body_doc = format_statements(stmts, ctx, registry)?;
320
+ docs.push(indent(concat(vec![hardline(), body_doc])));
321
+ }
322
+
323
+ if let Some(sub) = subsequent {
324
+ docs.push(hardline());
325
+ docs.push(format_rescue(sub, ctx, registry, dedent_level)?);
326
+ }
327
+
328
+ Ok(concat(docs))
329
+ }
330
+
331
+ /// Formats ensure clause
332
+ fn format_ensure(
333
+ node: &Node,
334
+ ctx: &mut FormatContext,
335
+ registry: &RuleRegistry,
336
+ dedent_level: usize,
337
+ ) -> Result<Doc> {
338
+ let mut docs: Vec<Doc> = Vec::with_capacity(6);
339
+ let _ = dedent_level;
340
+
341
+ // Leading comments
342
+ let leading = format_leading_comments(ctx, node.location.start_line);
343
+ if !leading.is_empty() {
344
+ docs.push(leading);
345
+ }
346
+
347
+ docs.push(text("ensure"));
348
+
349
+ // Emit ensure body. The hardline lives INSIDE the `indent(...)` wrap so
350
+ // that every body statement — including the first one — lands at
351
+ // `caller_indent + indent_width`.
352
+ for child in &node.children {
353
+ match &child.node_type {
354
+ NodeType::StatementsNode => {
355
+ let body_doc = format_statements(child, ctx, registry)?;
356
+ docs.push(indent(concat(vec![hardline(), body_doc])));
357
+ }
358
+ _ => {
359
+ let child_doc = format_child(child, ctx, registry)?;
360
+ docs.push(indent(concat(vec![hardline(), child_doc])));
361
+ }
362
+ }
363
+ }
364
+
365
+ Ok(concat(docs))
366
+ }
367
+
368
+ #[cfg(test)]
369
+ mod tests {
370
+ use super::*;
371
+ use crate::ast::{FormattingInfo, Location};
372
+ use crate::config::Config;
373
+ use crate::doc::Printer;
374
+ use std::collections::HashMap;
375
+
376
+ fn make_begin_node(children: Vec<Node>, start_offset: usize, end_offset: usize) -> Node {
377
+ Node {
378
+ node_type: NodeType::BeginNode,
379
+ location: Location::new(1, 0, 5, 3, start_offset, end_offset),
380
+ children,
381
+ metadata: HashMap::new(),
382
+ comments: Vec::new(),
383
+ formatting: FormattingInfo::default(),
384
+ }
385
+ }
386
+
387
+ #[test]
388
+ fn test_explicit_begin() {
389
+ let config = Config::default();
390
+ let source = "begin\n puts 'hello'\nrescue\n puts 'error'\nend";
391
+ let mut ctx = FormatContext::new(&config, source);
392
+ let registry = RuleRegistry::default_registry();
393
+
394
+ let statements = Node {
395
+ node_type: NodeType::StatementsNode,
396
+ location: Location::new(2, 2, 2, 14, 8, 20),
397
+ children: Vec::new(),
398
+ metadata: HashMap::new(),
399
+ comments: Vec::new(),
400
+ formatting: FormattingInfo::default(),
401
+ };
402
+
403
+ let rescue_body = Node {
404
+ node_type: NodeType::StatementsNode,
405
+ location: Location::new(4, 2, 4, 14, 30, 42),
406
+ children: Vec::new(),
407
+ metadata: HashMap::new(),
408
+ comments: Vec::new(),
409
+ formatting: FormattingInfo::default(),
410
+ };
411
+
412
+ let rescue = Node {
413
+ node_type: NodeType::RescueNode,
414
+ location: Location::new(3, 0, 4, 14, 21, 42),
415
+ children: vec![rescue_body],
416
+ metadata: HashMap::new(),
417
+ comments: Vec::new(),
418
+ formatting: FormattingInfo::default(),
419
+ };
420
+
421
+ let node = make_begin_node(vec![statements, rescue], 0, 46);
422
+ ctx.collect_comments(&node);
423
+
424
+ let rule = BeginRule;
425
+ let doc = rule.format(&node, &mut ctx, &registry).unwrap();
426
+
427
+ let mut printer = Printer::new(&config);
428
+ let result = printer.print(&doc);
429
+
430
+ assert!(result.contains("begin"));
431
+ assert!(result.contains("rescue"));
432
+ assert!(result.contains("end"));
433
+ }
434
+ }
@@ -0,0 +1,233 @@
1
+ //! Shared helpers for body-with-end constructs
2
+ //!
3
+ //! This module provides common formatting logic for Ruby constructs
4
+ //! that have a header, optional body, and `end` keyword (class, module, def).
5
+
6
+ use crate::ast::Node;
7
+ use crate::doc::{concat, hardline, indent, text, Doc};
8
+ use crate::error::Result;
9
+ use crate::format::context::FormatContext;
10
+ use crate::format::registry::RuleRegistry;
11
+ use crate::format::rule::{
12
+ format_child, format_comments_before_end, format_leading_comments, format_trailing_comment,
13
+ is_structural_node, mark_comments_in_range_emitted,
14
+ };
15
+
16
+ use super::begin::{format_implicit_begin_body, is_implicit_begin_with_clauses};
17
+
18
+ /// Configuration for formatting a body-with-end construct.
19
+ pub struct BodyEndConfig<'a> {
20
+ /// The keyword (e.g., "class", "module", "def")
21
+ pub keyword: &'static str,
22
+ /// The node being formatted
23
+ pub node: &'a Node,
24
+ /// Function to build the header after the keyword
25
+ pub header_builder: Box<dyn Fn(&'a Node) -> Vec<Doc> + 'a>,
26
+ /// Optional filter for which children are considered structural (skipped in body)
27
+ pub skip_same_line_children: bool,
28
+ }
29
+
30
+ /// Formats a body-with-end construct (class, module, def).
31
+ ///
32
+ /// This handles the common pattern of:
33
+ /// 1. Leading comments
34
+ /// 2. Header line (keyword + name + optional extras)
35
+ /// 3. Trailing comment on header line
36
+ /// 4. Indented body (skipping structural nodes)
37
+ /// 5. Comments before end
38
+ /// 6. End keyword
39
+ /// 7. Trailing comment on end line
40
+ pub fn format_body_end(
41
+ ctx: &mut FormatContext,
42
+ registry: &RuleRegistry,
43
+ config: BodyEndConfig,
44
+ ) -> Result<Doc> {
45
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
46
+
47
+ let start_line = config.node.location.start_line;
48
+ let end_line = config.node.location.end_line;
49
+
50
+ // 1. Leading comments before definition
51
+ let leading = format_leading_comments(ctx, start_line);
52
+ if !leading.is_empty() {
53
+ docs.push(leading);
54
+ }
55
+
56
+ // Single-line form: `def foo = expr` (endless), `def foo; body; end`,
57
+ // `class Foo; end`, etc. Emit the source verbatim instead of forcing
58
+ // a multi-line `def ... end` layout. This preserves Ruby 3+ endless
59
+ // methods and the `Error < StandardError; end` exception-hierarchy
60
+ // idiom that is pervasive in Rails code.
61
+ if start_line == end_line {
62
+ if let Some(source_text) = ctx.extract_source(config.node) {
63
+ docs.push(text(source_text.to_string()));
64
+ mark_comments_in_range_emitted(ctx, start_line, end_line);
65
+
66
+ let trailing = format_trailing_comment(ctx, end_line);
67
+ if !trailing.is_empty() {
68
+ docs.push(trailing);
69
+ }
70
+ return Ok(concat(docs));
71
+ }
72
+ }
73
+
74
+ // Body children (filtered) — reused by both the multi-line header path
75
+ // below and the normal path further down.
76
+ let body_children: Vec<&Node> = config
77
+ .node
78
+ .children
79
+ .iter()
80
+ .filter(|c| {
81
+ if config.skip_same_line_children && c.location.start_line == start_line {
82
+ return false;
83
+ }
84
+ !is_structural_node(c)
85
+ })
86
+ .collect();
87
+
88
+ // Multi-line header form: `def foo(a, # c1\n b, # c2\n c) # c3`.
89
+ //
90
+ // Rebuilding the header from `parameters_text` + a reconstructed `)`
91
+ // drops the closing line's trailing comment and, worse, lets comments
92
+ // that the params slice already contains be re-picked up by the later
93
+ // `format_trailing_comment` + `format_leading_comments` calls. The net
94
+ // effect is that the first inline comment is duplicated, the rest are
95
+ // moved into the body, and the file stops being idempotent because the
96
+ // relocated comments keep accumulating every format pass. Detect this
97
+ // case early and emit the header verbatim from source.
98
+ let params_text = config.node.metadata.get("parameters_text");
99
+ let header_is_multiline = params_text.is_some_and(|t| t.contains('\n'));
100
+ if header_is_multiline {
101
+ let header_end_line = body_children
102
+ .first()
103
+ .map(|c| c.location.start_line.saturating_sub(1))
104
+ .unwrap_or(start_line);
105
+ if header_end_line >= start_line {
106
+ let header_end_offset = line_end_offset(ctx.source(), header_end_line);
107
+ if let Some(header_src) = ctx
108
+ .source()
109
+ .get(config.node.location.start_offset..header_end_offset)
110
+ {
111
+ docs.push(text(header_src.to_string()));
112
+ mark_comments_in_range_emitted(ctx, start_line, header_end_line + 1);
113
+
114
+ // Body + "end" (same as the normal path; no header trailing
115
+ // comment — it's already baked into the source slice above).
116
+ push_body_and_end(
117
+ &mut docs,
118
+ ctx,
119
+ registry,
120
+ &body_children,
121
+ start_line,
122
+ end_line,
123
+ )?;
124
+ return Ok(concat(docs));
125
+ }
126
+ }
127
+ }
128
+
129
+ // 2. Build header: "keyword ..."
130
+ let mut header_parts: Vec<Doc> = vec![text(config.keyword), text(" ")];
131
+ header_parts.extend((config.header_builder)(config.node));
132
+ docs.push(concat(header_parts));
133
+
134
+ // 3. Trailing comment on definition line
135
+ let trailing = format_trailing_comment(ctx, start_line);
136
+ if !trailing.is_empty() {
137
+ docs.push(trailing);
138
+ }
139
+
140
+ // 4. Body (children), skipping structural nodes — already collected above.
141
+
142
+ // Special case: body is an implicit BeginNode carrying rescue/else/ensure.
143
+ // In that case the clause keywords must align with the opener, not with
144
+ // the body statements — so we split the body and clause emission instead
145
+ // of wrapping everything in a single `indent(...)`.
146
+ if body_children.len() == 1 && is_implicit_begin_with_clauses(body_children[0], ctx) {
147
+ docs.push(format_implicit_begin_body(body_children[0], ctx, registry)?);
148
+ } else if !body_children.is_empty() {
149
+ let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2);
150
+ for child in &body_children {
151
+ let child_doc = format_child(child, ctx, registry)?;
152
+ body_docs.push(hardline());
153
+ body_docs.push(child_doc);
154
+ }
155
+ docs.push(indent(concat(body_docs)));
156
+ }
157
+
158
+ // 5. Comments before end
159
+ let comments_before_end = format_comments_before_end(ctx, start_line, end_line);
160
+ if !comments_before_end.is_empty() {
161
+ docs.push(indent(comments_before_end));
162
+ }
163
+
164
+ // 6. Add newline before end
165
+ docs.push(hardline());
166
+
167
+ // 7. End keyword
168
+ docs.push(text("end"));
169
+
170
+ // 8. Trailing comment on end line
171
+ let end_trailing = format_trailing_comment(ctx, end_line);
172
+ if !end_trailing.is_empty() {
173
+ docs.push(end_trailing);
174
+ }
175
+
176
+ Ok(concat(docs))
177
+ }
178
+
179
+ /// Returns the byte offset of the character immediately past the end of the
180
+ /// given 1-based line (i.e. the index of the trailing `\n`, or `source.len()`
181
+ /// if the line has no trailing newline).
182
+ fn line_end_offset(source: &str, line: usize) -> usize {
183
+ let mut current = 1;
184
+ for (i, b) in source.bytes().enumerate() {
185
+ if b == b'\n' {
186
+ if current == line {
187
+ return i;
188
+ }
189
+ current += 1;
190
+ }
191
+ }
192
+ source.len()
193
+ }
194
+
195
+ /// Emits the body (possibly an implicit BeginNode with rescue clauses) and the
196
+ /// trailing `end` keyword. Extracted so that both the standard
197
+ /// header-reconstruction path and the multi-line source-extraction path can
198
+ /// share the emission logic.
199
+ fn push_body_and_end(
200
+ docs: &mut Vec<Doc>,
201
+ ctx: &mut FormatContext,
202
+ registry: &RuleRegistry,
203
+ body_children: &[&Node],
204
+ start_line: usize,
205
+ end_line: usize,
206
+ ) -> Result<()> {
207
+ if body_children.len() == 1 && is_implicit_begin_with_clauses(body_children[0], ctx) {
208
+ docs.push(format_implicit_begin_body(body_children[0], ctx, registry)?);
209
+ } else if !body_children.is_empty() {
210
+ let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2);
211
+ for child in body_children {
212
+ let child_doc = format_child(child, ctx, registry)?;
213
+ body_docs.push(hardline());
214
+ body_docs.push(child_doc);
215
+ }
216
+ docs.push(indent(concat(body_docs)));
217
+ }
218
+
219
+ let comments_before_end = format_comments_before_end(ctx, start_line, end_line);
220
+ if !comments_before_end.is_empty() {
221
+ docs.push(indent(comments_before_end));
222
+ }
223
+
224
+ docs.push(hardline());
225
+ docs.push(text("end"));
226
+
227
+ let end_trailing = format_trailing_comment(ctx, end_line);
228
+ if !end_trailing.is_empty() {
229
+ docs.push(end_trailing);
230
+ }
231
+
232
+ Ok(())
233
+ }