rfmt 1.5.2 → 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 +36 -0
- data/Cargo.lock +266 -92
- 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 -1760
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
//! IfRule and UnlessRule - Formats Ruby if/unless conditionals
|
|
2
|
+
//!
|
|
3
|
+
//! Handles:
|
|
4
|
+
//! - Normal if/unless: `if cond ... end`
|
|
5
|
+
//! - Postfix if/unless: `expr if cond`
|
|
6
|
+
//! - Ternary operator: `cond ? then_expr : else_expr`
|
|
7
|
+
//! - Inline then: `if cond then expr end`
|
|
8
|
+
//! - elsif/else chains
|
|
9
|
+
|
|
10
|
+
use crate::ast::{Node, NodeType};
|
|
11
|
+
use crate::doc::{concat, hardline, indent, text, Doc};
|
|
12
|
+
use crate::error::Result;
|
|
13
|
+
use crate::format::context::FormatContext;
|
|
14
|
+
use crate::format::registry::RuleRegistry;
|
|
15
|
+
use crate::format::rule::{
|
|
16
|
+
format_leading_comments, format_statements, format_trailing_comment, FormatRule,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/// Rule for formatting if conditionals.
|
|
20
|
+
pub struct IfRule;
|
|
21
|
+
|
|
22
|
+
/// Rule for formatting unless conditionals.
|
|
23
|
+
pub struct UnlessRule;
|
|
24
|
+
|
|
25
|
+
impl FormatRule for IfRule {
|
|
26
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
27
|
+
format_if_unless(node, ctx, registry, "if", false)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl FormatRule for UnlessRule {
|
|
32
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
33
|
+
format_if_unless(node, ctx, registry, "unless", false)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Formats if/unless/elsif/else constructs.
|
|
38
|
+
///
|
|
39
|
+
/// # Arguments
|
|
40
|
+
/// * `node` - The if/unless node
|
|
41
|
+
/// * `ctx` - The formatting context
|
|
42
|
+
/// * `registry` - The rule registry for recursive formatting
|
|
43
|
+
/// * `keyword` - "if" or "unless"
|
|
44
|
+
/// * `is_elsif` - true if this is an elsif clause (don't emit 'end')
|
|
45
|
+
fn format_if_unless(
|
|
46
|
+
node: &Node,
|
|
47
|
+
ctx: &mut FormatContext,
|
|
48
|
+
registry: &RuleRegistry,
|
|
49
|
+
keyword: &str,
|
|
50
|
+
is_elsif: bool,
|
|
51
|
+
) -> Result<Doc> {
|
|
52
|
+
// Check if this is a postfix if (modifier form)
|
|
53
|
+
let is_postfix = if let (Some(predicate), Some(statements)) =
|
|
54
|
+
(node.children.first(), node.children.get(1))
|
|
55
|
+
{
|
|
56
|
+
statements.location.start_offset < predicate.location.start_offset
|
|
57
|
+
} else {
|
|
58
|
+
false
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Postfix if/unless: "statement if/unless condition"
|
|
62
|
+
if is_postfix && !is_elsif {
|
|
63
|
+
return format_postfix(node, ctx, registry, keyword);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for ternary operator
|
|
67
|
+
let is_ternary = node
|
|
68
|
+
.metadata
|
|
69
|
+
.get("is_ternary")
|
|
70
|
+
.map(|v| v == "true")
|
|
71
|
+
.unwrap_or(false);
|
|
72
|
+
|
|
73
|
+
if is_ternary && !is_elsif {
|
|
74
|
+
return format_ternary(node, ctx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for inline then style: "if true then 1 end"
|
|
78
|
+
let is_single_line = node.location.start_line == node.location.end_line;
|
|
79
|
+
let is_inline_then = !is_elsif && is_single_line && node.children.get(2).is_none();
|
|
80
|
+
|
|
81
|
+
if is_inline_then {
|
|
82
|
+
return format_inline_then(node, ctx, keyword);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Normal if/unless/elsif
|
|
86
|
+
format_normal(node, ctx, registry, keyword, is_elsif)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Formats postfix if/unless: `statement if/unless condition`
|
|
90
|
+
fn format_postfix(
|
|
91
|
+
node: &Node,
|
|
92
|
+
ctx: &mut FormatContext,
|
|
93
|
+
_registry: &RuleRegistry,
|
|
94
|
+
keyword: &str,
|
|
95
|
+
) -> Result<Doc> {
|
|
96
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(6);
|
|
97
|
+
|
|
98
|
+
// Leading comments
|
|
99
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
100
|
+
if !leading.is_empty() {
|
|
101
|
+
docs.push(leading);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Emit statement
|
|
105
|
+
if let Some(statements) = node.children.get(1) {
|
|
106
|
+
if let Some(source_text) = ctx.extract_source(statements) {
|
|
107
|
+
docs.push(text(source_text.trim()));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
docs.push(text(" "));
|
|
112
|
+
docs.push(text(keyword));
|
|
113
|
+
docs.push(text(" "));
|
|
114
|
+
|
|
115
|
+
// Emit condition
|
|
116
|
+
if let Some(predicate) = node.children.first() {
|
|
117
|
+
if let Some(source_text) = ctx.extract_source(predicate) {
|
|
118
|
+
docs.push(text(source_text));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Trailing comment
|
|
123
|
+
let trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
124
|
+
if !trailing.is_empty() {
|
|
125
|
+
docs.push(trailing);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Ok(concat(docs))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Formats ternary operator: `cond ? then_expr : else_expr`
|
|
132
|
+
fn format_ternary(node: &Node, ctx: &mut FormatContext) -> Result<Doc> {
|
|
133
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(8);
|
|
134
|
+
|
|
135
|
+
// Leading comments
|
|
136
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
137
|
+
if !leading.is_empty() {
|
|
138
|
+
docs.push(leading);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Emit condition
|
|
142
|
+
if let Some(predicate) = node.children.first() {
|
|
143
|
+
if let Some(source_text) = ctx.extract_source(predicate) {
|
|
144
|
+
docs.push(text(source_text));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
docs.push(text(" ? "));
|
|
149
|
+
|
|
150
|
+
// Emit then expression
|
|
151
|
+
if let Some(statements) = node.children.get(1) {
|
|
152
|
+
if let Some(source_text) = ctx.extract_source(statements) {
|
|
153
|
+
docs.push(text(source_text.trim()));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
docs.push(text(" : "));
|
|
158
|
+
|
|
159
|
+
// Emit else expression
|
|
160
|
+
if let Some(else_node) = node.children.get(2) {
|
|
161
|
+
if let Some(else_statements) = else_node.children.first() {
|
|
162
|
+
if let Some(source_text) = ctx.extract_source(else_statements) {
|
|
163
|
+
docs.push(text(source_text.trim()));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Trailing comment
|
|
169
|
+
let trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
170
|
+
if !trailing.is_empty() {
|
|
171
|
+
docs.push(trailing);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Ok(concat(docs))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Formats inline then style: `if cond then expr end`
|
|
178
|
+
fn format_inline_then(node: &Node, ctx: &mut FormatContext, keyword: &str) -> Result<Doc> {
|
|
179
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(8);
|
|
180
|
+
|
|
181
|
+
// Leading comments
|
|
182
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
183
|
+
if !leading.is_empty() {
|
|
184
|
+
docs.push(leading);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
docs.push(text(keyword));
|
|
188
|
+
docs.push(text(" "));
|
|
189
|
+
|
|
190
|
+
// Emit condition
|
|
191
|
+
if let Some(predicate) = node.children.first() {
|
|
192
|
+
if let Some(source_text) = ctx.extract_source(predicate) {
|
|
193
|
+
docs.push(text(source_text));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
docs.push(text(" then "));
|
|
198
|
+
|
|
199
|
+
// Emit statement
|
|
200
|
+
if let Some(statements) = node.children.get(1) {
|
|
201
|
+
if let Some(source_text) = ctx.extract_source(statements) {
|
|
202
|
+
docs.push(text(source_text.trim()));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
docs.push(text(" end"));
|
|
207
|
+
|
|
208
|
+
// Trailing comment
|
|
209
|
+
let trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
210
|
+
if !trailing.is_empty() {
|
|
211
|
+
docs.push(trailing);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Ok(concat(docs))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Formats normal if/unless/elsif with potential else
|
|
218
|
+
fn format_normal(
|
|
219
|
+
node: &Node,
|
|
220
|
+
ctx: &mut FormatContext,
|
|
221
|
+
registry: &RuleRegistry,
|
|
222
|
+
keyword: &str,
|
|
223
|
+
is_elsif: bool,
|
|
224
|
+
) -> Result<Doc> {
|
|
225
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(12);
|
|
226
|
+
|
|
227
|
+
// Leading comments (only for outermost if/unless)
|
|
228
|
+
if !is_elsif {
|
|
229
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
230
|
+
if !leading.is_empty() {
|
|
231
|
+
docs.push(leading);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Emit 'if'/'unless' or 'elsif' keyword
|
|
236
|
+
if is_elsif {
|
|
237
|
+
docs.push(text("elsif "));
|
|
238
|
+
} else {
|
|
239
|
+
docs.push(text(keyword));
|
|
240
|
+
docs.push(text(" "));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Emit predicate (condition)
|
|
244
|
+
if let Some(predicate) = node.children.first() {
|
|
245
|
+
if let Some(source_text) = ctx.extract_source(predicate) {
|
|
246
|
+
docs.push(text(source_text));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Trailing comment on same line as if/unless/elsif
|
|
251
|
+
let trailing = format_trailing_comment(ctx, node.location.start_line);
|
|
252
|
+
if !trailing.is_empty() {
|
|
253
|
+
docs.push(trailing);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Emit then clause (second child is StatementsNode)
|
|
257
|
+
if let Some(statements) = node.children.get(1) {
|
|
258
|
+
if matches!(statements.node_type, NodeType::StatementsNode) {
|
|
259
|
+
let body_doc = format_statements(statements, ctx, registry)?;
|
|
260
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check for elsif/else (third child)
|
|
265
|
+
if let Some(consequent) = node.children.get(2) {
|
|
266
|
+
match &consequent.node_type {
|
|
267
|
+
NodeType::IfNode => {
|
|
268
|
+
// This is an elsif clause
|
|
269
|
+
docs.push(hardline());
|
|
270
|
+
let elsif_doc = format_if_unless(consequent, ctx, registry, "if", true)?;
|
|
271
|
+
docs.push(elsif_doc);
|
|
272
|
+
}
|
|
273
|
+
NodeType::ElseNode => {
|
|
274
|
+
// This is an else clause
|
|
275
|
+
docs.push(hardline());
|
|
276
|
+
docs.push(text("else"));
|
|
277
|
+
|
|
278
|
+
// Emit else body (first child of ElseNode)
|
|
279
|
+
if let Some(else_statements) = consequent.children.first() {
|
|
280
|
+
if matches!(else_statements.node_type, NodeType::StatementsNode) {
|
|
281
|
+
let body_doc = format_statements(else_statements, ctx, registry)?;
|
|
282
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
_ => {}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Only emit 'end' for the outermost if (not for elsif)
|
|
291
|
+
if !is_elsif {
|
|
292
|
+
docs.push(hardline());
|
|
293
|
+
docs.push(text("end"));
|
|
294
|
+
|
|
295
|
+
// Trailing comment on end line
|
|
296
|
+
let end_trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
297
|
+
if !end_trailing.is_empty() {
|
|
298
|
+
docs.push(end_trailing);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
Ok(concat(docs))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#[cfg(test)]
|
|
306
|
+
mod tests {
|
|
307
|
+
use super::*;
|
|
308
|
+
use crate::ast::{FormattingInfo, Location};
|
|
309
|
+
use crate::config::Config;
|
|
310
|
+
use crate::doc::Printer;
|
|
311
|
+
use std::collections::HashMap;
|
|
312
|
+
|
|
313
|
+
fn make_if_node(
|
|
314
|
+
children: Vec<Node>,
|
|
315
|
+
metadata: HashMap<String, String>,
|
|
316
|
+
start_line: usize,
|
|
317
|
+
end_line: usize,
|
|
318
|
+
start_offset: usize,
|
|
319
|
+
end_offset: usize,
|
|
320
|
+
) -> Node {
|
|
321
|
+
Node {
|
|
322
|
+
node_type: NodeType::IfNode,
|
|
323
|
+
location: Location::new(start_line, 0, end_line, 0, start_offset, end_offset),
|
|
324
|
+
children,
|
|
325
|
+
metadata,
|
|
326
|
+
comments: Vec::new(),
|
|
327
|
+
formatting: FormattingInfo::default(),
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fn make_predicate_node(start_offset: usize, end_offset: usize, start_line: usize) -> Node {
|
|
332
|
+
Node {
|
|
333
|
+
node_type: NodeType::CallNode,
|
|
334
|
+
location: Location::new(start_line, 0, start_line, 0, start_offset, end_offset),
|
|
335
|
+
children: Vec::new(),
|
|
336
|
+
metadata: HashMap::new(),
|
|
337
|
+
comments: Vec::new(),
|
|
338
|
+
formatting: FormattingInfo::default(),
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fn make_statements_node(
|
|
343
|
+
start_offset: usize,
|
|
344
|
+
end_offset: usize,
|
|
345
|
+
start_line: usize,
|
|
346
|
+
end_line: usize,
|
|
347
|
+
) -> Node {
|
|
348
|
+
Node {
|
|
349
|
+
node_type: NodeType::StatementsNode,
|
|
350
|
+
location: Location::new(start_line, 0, end_line, 0, start_offset, end_offset),
|
|
351
|
+
children: Vec::new(),
|
|
352
|
+
metadata: HashMap::new(),
|
|
353
|
+
comments: Vec::new(),
|
|
354
|
+
formatting: FormattingInfo::default(),
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#[test]
|
|
359
|
+
fn test_simple_if() {
|
|
360
|
+
let config = Config::default();
|
|
361
|
+
let source = "if true\n puts 'yes'\nend";
|
|
362
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
363
|
+
let registry = RuleRegistry::default_registry();
|
|
364
|
+
|
|
365
|
+
// predicate: "true" at offset 3-7
|
|
366
|
+
let predicate = make_predicate_node(3, 7, 1);
|
|
367
|
+
// statements: "puts 'yes'" at offset 10-20
|
|
368
|
+
let statements = make_statements_node(10, 20, 2, 2);
|
|
369
|
+
|
|
370
|
+
let node = make_if_node(vec![predicate, statements], HashMap::new(), 1, 3, 0, 24);
|
|
371
|
+
ctx.collect_comments(&node);
|
|
372
|
+
|
|
373
|
+
let rule = IfRule;
|
|
374
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
375
|
+
|
|
376
|
+
let mut printer = Printer::new(&config);
|
|
377
|
+
let result = printer.print(&doc);
|
|
378
|
+
|
|
379
|
+
assert!(result.contains("if true"));
|
|
380
|
+
assert!(result.contains("end"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#[test]
|
|
384
|
+
fn test_postfix_if() {
|
|
385
|
+
let config = Config::default();
|
|
386
|
+
let source = "puts 'yes' if true";
|
|
387
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
388
|
+
let registry = RuleRegistry::default_registry();
|
|
389
|
+
|
|
390
|
+
// For postfix: statements come before predicate in source
|
|
391
|
+
// predicate: "true" at offset 14-18
|
|
392
|
+
let predicate = make_predicate_node(14, 18, 1);
|
|
393
|
+
// statements: "puts 'yes'" at offset 0-10
|
|
394
|
+
let statements = make_statements_node(0, 10, 1, 1);
|
|
395
|
+
|
|
396
|
+
let node = make_if_node(vec![predicate, statements], HashMap::new(), 1, 1, 0, 18);
|
|
397
|
+
ctx.collect_comments(&node);
|
|
398
|
+
|
|
399
|
+
let rule = IfRule;
|
|
400
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
401
|
+
|
|
402
|
+
let mut printer = Printer::new(&config);
|
|
403
|
+
let result = printer.print(&doc);
|
|
404
|
+
|
|
405
|
+
assert!(result.contains("puts 'yes' if true"));
|
|
406
|
+
}
|
|
407
|
+
}
|