rfmt 1.5.3 → 1.6.0

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,295 @@
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
+ /// Rule for formatting begin/rescue/ensure blocks.
17
+ pub struct BeginRule;
18
+
19
+ /// Rule for formatting rescue clauses.
20
+ pub struct RescueRule;
21
+
22
+ /// Rule for formatting ensure clauses.
23
+ pub struct EnsureRule;
24
+
25
+ impl FormatRule for BeginRule {
26
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
27
+ format_begin(node, ctx, registry)
28
+ }
29
+ }
30
+
31
+ impl FormatRule for RescueRule {
32
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
33
+ format_rescue(node, ctx, registry, 0)
34
+ }
35
+ }
36
+
37
+ impl FormatRule for EnsureRule {
38
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
39
+ format_ensure(node, ctx, registry, 0)
40
+ }
41
+ }
42
+
43
+ /// Formats begin block
44
+ fn format_begin(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
45
+ // Check if this is an explicit begin block by looking at source
46
+ let is_explicit_begin = ctx
47
+ .extract_source(node)
48
+ .map(|s| s.trim_start().starts_with("begin"))
49
+ .unwrap_or(false);
50
+
51
+ if is_explicit_begin {
52
+ format_explicit_begin(node, ctx, registry)
53
+ } else {
54
+ // Implicit begin - emit children directly
55
+ format_implicit_begin(node, ctx, registry)
56
+ }
57
+ }
58
+
59
+ /// Formats explicit begin...end block
60
+ fn format_explicit_begin(
61
+ node: &Node,
62
+ ctx: &mut FormatContext,
63
+ registry: &RuleRegistry,
64
+ ) -> Result<Doc> {
65
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
66
+
67
+ // Leading comments
68
+ let leading = format_leading_comments(ctx, node.location.start_line);
69
+ if !leading.is_empty() {
70
+ docs.push(leading);
71
+ }
72
+
73
+ docs.push(text("begin"));
74
+ docs.push(hardline());
75
+
76
+ for child in &node.children {
77
+ let child_doc = format_child(child, ctx, registry)?;
78
+ docs.push(child_doc);
79
+ docs.push(hardline());
80
+ }
81
+
82
+ docs.push(text("end"));
83
+
84
+ // Trailing comment on end line
85
+ let trailing = format_trailing_comment(ctx, node.location.end_line);
86
+ if !trailing.is_empty() {
87
+ docs.push(trailing);
88
+ }
89
+
90
+ Ok(concat(docs))
91
+ }
92
+
93
+ /// Formats implicit begin (children only, no begin/end keywords)
94
+ fn format_implicit_begin(
95
+ node: &Node,
96
+ ctx: &mut FormatContext,
97
+ registry: &RuleRegistry,
98
+ ) -> Result<Doc> {
99
+ let mut docs: Vec<Doc> = Vec::with_capacity(node.children.len() * 2);
100
+
101
+ for (i, child) in node.children.iter().enumerate() {
102
+ if i > 0 {
103
+ docs.push(hardline());
104
+ }
105
+ let child_doc = format_child(child, ctx, registry)?;
106
+ docs.push(child_doc);
107
+ }
108
+
109
+ Ok(concat(docs))
110
+ }
111
+
112
+ /// Formats rescue clause
113
+ fn format_rescue(
114
+ node: &Node,
115
+ ctx: &mut FormatContext,
116
+ registry: &RuleRegistry,
117
+ dedent_level: usize,
118
+ ) -> Result<Doc> {
119
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
120
+
121
+ // For rescue within a method, dedent by 1 level
122
+ // But since we're in Doc IR, we handle this at the caller level
123
+ let _ = dedent_level;
124
+
125
+ docs.push(text("rescue"));
126
+
127
+ // Extract exception classes and variable from source
128
+ if let Some(source_text) = ctx.extract_source(node) {
129
+ // Find the rescue declaration part (first line only, unless trailing comma/backslash)
130
+ let mut rescue_decl = String::new();
131
+ let mut expect_continuation = false;
132
+
133
+ for line in source_text.lines() {
134
+ let trimmed = line.trim();
135
+
136
+ if rescue_decl.is_empty() {
137
+ // First line - remove "rescue" prefix
138
+ let after_rescue = trimmed.trim_start_matches("rescue").trim();
139
+ if !after_rescue.is_empty() {
140
+ // Check if line ends with continuation marker
141
+ expect_continuation =
142
+ after_rescue.ends_with(',') || after_rescue.ends_with('\\');
143
+ rescue_decl.push_str(after_rescue.trim_end_matches('\\').trim());
144
+ }
145
+ if !expect_continuation {
146
+ break;
147
+ }
148
+ } else if expect_continuation {
149
+ // Continuation line after trailing comma or backslash
150
+ if !rescue_decl.ends_with(' ') {
151
+ rescue_decl.push(' ');
152
+ }
153
+ let content = trimmed.trim_end_matches('\\').trim();
154
+ rescue_decl.push_str(content);
155
+ expect_continuation = trimmed.ends_with(',') || trimmed.ends_with('\\');
156
+ if !expect_continuation {
157
+ break;
158
+ }
159
+ } else {
160
+ break;
161
+ }
162
+ }
163
+
164
+ if !rescue_decl.is_empty() {
165
+ docs.push(text(" "));
166
+ docs.push(text(rescue_decl));
167
+ }
168
+ }
169
+
170
+ docs.push(hardline());
171
+
172
+ // Emit rescue body and handle subsequent rescue nodes
173
+ for child in &node.children {
174
+ match &child.node_type {
175
+ NodeType::StatementsNode => {
176
+ let body_doc = format_statements(child, ctx, registry)?;
177
+ docs.push(indent(body_doc));
178
+ }
179
+ NodeType::RescueNode => {
180
+ // Emit subsequent rescue clause
181
+ let rescue_doc = format_rescue(child, ctx, registry, dedent_level)?;
182
+ docs.push(rescue_doc);
183
+ }
184
+ _ => {
185
+ // Skip exception classes and variable (already handled above)
186
+ }
187
+ }
188
+ }
189
+
190
+ Ok(concat(docs))
191
+ }
192
+
193
+ /// Formats ensure clause
194
+ fn format_ensure(
195
+ node: &Node,
196
+ ctx: &mut FormatContext,
197
+ registry: &RuleRegistry,
198
+ dedent_level: usize,
199
+ ) -> Result<Doc> {
200
+ let mut docs: Vec<Doc> = Vec::with_capacity(6);
201
+ let _ = dedent_level;
202
+
203
+ // Leading comments
204
+ let leading = format_leading_comments(ctx, node.location.start_line);
205
+ if !leading.is_empty() {
206
+ docs.push(leading);
207
+ }
208
+
209
+ docs.push(text("ensure"));
210
+ docs.push(hardline());
211
+
212
+ // Emit ensure body statements
213
+ for child in &node.children {
214
+ match &child.node_type {
215
+ NodeType::StatementsNode => {
216
+ let body_doc = format_statements(child, ctx, registry)?;
217
+ docs.push(indent(body_doc));
218
+ }
219
+ _ => {
220
+ let child_doc = format_child(child, ctx, registry)?;
221
+ docs.push(indent(child_doc));
222
+ }
223
+ }
224
+ }
225
+
226
+ Ok(concat(docs))
227
+ }
228
+
229
+ #[cfg(test)]
230
+ mod tests {
231
+ use super::*;
232
+ use crate::ast::{FormattingInfo, Location};
233
+ use crate::config::Config;
234
+ use crate::doc::Printer;
235
+ use std::collections::HashMap;
236
+
237
+ fn make_begin_node(children: Vec<Node>, start_offset: usize, end_offset: usize) -> Node {
238
+ Node {
239
+ node_type: NodeType::BeginNode,
240
+ location: Location::new(1, 0, 5, 3, start_offset, end_offset),
241
+ children,
242
+ metadata: HashMap::new(),
243
+ comments: Vec::new(),
244
+ formatting: FormattingInfo::default(),
245
+ }
246
+ }
247
+
248
+ #[test]
249
+ fn test_explicit_begin() {
250
+ let config = Config::default();
251
+ let source = "begin\n puts 'hello'\nrescue\n puts 'error'\nend";
252
+ let mut ctx = FormatContext::new(&config, source);
253
+ let registry = RuleRegistry::default_registry();
254
+
255
+ let statements = Node {
256
+ node_type: NodeType::StatementsNode,
257
+ location: Location::new(2, 2, 2, 14, 8, 20),
258
+ children: Vec::new(),
259
+ metadata: HashMap::new(),
260
+ comments: Vec::new(),
261
+ formatting: FormattingInfo::default(),
262
+ };
263
+
264
+ let rescue_body = Node {
265
+ node_type: NodeType::StatementsNode,
266
+ location: Location::new(4, 2, 4, 14, 30, 42),
267
+ children: Vec::new(),
268
+ metadata: HashMap::new(),
269
+ comments: Vec::new(),
270
+ formatting: FormattingInfo::default(),
271
+ };
272
+
273
+ let rescue = Node {
274
+ node_type: NodeType::RescueNode,
275
+ location: Location::new(3, 0, 4, 14, 21, 42),
276
+ children: vec![rescue_body],
277
+ metadata: HashMap::new(),
278
+ comments: Vec::new(),
279
+ formatting: FormattingInfo::default(),
280
+ };
281
+
282
+ let node = make_begin_node(vec![statements, rescue], 0, 46);
283
+ ctx.collect_comments(&node);
284
+
285
+ let rule = BeginRule;
286
+ let doc = rule.format(&node, &mut ctx, &registry).unwrap();
287
+
288
+ let mut printer = Printer::new(&config);
289
+ let result = printer.print(&doc);
290
+
291
+ assert!(result.contains("begin"));
292
+ assert!(result.contains("rescue"));
293
+ assert!(result.contains("end"));
294
+ }
295
+ }
@@ -0,0 +1,109 @@
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,
14
+ };
15
+
16
+ /// Configuration for formatting a body-with-end construct.
17
+ pub struct BodyEndConfig<'a> {
18
+ /// The keyword (e.g., "class", "module", "def")
19
+ pub keyword: &'static str,
20
+ /// The node being formatted
21
+ pub node: &'a Node,
22
+ /// Function to build the header after the keyword
23
+ pub header_builder: Box<dyn Fn(&'a Node) -> Vec<Doc> + 'a>,
24
+ /// Optional filter for which children are considered structural (skipped in body)
25
+ pub skip_same_line_children: bool,
26
+ }
27
+
28
+ /// Formats a body-with-end construct (class, module, def).
29
+ ///
30
+ /// This handles the common pattern of:
31
+ /// 1. Leading comments
32
+ /// 2. Header line (keyword + name + optional extras)
33
+ /// 3. Trailing comment on header line
34
+ /// 4. Indented body (skipping structural nodes)
35
+ /// 5. Comments before end
36
+ /// 6. End keyword
37
+ /// 7. Trailing comment on end line
38
+ pub fn format_body_end(
39
+ ctx: &mut FormatContext,
40
+ registry: &RuleRegistry,
41
+ config: BodyEndConfig,
42
+ ) -> Result<Doc> {
43
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
44
+
45
+ let start_line = config.node.location.start_line;
46
+ let end_line = config.node.location.end_line;
47
+
48
+ // 1. Leading comments before definition
49
+ let leading = format_leading_comments(ctx, start_line);
50
+ if !leading.is_empty() {
51
+ docs.push(leading);
52
+ }
53
+
54
+ // 2. Build header: "keyword ..."
55
+ let mut header_parts: Vec<Doc> = vec![text(config.keyword), text(" ")];
56
+ header_parts.extend((config.header_builder)(config.node));
57
+ docs.push(concat(header_parts));
58
+
59
+ // 3. Trailing comment on definition line
60
+ let trailing = format_trailing_comment(ctx, start_line);
61
+ if !trailing.is_empty() {
62
+ docs.push(trailing);
63
+ }
64
+
65
+ // 4. Body (children), skipping structural nodes
66
+ let mut body_docs: Vec<Doc> = Vec::new();
67
+ let mut has_body_content = false;
68
+
69
+ for child in &config.node.children {
70
+ // Skip nodes on the same line as definition (name, parameters, etc.)
71
+ if config.skip_same_line_children && child.location.start_line == start_line {
72
+ continue;
73
+ }
74
+ if is_structural_node(child) {
75
+ continue;
76
+ }
77
+
78
+ has_body_content = true;
79
+
80
+ // Format the child node using recursive rule dispatch
81
+ let child_doc = format_child(child, ctx, registry)?;
82
+ body_docs.push(hardline());
83
+ body_docs.push(child_doc);
84
+ }
85
+
86
+ if has_body_content {
87
+ docs.push(indent(concat(body_docs)));
88
+ }
89
+
90
+ // 5. Comments before end
91
+ let comments_before_end = format_comments_before_end(ctx, start_line, end_line);
92
+ if !comments_before_end.is_empty() {
93
+ docs.push(indent(comments_before_end));
94
+ }
95
+
96
+ // 6. Add newline before end
97
+ docs.push(hardline());
98
+
99
+ // 7. End keyword
100
+ docs.push(text("end"));
101
+
102
+ // 8. Trailing comment on end line
103
+ let end_trailing = format_trailing_comment(ctx, end_line);
104
+ if !end_trailing.is_empty() {
105
+ docs.push(end_trailing);
106
+ }
107
+
108
+ Ok(concat(docs))
109
+ }