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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -0
- data/Cargo.lock +1 -1
- data/README.md +22 -18
- data/ext/rfmt/Cargo.toml +4 -1
- data/ext/rfmt/src/doc/builders.rs +528 -0
- data/ext/rfmt/src/doc/mod.rs +220 -0
- data/ext/rfmt/src/doc/printer.rs +721 -0
- data/ext/rfmt/src/format/context.rs +448 -0
- data/ext/rfmt/src/format/formatter.rs +250 -0
- data/ext/rfmt/src/format/mod.rs +35 -0
- data/ext/rfmt/src/format/registry.rs +195 -0
- data/ext/rfmt/src/format/rule.rs +726 -0
- data/ext/rfmt/src/format/rules/begin.rs +434 -0
- data/ext/rfmt/src/format/rules/body_end.rs +233 -0
- data/ext/rfmt/src/format/rules/call.rs +448 -0
- data/ext/rfmt/src/format/rules/case.rs +359 -0
- data/ext/rfmt/src/format/rules/class.rs +160 -0
- data/ext/rfmt/src/format/rules/def.rs +216 -0
- data/ext/rfmt/src/format/rules/fallback.rs +130 -0
- data/ext/rfmt/src/format/rules/if_unless.rs +454 -0
- data/ext/rfmt/src/format/rules/loops.rs +325 -0
- data/ext/rfmt/src/format/rules/mod.rs +31 -0
- data/ext/rfmt/src/format/rules/module.rs +150 -0
- data/ext/rfmt/src/format/rules/singleton_class.rs +202 -0
- data/ext/rfmt/src/format/rules/statements.rs +122 -0
- data/ext/rfmt/src/format/rules/variable_write.rs +314 -0
- data/ext/rfmt/src/lib.rs +8 -5
- data/ext/rfmt/src/parser/prism_adapter.rs +157 -2
- data/lib/rfmt/prism_bridge.rb +43 -12
- data/lib/rfmt/version.rb +1 -1
- data/lib/ruby_lsp/rfmt/formatter_runner.rb +2 -0
- metadata +23 -2
- data/ext/rfmt/src/emitter/mod.rs +0 -1844
|
@@ -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, ®istry).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
|
+
}
|