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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -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 +684 -0
- data/ext/rfmt/src/format/context.rs +448 -0
- data/ext/rfmt/src/format/formatter.rs +226 -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 +555 -0
- data/ext/rfmt/src/format/rules/begin.rs +295 -0
- data/ext/rfmt/src/format/rules/body_end.rs +109 -0
- data/ext/rfmt/src/format/rules/call.rs +409 -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 +116 -0
- data/ext/rfmt/src/format/rules/if_unless.rs +407 -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 +296 -0
- data/ext/rfmt/src/lib.rs +8 -5
- data/ext/rfmt/src/parser/prism_adapter.rs +157 -2
- 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,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, ®istry).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
|
+
}
|