rfmt 1.5.3 → 1.6.1

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