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.
@@ -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, &registry).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, &registry).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
+ }