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,409 @@
1
+ //! CallRule - Formats Ruby method calls
2
+ //!
3
+ //! Handles:
4
+ //! - Simple calls: `foo.bar`
5
+ //! - Calls with blocks: `foo.bar do ... end` or `foo.bar { ... }`
6
+ //! - Method chains: `foo.bar.baz`
7
+
8
+ use crate::ast::{Node, NodeType};
9
+ use crate::doc::{concat, hardline, indent, text, Doc};
10
+ use crate::error::Result;
11
+ use crate::format::context::FormatContext;
12
+ use crate::format::registry::RuleRegistry;
13
+ use crate::format::rule::{
14
+ format_child, format_leading_comments, format_statements, format_trailing_comment,
15
+ mark_comments_in_range_emitted, reformat_chain_lines, FormatRule,
16
+ };
17
+
18
+ /// Rule for formatting method calls.
19
+ pub struct CallRule;
20
+
21
+ /// Rule for formatting blocks (do...end and {...}).
22
+ pub struct BlockRule;
23
+
24
+ /// Rule for formatting lambdas.
25
+ pub struct LambdaRule;
26
+
27
+ /// Block style for Ruby blocks
28
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29
+ enum BlockStyle {
30
+ DoEnd, // do ... end
31
+ Braces, // { ... }
32
+ }
33
+
34
+ impl FormatRule for CallRule {
35
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
36
+ format_call(node, ctx, registry)
37
+ }
38
+ }
39
+
40
+ impl FormatRule for BlockRule {
41
+ fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
42
+ // Detect block style and format accordingly
43
+ let block_style = detect_block_style(node, ctx);
44
+ match block_style {
45
+ BlockStyle::DoEnd => format_do_end_block(node, ctx, registry),
46
+ BlockStyle::Braces => format_brace_block(node, ctx, registry),
47
+ }
48
+ }
49
+ }
50
+
51
+ impl FormatRule for LambdaRule {
52
+ fn format(
53
+ &self,
54
+ node: &Node,
55
+ ctx: &mut FormatContext,
56
+ _registry: &RuleRegistry,
57
+ ) -> Result<Doc> {
58
+ // Lambda syntax is complex (-> vs lambda, {} vs do-end)
59
+ // Use source extraction to preserve original style
60
+ let mut docs: Vec<Doc> = Vec::with_capacity(3);
61
+
62
+ // Leading comments
63
+ let leading = format_leading_comments(ctx, node.location.start_line);
64
+ if !leading.is_empty() {
65
+ docs.push(leading);
66
+ }
67
+
68
+ // Extract source
69
+ if let Some(source_text) = ctx.extract_source(node) {
70
+ docs.push(text(source_text));
71
+ }
72
+
73
+ // Mark internal comments as emitted
74
+ mark_comments_in_range_emitted(ctx, node.location.start_line, node.location.end_line);
75
+
76
+ // Trailing comment
77
+ let trailing = format_trailing_comment(ctx, node.location.end_line);
78
+ if !trailing.is_empty() {
79
+ docs.push(trailing);
80
+ }
81
+
82
+ Ok(concat(docs))
83
+ }
84
+ }
85
+
86
+ /// Formats method call
87
+ fn format_call(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
88
+ // Leading comments
89
+ let mut docs: Vec<Doc> = Vec::with_capacity(4);
90
+ let leading = format_leading_comments(ctx, node.location.start_line);
91
+ if !leading.is_empty() {
92
+ docs.push(leading);
93
+ }
94
+
95
+ // Check if this call has a block (last child is BlockNode)
96
+ let has_block = node
97
+ .children
98
+ .last()
99
+ .map(|c| matches!(c.node_type, NodeType::BlockNode))
100
+ .unwrap_or(false);
101
+
102
+ if !has_block {
103
+ // Simple call - use source extraction with chain reformatting
104
+ if let Some(source_text) = ctx.extract_source(node) {
105
+ let reformatted =
106
+ reformat_chain_lines(source_text, ctx.config().formatting.indent_width);
107
+ docs.push(text(reformatted));
108
+ }
109
+
110
+ // Mark comments in this range as emitted (they're in source extraction)
111
+ mark_comments_in_range_emitted(ctx, node.location.start_line, node.location.end_line);
112
+
113
+ // Trailing comment
114
+ let trailing = format_trailing_comment(ctx, node.location.end_line);
115
+ if !trailing.is_empty() {
116
+ docs.push(trailing);
117
+ }
118
+
119
+ return Ok(concat(docs));
120
+ }
121
+
122
+ // Has block - need to handle specially
123
+ let block_node = node.children.last().unwrap();
124
+ let block_style = detect_block_style(block_node, ctx);
125
+
126
+ // Emit the call part (receiver.method(args)) from source with chain reformatting
127
+ let call_end_offset = block_node.location.start_offset;
128
+ if let Some(call_text) = ctx
129
+ .source()
130
+ .get(node.location.start_offset..call_end_offset)
131
+ {
132
+ let reformatted =
133
+ reformat_chain_lines(call_text.trim_end(), ctx.config().formatting.indent_width);
134
+ docs.push(text(reformatted));
135
+ }
136
+
137
+ // Mark comments in the call part (before block) as emitted
138
+ // This includes trailing comments that are part of the extracted source
139
+ mark_comments_in_range_emitted(
140
+ ctx,
141
+ node.location.start_line,
142
+ block_node.location.start_line,
143
+ );
144
+
145
+ // Format the block
146
+ match block_style {
147
+ BlockStyle::DoEnd => {
148
+ let block_doc = format_do_end_block(block_node, ctx, registry)?;
149
+ docs.push(block_doc);
150
+ }
151
+ BlockStyle::Braces => {
152
+ let block_doc = format_brace_block(block_node, ctx, registry)?;
153
+ docs.push(block_doc);
154
+ }
155
+ }
156
+
157
+ Ok(concat(docs))
158
+ }
159
+
160
+ /// Detect whether block uses do...end or { } style
161
+ fn detect_block_style(block_node: &Node, ctx: &FormatContext) -> BlockStyle {
162
+ if let Some(first_char) = ctx
163
+ .source()
164
+ .get(block_node.location.start_offset..block_node.location.start_offset + 1)
165
+ {
166
+ if first_char == "{" {
167
+ return BlockStyle::Braces;
168
+ }
169
+ }
170
+ BlockStyle::DoEnd
171
+ }
172
+
173
+ /// Formats do...end style block
174
+ fn format_do_end_block(
175
+ block_node: &Node,
176
+ ctx: &mut FormatContext,
177
+ registry: &RuleRegistry,
178
+ ) -> Result<Doc> {
179
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
180
+
181
+ docs.push(text(" do"));
182
+
183
+ // Emit block parameters if present (|x, y|)
184
+ if let Some(params) = extract_block_parameters(block_node, ctx) {
185
+ docs.push(text(" "));
186
+ docs.push(text(params));
187
+ }
188
+
189
+ // Trailing comment on same line as do |...|
190
+ let trailing = format_trailing_comment(ctx, block_node.location.start_line);
191
+ if !trailing.is_empty() {
192
+ docs.push(trailing);
193
+ }
194
+
195
+ // Find and emit the body (StatementsNode or BeginNode among children)
196
+ for child in &block_node.children {
197
+ match &child.node_type {
198
+ NodeType::StatementsNode => {
199
+ let body_doc = format_statements(child, ctx, registry)?;
200
+ docs.push(indent(concat(vec![hardline(), body_doc])));
201
+ break;
202
+ }
203
+ NodeType::BeginNode => {
204
+ // Block with rescue/ensure/else
205
+ let body_doc = format_child(child, ctx, registry)?;
206
+ docs.push(indent(concat(vec![hardline(), body_doc])));
207
+ break;
208
+ }
209
+ _ => {
210
+ // Skip parameter nodes
211
+ }
212
+ }
213
+ }
214
+
215
+ // Emit 'end'
216
+ docs.push(hardline());
217
+ docs.push(text("end"));
218
+
219
+ // Trailing comment on end line
220
+ let end_trailing = format_trailing_comment(ctx, block_node.location.end_line);
221
+ if !end_trailing.is_empty() {
222
+ docs.push(end_trailing);
223
+ }
224
+
225
+ Ok(concat(docs))
226
+ }
227
+
228
+ /// Formats { } style block
229
+ fn format_brace_block(
230
+ block_node: &Node,
231
+ ctx: &mut FormatContext,
232
+ registry: &RuleRegistry,
233
+ ) -> Result<Doc> {
234
+ let is_multiline = block_node.location.start_line != block_node.location.end_line;
235
+
236
+ if is_multiline {
237
+ format_multiline_brace_block(block_node, ctx, registry)
238
+ } else {
239
+ format_inline_brace_block(block_node, ctx)
240
+ }
241
+ }
242
+
243
+ /// Formats multiline brace block
244
+ fn format_multiline_brace_block(
245
+ block_node: &Node,
246
+ ctx: &mut FormatContext,
247
+ registry: &RuleRegistry,
248
+ ) -> Result<Doc> {
249
+ let mut docs: Vec<Doc> = Vec::with_capacity(8);
250
+
251
+ docs.push(text(" {"));
252
+
253
+ // Emit block parameters if present
254
+ if let Some(params) = extract_block_parameters(block_node, ctx) {
255
+ docs.push(text(" "));
256
+ docs.push(text(params));
257
+ }
258
+
259
+ // Emit body
260
+ for child in &block_node.children {
261
+ if matches!(child.node_type, NodeType::StatementsNode) {
262
+ let body_doc = format_statements(child, ctx, registry)?;
263
+ docs.push(indent(concat(vec![hardline(), body_doc])));
264
+ break;
265
+ }
266
+ }
267
+
268
+ docs.push(hardline());
269
+ docs.push(text("}"));
270
+
271
+ // Trailing comment
272
+ let trailing = format_trailing_comment(ctx, block_node.location.end_line);
273
+ if !trailing.is_empty() {
274
+ docs.push(trailing);
275
+ }
276
+
277
+ Ok(concat(docs))
278
+ }
279
+
280
+ /// Formats inline brace block
281
+ fn format_inline_brace_block(block_node: &Node, ctx: &mut FormatContext) -> Result<Doc> {
282
+ let mut docs: Vec<Doc> = Vec::with_capacity(3);
283
+
284
+ docs.push(text(" "));
285
+
286
+ // Extract from source to preserve spacing
287
+ if let Some(source_text) = ctx.extract_source(block_node) {
288
+ docs.push(text(source_text));
289
+ }
290
+
291
+ // Mark internal comments as emitted
292
+ mark_comments_in_range_emitted(
293
+ ctx,
294
+ block_node.location.start_line,
295
+ block_node.location.end_line,
296
+ );
297
+
298
+ // Trailing comment
299
+ let trailing = format_trailing_comment(ctx, block_node.location.end_line);
300
+ if !trailing.is_empty() {
301
+ docs.push(trailing);
302
+ }
303
+
304
+ Ok(concat(docs))
305
+ }
306
+
307
+ /// Extract block parameters (|x, y|) from block node
308
+ fn extract_block_parameters(block_node: &Node, ctx: &FormatContext) -> Option<String> {
309
+ let source = ctx.source();
310
+ if source.is_empty() {
311
+ return None;
312
+ }
313
+
314
+ let block_source =
315
+ source.get(block_node.location.start_offset..block_node.location.end_offset)?;
316
+
317
+ // Only look at the first line of the block for parameters
318
+ let first_line = block_source.lines().next()?;
319
+
320
+ // Find |...| pattern in the first line only
321
+ let pipe_start = first_line.find('|')?;
322
+ let rest = &first_line[pipe_start + 1..];
323
+ let pipe_end = rest.find('|')?;
324
+
325
+ Some(first_line[pipe_start..=pipe_start + 1 + pipe_end].to_string())
326
+ }
327
+
328
+ #[cfg(test)]
329
+ mod tests {
330
+ use super::*;
331
+ use crate::ast::{FormattingInfo, Location};
332
+ use crate::config::Config;
333
+ use crate::doc::Printer;
334
+ use std::collections::HashMap;
335
+
336
+ fn make_call_node(
337
+ children: Vec<Node>,
338
+ start_offset: usize,
339
+ end_offset: usize,
340
+ start_line: usize,
341
+ end_line: usize,
342
+ ) -> Node {
343
+ Node {
344
+ node_type: NodeType::CallNode,
345
+ location: Location::new(start_line, 0, end_line, 0, start_offset, end_offset),
346
+ children,
347
+ metadata: HashMap::new(),
348
+ comments: Vec::new(),
349
+ formatting: FormattingInfo::default(),
350
+ }
351
+ }
352
+
353
+ #[test]
354
+ fn test_simple_call() {
355
+ let config = Config::default();
356
+ let source = "puts 'hello'";
357
+ let mut ctx = FormatContext::new(&config, source);
358
+ let registry = RuleRegistry::default_registry();
359
+
360
+ let node = make_call_node(Vec::new(), 0, 12, 1, 1);
361
+ ctx.collect_comments(&node);
362
+
363
+ let rule = CallRule;
364
+ let doc = rule.format(&node, &mut ctx, &registry).unwrap();
365
+
366
+ let mut printer = Printer::new(&config);
367
+ let result = printer.print(&doc);
368
+
369
+ assert_eq!(result.trim(), "puts 'hello'");
370
+ }
371
+
372
+ #[test]
373
+ fn test_call_with_do_block() {
374
+ let config = Config::default();
375
+ let source = "items.each do |item|\n puts item\nend";
376
+ let mut ctx = FormatContext::new(&config, source);
377
+ let registry = RuleRegistry::default_registry();
378
+
379
+ let block_body = Node {
380
+ node_type: NodeType::StatementsNode,
381
+ location: Location::new(2, 2, 2, 11, 23, 32),
382
+ children: Vec::new(),
383
+ metadata: HashMap::new(),
384
+ comments: Vec::new(),
385
+ formatting: FormattingInfo::default(),
386
+ };
387
+
388
+ let block = Node {
389
+ node_type: NodeType::BlockNode,
390
+ location: Location::new(1, 11, 3, 3, 11, 36),
391
+ children: vec![block_body],
392
+ metadata: HashMap::new(),
393
+ comments: Vec::new(),
394
+ formatting: FormattingInfo::default(),
395
+ };
396
+
397
+ let node = make_call_node(vec![block], 0, 36, 1, 3);
398
+ ctx.collect_comments(&node);
399
+
400
+ let rule = CallRule;
401
+ let doc = rule.format(&node, &mut ctx, &registry).unwrap();
402
+
403
+ let mut printer = Printer::new(&config);
404
+ let result = printer.print(&doc);
405
+
406
+ assert!(result.contains("items.each do"));
407
+ assert!(result.contains("end"));
408
+ }
409
+ }