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.
@@ -1,1760 +0,0 @@
1
- use crate::ast::{Comment, Node, NodeType};
2
- use crate::config::{Config, IndentStyle};
3
- use crate::error::Result;
4
- use std::collections::{BTreeMap, HashSet};
5
- use std::fmt::Write;
6
-
7
- /// Block style for Ruby blocks
8
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
9
- enum BlockStyle {
10
- DoEnd, // do ... end
11
- Braces, // { ... }
12
- }
13
-
14
- /// Code emitter that converts AST back to Ruby source code
15
- pub struct Emitter {
16
- config: Config,
17
- source: String,
18
- buffer: String,
19
- all_comments: Vec<Comment>,
20
- emitted_comment_indices: HashSet<usize>,
21
- /// Cached indent strings by level (index = level, value = indent string)
22
- indent_cache: Vec<String>,
23
- /// Index of comment indices by start line for O(log n) lookup
24
- /// Key: start_line, Value: Vec of comment indices that start on that line
25
- comments_by_line: BTreeMap<usize, Vec<usize>>,
26
- }
27
-
28
- impl Emitter {
29
- pub fn new(config: Config) -> Self {
30
- Self {
31
- config,
32
- source: String::new(),
33
- buffer: String::new(),
34
- all_comments: Vec::new(),
35
- emitted_comment_indices: HashSet::new(),
36
- indent_cache: Vec::new(),
37
- comments_by_line: BTreeMap::new(),
38
- }
39
- }
40
-
41
- /// Create emitter with source code for fallback extraction
42
- pub fn with_source(config: Config, source: String) -> Self {
43
- Self {
44
- config,
45
- source,
46
- buffer: String::new(),
47
- all_comments: Vec::new(),
48
- emitted_comment_indices: HashSet::new(),
49
- indent_cache: Vec::new(),
50
- comments_by_line: BTreeMap::new(),
51
- }
52
- }
53
-
54
- /// Emit Ruby source code from an AST
55
- pub fn emit(&mut self, ast: &Node) -> Result<String> {
56
- self.buffer.clear();
57
- self.emitted_comment_indices.clear();
58
- self.comments_by_line.clear();
59
-
60
- self.collect_comments(ast);
61
- self.build_comment_index();
62
-
63
- self.emit_node(ast, 0)?;
64
-
65
- let last_code_line = Self::find_last_code_line(ast);
66
- self.emit_remaining_comments(last_code_line)?;
67
-
68
- if !self.buffer.ends_with('\n') {
69
- self.buffer.push('\n');
70
- }
71
-
72
- Ok(std::mem::take(&mut self.buffer))
73
- }
74
-
75
- /// Find the last line of code in the AST (excluding comments)
76
- fn find_last_code_line(ast: &Node) -> usize {
77
- let mut max_line = ast.location.end_line;
78
- let mut stack = vec![ast];
79
-
80
- while let Some(node) = stack.pop() {
81
- max_line = max_line.max(node.location.end_line);
82
- stack.extend(node.children.iter());
83
- }
84
-
85
- max_line
86
- }
87
-
88
- /// Emit all comments that haven't been emitted yet
89
- fn emit_remaining_comments(&mut self, last_code_line: usize) -> Result<()> {
90
- let mut last_end_line: Option<usize> = Some(last_code_line);
91
- let mut is_first_comment = true;
92
-
93
- for (idx, comment) in self.all_comments.iter().enumerate() {
94
- if self.emitted_comment_indices.contains(&idx) {
95
- continue;
96
- }
97
-
98
- // For the first remaining comment:
99
- // - If buffer is empty, don't add any leading newline
100
- // - If buffer has content, ensure we start on a new line
101
- if is_first_comment && self.buffer.is_empty() {
102
- // Don't add leading newline for first comment when buffer is empty
103
- } else if !self.buffer.ends_with('\n') {
104
- self.buffer.push('\n');
105
- }
106
-
107
- // Preserve blank lines between code/comments
108
- // But only if this is not the first comment in an empty buffer
109
- if !(is_first_comment && self.buffer.is_empty()) {
110
- if let Some(prev_line) = last_end_line {
111
- let gap = comment.location.start_line.saturating_sub(prev_line);
112
- for _ in 1..gap {
113
- self.buffer.push('\n');
114
- }
115
- }
116
- }
117
-
118
- writeln!(self.buffer, "{}", comment.text)?;
119
- self.emitted_comment_indices.insert(idx);
120
- last_end_line = Some(comment.location.end_line);
121
- is_first_comment = false;
122
- }
123
- Ok(())
124
- }
125
-
126
- /// Recursively collect all comments from the AST
127
- fn collect_comments(&mut self, node: &Node) {
128
- self.all_comments.extend(node.comments.clone());
129
- for child in &node.children {
130
- self.collect_comments(child);
131
- }
132
- }
133
-
134
- /// Build the comment index by start line for O(log n) range lookups
135
- fn build_comment_index(&mut self) {
136
- for (idx, comment) in self.all_comments.iter().enumerate() {
137
- self.comments_by_line
138
- .entry(comment.location.start_line)
139
- .or_default()
140
- .push(idx);
141
- }
142
- }
143
-
144
- /// Get comment indices in the given line range [start_line, end_line)
145
- /// Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
146
- fn get_comment_indices_in_range(&self, start_line: usize, end_line: usize) -> Vec<usize> {
147
- // Guard against invalid range (e.g., endless methods where start_line >= end_line)
148
- if start_line >= end_line {
149
- return Vec::new();
150
- }
151
- self.comments_by_line
152
- .range(start_line..end_line)
153
- .flat_map(|(_, indices)| indices.iter().copied())
154
- .filter(|&idx| !self.emitted_comment_indices.contains(&idx))
155
- .collect()
156
- }
157
-
158
- /// Get comment indices before a given line (exclusive)
159
- /// Uses BTreeMap range for O(log n) lookup
160
- fn get_comment_indices_before(&self, line: usize) -> Vec<usize> {
161
- self.comments_by_line
162
- .range(..line)
163
- .flat_map(|(_, indices)| indices.iter().copied())
164
- .filter(|&idx| {
165
- !self.emitted_comment_indices.contains(&idx)
166
- && self.all_comments[idx].location.end_line < line
167
- })
168
- .collect()
169
- }
170
-
171
- /// Get comment indices on a specific line (for trailing comments)
172
- /// Uses BTreeMap get for O(log n) lookup
173
- fn get_comment_indices_on_line(&self, line: usize) -> Vec<usize> {
174
- self.comments_by_line
175
- .get(&line)
176
- .map(|indices| {
177
- indices
178
- .iter()
179
- .copied()
180
- .filter(|&idx| !self.emitted_comment_indices.contains(&idx))
181
- .collect()
182
- })
183
- .unwrap_or_default()
184
- }
185
-
186
- /// Emit comments that appear before a given line
187
- /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
188
- fn emit_comments_before(&mut self, line: usize, indent_level: usize) -> Result<()> {
189
- self.ensure_indent_cache(indent_level);
190
-
191
- let indices = self.get_comment_indices_before(line);
192
-
193
- let mut comments_to_emit: Vec<_> = indices
194
- .into_iter()
195
- .map(|idx| {
196
- let comment = &self.all_comments[idx];
197
- (idx, comment.location.start_line, comment.location.end_line)
198
- })
199
- .collect();
200
-
201
- comments_to_emit.sort_by_key(|(_, start, _)| *start);
202
-
203
- let comments_count = comments_to_emit.len();
204
- let mut last_comment_end_line: Option<usize> = None;
205
-
206
- for (i, (idx, comment_start_line, comment_end_line)) in
207
- comments_to_emit.into_iter().enumerate()
208
- {
209
- if let Some(prev_end) = last_comment_end_line {
210
- let gap = comment_start_line.saturating_sub(prev_end);
211
- for _ in 1..gap {
212
- self.buffer.push('\n');
213
- }
214
- }
215
-
216
- writeln!(
217
- self.buffer,
218
- "{}{}",
219
- &self.indent_cache[indent_level], &self.all_comments[idx].text
220
- )?;
221
- self.emitted_comment_indices.insert(idx);
222
- last_comment_end_line = Some(comment_end_line);
223
-
224
- if i == comments_count - 1 && line > comment_end_line + 1 {
225
- self.buffer.push('\n');
226
- }
227
- }
228
-
229
- Ok(())
230
- }
231
-
232
- /// Check if there are any unemitted comments in the given line range
233
- /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
234
- fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
235
- // Guard against invalid range (e.g., endless methods where start_line >= end_line)
236
- if start_line >= end_line {
237
- return false;
238
- }
239
- self.comments_by_line
240
- .range(start_line..end_line)
241
- .flat_map(|(_, indices)| indices.iter())
242
- .any(|&idx| {
243
- !self.emitted_comment_indices.contains(&idx)
244
- && self.all_comments[idx].location.end_line < end_line
245
- })
246
- }
247
-
248
- /// Emit comments that appear immediately before the end statement while preserving their position
249
- /// This is crucial for maintaining semantic relationships between comments and the code they precede
250
- fn emit_comments_before_end(
251
- &mut self,
252
- construct_start_line: usize,
253
- construct_end_line: usize,
254
- indent_level: usize,
255
- ) -> Result<()> {
256
- self.ensure_indent_cache(indent_level);
257
-
258
- // Implement proper comment positioning logic
259
- // Only emit standalone comments that appear on their own lines
260
- // This prevents comments from being incorrectly attached to code statements
261
-
262
- // Find comments that are between the construct and the end line
263
- // Only emit comments that haven't been emitted yet AND are on their own lines
264
- let indices =
265
- self.get_comment_indices_in_range(construct_start_line + 1, construct_end_line);
266
-
267
- let mut comments_to_emit: Vec<_> = indices
268
- .into_iter()
269
- .filter(|&idx| {
270
- let comment = &self.all_comments[idx];
271
- // Only emit if: not already emitted, before end line, and is standalone
272
- !self.emitted_comment_indices.contains(&idx)
273
- && comment.location.end_line < construct_end_line
274
- && self.is_standalone_comment(comment)
275
- })
276
- .map(|idx| {
277
- let comment = &self.all_comments[idx];
278
- (idx, comment.location.start_line, comment.location.end_line)
279
- })
280
- .collect();
281
-
282
- if comments_to_emit.is_empty() {
283
- return Ok(());
284
- }
285
-
286
- comments_to_emit.sort_by_key(|(_, start, _)| *start);
287
-
288
- // Ensure newline before first comment if buffer doesn't end with one
289
- if !self.buffer.ends_with('\n') {
290
- self.buffer.push('\n');
291
- }
292
-
293
- let mut last_emitted_line: Option<usize> = None;
294
-
295
- // Emit comments while preserving their exact line positioning
296
- for (idx, comment_start_line, comment_end_line) in comments_to_emit {
297
- // Preserve blank lines between comments
298
- if let Some(prev_line) = last_emitted_line {
299
- let gap = comment_start_line.saturating_sub(prev_line);
300
- for _ in 1..gap {
301
- self.buffer.push('\n');
302
- }
303
- }
304
-
305
- writeln!(
306
- self.buffer,
307
- "{}{}",
308
- &self.indent_cache[indent_level], &self.all_comments[idx].text
309
- )?;
310
- self.emitted_comment_indices.insert(idx);
311
- last_emitted_line = Some(comment_end_line);
312
- }
313
-
314
- Ok(())
315
- }
316
-
317
- /// Check if a comment should be treated as standalone
318
- /// A standalone comment is one that should appear on its own line,
319
- /// not attached to the end of a code statement
320
- fn is_standalone_comment(&self, comment: &Comment) -> bool {
321
- let comment_line = comment.location.start_line;
322
- let _comment_start_offset = comment.location.start_offset;
323
-
324
- // Get the source lines to analyze the comment's position
325
- let lines: Vec<&str> = self.source.lines().collect();
326
-
327
- // Check if we have a valid line number (1-indexed to 0-indexed)
328
- if comment_line == 0 || comment_line > lines.len() {
329
- return false;
330
- }
331
-
332
- let line = lines[comment_line - 1]; // Convert to 0-indexed
333
-
334
- // Find where the comment starts within the line
335
- let comment_text = &comment.text;
336
-
337
- // Look for the comment marker (#) in the line
338
- if let Some(hash_pos) = line.find('#') {
339
- // Check if there's only whitespace before the comment
340
- let before_comment = &line[..hash_pos];
341
- let is_only_whitespace = before_comment.trim().is_empty();
342
-
343
- // Also verify this is actually our comment by checking the text matches
344
- let line_comment_text = &line[hash_pos..];
345
- let is_same_comment = line_comment_text.trim_end() == comment_text.trim_end();
346
-
347
- return is_only_whitespace && is_same_comment;
348
- }
349
-
350
- // If we can't find the comment marker, assume it's standalone
351
- // This is a fallback for edge cases
352
- false
353
- }
354
-
355
- /// Check if the node spans only a single line
356
- fn is_single_line(&self, node: &Node) -> bool {
357
- node.location.start_line == node.location.end_line
358
- }
359
-
360
- /// Extract and write source text for a node
361
- fn write_source_text(&mut self, node: &Node) -> Result<()> {
362
- let start = node.location.start_offset;
363
- let end = node.location.end_offset;
364
- if let Some(text) = self.source.get(start..end) {
365
- write!(self.buffer, "{}", text)?;
366
- }
367
- Ok(())
368
- }
369
-
370
- /// Extract and write trimmed source text for a node
371
- fn write_source_text_trimmed(&mut self, node: &Node) -> Result<()> {
372
- let start = node.location.start_offset;
373
- let end = node.location.end_offset;
374
- if let Some(text) = self.source.get(start..end) {
375
- write!(self.buffer, "{}", text.trim())?;
376
- }
377
- Ok(())
378
- }
379
-
380
- /// Emit comments that are within a given line range, preserving blank lines from prev_line
381
- /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
382
- fn emit_comments_in_range_with_prev_line(
383
- &mut self,
384
- start_line: usize,
385
- end_line: usize,
386
- indent_level: usize,
387
- prev_line: usize,
388
- ) -> Result<()> {
389
- self.ensure_indent_cache(indent_level);
390
-
391
- let indices = self.get_comment_indices_in_range(start_line, end_line);
392
-
393
- let mut comments_to_emit: Vec<_> = indices
394
- .into_iter()
395
- .filter(|&idx| self.all_comments[idx].location.end_line < end_line)
396
- .map(|idx| {
397
- let comment = &self.all_comments[idx];
398
- (idx, comment.location.start_line, comment.location.end_line)
399
- })
400
- .collect();
401
-
402
- comments_to_emit.sort_by_key(|(_, start, _)| *start);
403
-
404
- let mut last_end_line: usize = prev_line;
405
-
406
- for (idx, comment_start_line, comment_end_line) in comments_to_emit {
407
- // Preserve blank lines between previous content and this comment
408
- let gap = comment_start_line.saturating_sub(last_end_line);
409
- for _ in 1..gap {
410
- self.buffer.push('\n');
411
- }
412
-
413
- writeln!(
414
- self.buffer,
415
- "{}{}",
416
- &self.indent_cache[indent_level], &self.all_comments[idx].text
417
- )?;
418
- self.emitted_comment_indices.insert(idx);
419
- last_end_line = comment_end_line;
420
- }
421
-
422
- Ok(())
423
- }
424
-
425
- /// Emit comments that appear on the same line (trailing comments)
426
- /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
427
- fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
428
- // Use indexed lookup for O(log n) access
429
- let indices = self.get_comment_indices_on_line(line);
430
-
431
- // Collect indices only (no text clone needed)
432
- let indices_to_emit: Vec<usize> = indices;
433
-
434
- // Now emit the collected comments by accessing text at write time
435
- for idx in indices_to_emit {
436
- write!(self.buffer, " {}", &self.all_comments[idx].text)?;
437
- self.emitted_comment_indices.insert(idx);
438
- }
439
-
440
- Ok(())
441
- }
442
-
443
- /// Emit a node with given indentation level
444
- fn emit_node(&mut self, node: &Node, indent_level: usize) -> Result<()> {
445
- match &node.node_type {
446
- NodeType::ProgramNode => self.emit_program(node, indent_level)?,
447
- NodeType::StatementsNode => self.emit_statements(node, indent_level)?,
448
- NodeType::ClassNode => self.emit_class(node, indent_level)?,
449
- NodeType::ModuleNode => self.emit_module(node, indent_level)?,
450
- NodeType::DefNode => self.emit_method(node, indent_level)?,
451
- NodeType::IfNode => self.emit_if_unless(node, indent_level, false, "if")?,
452
- NodeType::UnlessNode => self.emit_if_unless(node, indent_level, false, "unless")?,
453
- NodeType::CallNode => self.emit_call(node, indent_level)?,
454
- NodeType::BeginNode => self.emit_begin(node, indent_level)?,
455
- NodeType::RescueNode => self.emit_rescue(node, indent_level)?,
456
- NodeType::EnsureNode => self.emit_ensure(node, indent_level)?,
457
- NodeType::LambdaNode => self.emit_lambda(node, indent_level)?,
458
- NodeType::CaseNode => self.emit_case(node, indent_level)?,
459
- NodeType::WhenNode => self.emit_when(node, indent_level)?,
460
- NodeType::WhileNode => self.emit_while_until(node, indent_level, "while")?,
461
- NodeType::UntilNode => self.emit_while_until(node, indent_level, "until")?,
462
- NodeType::ForNode => self.emit_for(node, indent_level)?,
463
- NodeType::SingletonClassNode => self.emit_singleton_class(node, indent_level)?,
464
- NodeType::CaseMatchNode => self.emit_case_match(node, indent_level)?,
465
- NodeType::InNode => self.emit_in(node, indent_level)?,
466
- NodeType::LocalVariableWriteNode | NodeType::InstanceVariableWriteNode => {
467
- self.emit_variable_write(node, indent_level)?
468
- }
469
- _ => self.emit_generic(node, indent_level)?,
470
- }
471
- Ok(())
472
- }
473
-
474
- /// Emit program node (root)
475
- fn emit_program(&mut self, node: &Node, indent_level: usize) -> Result<()> {
476
- for (i, child) in node.children.iter().enumerate() {
477
- self.emit_node(child, indent_level)?;
478
-
479
- // Add newlines between top-level statements, normalizing to max 1 blank line
480
- if i < node.children.len() - 1 {
481
- let current_end_line = child.location.end_line;
482
- let next_start_line = node.children[i + 1].location.start_line;
483
- let line_diff = next_start_line.saturating_sub(current_end_line);
484
-
485
- // Add 1 newline if consecutive, 2 newlines (1 blank line) if there was a gap
486
- let newlines = if line_diff > 1 { 2 } else { 1 };
487
- for _ in 0..newlines {
488
- self.buffer.push('\n');
489
- }
490
- }
491
- }
492
- Ok(())
493
- }
494
-
495
- /// Emit statements node (body of class/module/def)
496
- fn emit_statements(&mut self, node: &Node, indent_level: usize) -> Result<()> {
497
- for (i, child) in node.children.iter().enumerate() {
498
- self.emit_node(child, indent_level)?;
499
-
500
- if i < node.children.len() - 1 {
501
- let current_end_line = child.location.end_line;
502
- let next_child = &node.children[i + 1];
503
- let next_start_line = next_child.location.start_line;
504
-
505
- // Find the first comment between current and next node (if any)
506
- // Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
507
- let first_comment_line = self
508
- .comments_by_line
509
- .range((current_end_line + 1)..next_start_line)
510
- .next()
511
- .map(|(line, _)| *line);
512
-
513
- // Calculate line diff based on whether there's a comment
514
- let effective_next_line = first_comment_line.unwrap_or(next_start_line);
515
- let line_diff = effective_next_line.saturating_sub(current_end_line);
516
-
517
- let newlines = if line_diff > 1 { 2 } else { 1 };
518
-
519
- for _ in 0..newlines {
520
- self.buffer.push('\n');
521
- }
522
- }
523
- }
524
- Ok(())
525
- }
526
-
527
- /// Emit class definition
528
- fn emit_class(&mut self, node: &Node, indent_level: usize) -> Result<()> {
529
- // Emit any comments before this class
530
- self.emit_comments_before(node.location.start_line, indent_level)?;
531
-
532
- self.emit_indent(indent_level)?;
533
- write!(self.buffer, "class ")?;
534
-
535
- if let Some(name) = node.metadata.get("name") {
536
- write!(self.buffer, "{}", name)?;
537
- }
538
-
539
- if let Some(superclass) = node.metadata.get("superclass") {
540
- write!(self.buffer, " < {}", superclass)?;
541
- }
542
-
543
- // Emit trailing comments on the class definition line (e.g., # rubocop:disable)
544
- self.emit_trailing_comments(node.location.start_line)?;
545
- self.buffer.push('\n');
546
-
547
- // Emit body (children), but skip structural nodes (class name, superclass)
548
- // Use start_line check to properly handle CallNode superclasses like ActiveRecord::Migration[8.0]
549
- let class_start_line = node.location.start_line;
550
- let class_end_line = node.location.end_line;
551
- let mut has_body_content = false;
552
-
553
- for child in &node.children {
554
- // Skip nodes on the same line as class definition (name, superclass)
555
- if child.location.start_line == class_start_line {
556
- continue;
557
- }
558
- if self.is_structural_node(&child.node_type) {
559
- continue;
560
- }
561
- has_body_content = true;
562
- self.emit_node(child, indent_level + 1)?;
563
- }
564
-
565
- // Emit comments that appear before the end statement while preserving their position
566
- self.emit_comments_before_end(class_start_line, class_end_line, indent_level + 1)?;
567
-
568
- // Add newline before end if there was body content or internal comments
569
- if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
570
- && !self.buffer.ends_with('\n')
571
- {
572
- self.buffer.push('\n');
573
- }
574
-
575
- self.emit_indent(indent_level)?;
576
- write!(self.buffer, "end")?;
577
- // Emit trailing comments on end line (e.g., `end # rubocop:disable`)
578
- self.emit_trailing_comments(node.location.end_line)?;
579
-
580
- Ok(())
581
- }
582
-
583
- /// Emit module definition
584
- fn emit_module(&mut self, node: &Node, indent_level: usize) -> Result<()> {
585
- // Emit any comments before this module
586
- self.emit_comments_before(node.location.start_line, indent_level)?;
587
-
588
- self.emit_indent(indent_level)?;
589
- write!(self.buffer, "module ")?;
590
-
591
- if let Some(name) = node.metadata.get("name") {
592
- write!(self.buffer, "{}", name)?;
593
- }
594
-
595
- // Emit trailing comments on the module definition line
596
- self.emit_trailing_comments(node.location.start_line)?;
597
- self.buffer.push('\n');
598
-
599
- let module_start_line = node.location.start_line;
600
- let module_end_line = node.location.end_line;
601
- let mut has_body_content = false;
602
-
603
- // Emit body (children), but skip structural nodes
604
- for child in &node.children {
605
- if self.is_structural_node(&child.node_type) {
606
- continue;
607
- }
608
- has_body_content = true;
609
- self.emit_node(child, indent_level + 1)?;
610
- }
611
-
612
- // Emit comments that appear before the end statement while preserving their position
613
- self.emit_comments_before_end(module_start_line, module_end_line, indent_level + 1)?;
614
-
615
- // Add newline before end if there was body content or internal comments
616
- if (has_body_content || self.has_comments_in_range(module_start_line + 1, module_end_line))
617
- && !self.buffer.ends_with('\n')
618
- {
619
- self.buffer.push('\n');
620
- }
621
-
622
- self.emit_indent(indent_level)?;
623
- write!(self.buffer, "end")?;
624
- // Emit trailing comments on end line (e.g., `end # rubocop:disable`)
625
- self.emit_trailing_comments(node.location.end_line)?;
626
-
627
- Ok(())
628
- }
629
-
630
- /// Emit method definition
631
- fn emit_method(&mut self, node: &Node, indent_level: usize) -> Result<()> {
632
- // Emit any comments before this method
633
- self.emit_comments_before(node.location.start_line, indent_level)?;
634
-
635
- self.emit_indent(indent_level)?;
636
- write!(self.buffer, "def ")?;
637
-
638
- // Handle class methods (def self.method_name)
639
- if let Some(receiver) = node.metadata.get("receiver") {
640
- write!(self.buffer, "{}.", receiver)?;
641
- }
642
-
643
- if let Some(name) = node.metadata.get("name") {
644
- write!(self.buffer, "{}", name)?;
645
- }
646
-
647
- // Emit parameters using metadata from prism_bridge
648
- if let Some(params_text) = node.metadata.get("parameters_text") {
649
- let has_parens = node
650
- .metadata
651
- .get("has_parens")
652
- .map(|v| v == "true")
653
- .unwrap_or(false);
654
- if has_parens {
655
- write!(self.buffer, "({})", params_text)?;
656
- } else {
657
- write!(self.buffer, " {}", params_text)?;
658
- }
659
- }
660
-
661
- // Emit trailing comment on same line as def
662
- self.emit_trailing_comments(node.location.start_line)?;
663
- self.buffer.push('\n');
664
-
665
- // Emit body (children), but skip structural nodes like parameter nodes
666
- for child in &node.children {
667
- if self.is_structural_node(&child.node_type) {
668
- continue;
669
- }
670
- self.emit_node(child, indent_level + 1)?;
671
- }
672
-
673
- // Emit comments that appear before the end statement while preserving their position
674
- self.emit_comments_before_end(
675
- node.location.start_line,
676
- node.location.end_line,
677
- indent_level + 1,
678
- )?;
679
-
680
- // Add newline before end if there was body content
681
- if node
682
- .children
683
- .iter()
684
- .any(|c| !self.is_structural_node(&c.node_type))
685
- {
686
- self.buffer.push('\n');
687
- }
688
-
689
- self.emit_indent(indent_level)?;
690
- write!(self.buffer, "end")?;
691
- // Emit trailing comments on end line (e.g., `end # rubocop:disable`)
692
- self.emit_trailing_comments(node.location.end_line)?;
693
-
694
- Ok(())
695
- }
696
-
697
- /// Emit begin node
698
- /// BeginNode can be either:
699
- /// 1. Explicit begin...end block (source starts with "begin")
700
- /// 2. Implicit begin wrapping method body with rescue/ensure
701
- fn emit_begin(&mut self, node: &Node, indent_level: usize) -> Result<()> {
702
- // Check if this is an explicit begin block by looking at source
703
- let is_explicit_begin = if !self.source.is_empty() {
704
- self.source
705
- .get(node.location.start_offset..)
706
- .map(|s| s.trim_start().starts_with("begin"))
707
- .unwrap_or(false)
708
- } else {
709
- false
710
- };
711
-
712
- if is_explicit_begin {
713
- self.emit_comments_before(node.location.start_line, indent_level)?;
714
- self.emit_indent(indent_level)?;
715
- writeln!(self.buffer, "begin")?;
716
-
717
- for child in &node.children {
718
- self.emit_node(child, indent_level + 1)?;
719
- self.buffer.push('\n');
720
- }
721
-
722
- self.emit_indent(indent_level)?;
723
- write!(self.buffer, "end")?;
724
- // Emit trailing comments on end line
725
- self.emit_trailing_comments(node.location.end_line)?;
726
- } else {
727
- // Implicit begin - emit children directly
728
- for (i, child) in node.children.iter().enumerate() {
729
- if i > 0 {
730
- self.buffer.push('\n');
731
- }
732
- self.emit_node(child, indent_level)?;
733
- }
734
- }
735
- Ok(())
736
- }
737
-
738
- /// Emit rescue node
739
- fn emit_rescue(&mut self, node: &Node, indent_level: usize) -> Result<()> {
740
- // Rescue node structure:
741
- // - First children are exception class references (ConstantReadNode)
742
- // - Then exception variable (LocalVariableTargetNode)
743
- // - Last child is StatementsNode with the rescue body
744
-
745
- // Dedent by 1 level since rescue is at the same level as method body
746
- let rescue_indent = indent_level.saturating_sub(1);
747
- self.emit_indent(rescue_indent)?;
748
- write!(self.buffer, "rescue")?;
749
-
750
- // Extract exception classes and variable from source
751
- // Handle multi-line rescue clauses (e.g., multiple exception classes spanning lines)
752
- if !self.source.is_empty() && node.location.end_offset <= self.source.len() {
753
- if let Some(source_text) = self
754
- .source
755
- .get(node.location.start_offset..node.location.end_offset)
756
- {
757
- // Find the rescue declaration part (first line only, unless trailing comma/backslash)
758
- let mut rescue_decl = String::new();
759
- let mut expect_continuation = false;
760
-
761
- for line in source_text.lines() {
762
- let trimmed = line.trim();
763
-
764
- if rescue_decl.is_empty() {
765
- // First line - remove "rescue" prefix
766
- let after_rescue = trimmed.trim_start_matches("rescue").trim();
767
- if !after_rescue.is_empty() {
768
- // Check if line ends with continuation marker
769
- expect_continuation =
770
- after_rescue.ends_with(',') || after_rescue.ends_with('\\');
771
- rescue_decl.push_str(after_rescue.trim_end_matches('\\').trim());
772
- }
773
- if !expect_continuation {
774
- break;
775
- }
776
- } else if expect_continuation {
777
- // Continuation line after trailing comma or backslash
778
- // Add space after comma or if no trailing space
779
- if !rescue_decl.ends_with(' ') {
780
- rescue_decl.push(' ');
781
- }
782
- let content = trimmed.trim_end_matches('\\').trim();
783
- rescue_decl.push_str(content);
784
- expect_continuation = trimmed.ends_with(',') || trimmed.ends_with('\\');
785
- if !expect_continuation {
786
- break;
787
- }
788
- } else {
789
- break;
790
- }
791
- }
792
-
793
- if !rescue_decl.is_empty() {
794
- write!(self.buffer, " {}", rescue_decl)?;
795
- }
796
- }
797
- }
798
-
799
- self.buffer.push('\n');
800
-
801
- // Emit rescue body and handle subsequent rescue nodes
802
- // Children structure:
803
- // - ConstantReadNode/ConstantPathNode (exception classes)
804
- // - LocalVariableTargetNode (optional, exception variable)
805
- // - StatementsNode (rescue body)
806
- // - RescueNode (optional, subsequent rescue clause)
807
- for child in &node.children {
808
- match &child.node_type {
809
- NodeType::StatementsNode => {
810
- self.emit_node(child, indent_level)?;
811
- }
812
- NodeType::RescueNode => {
813
- // Emit subsequent rescue clause
814
- // Ensure newline before subsequent rescue
815
- if !self.buffer.ends_with('\n') {
816
- self.buffer.push('\n');
817
- }
818
- self.emit_rescue(child, indent_level)?;
819
- }
820
- _ => {
821
- // Skip exception classes and variable (already handled above)
822
- }
823
- }
824
- }
825
-
826
- Ok(())
827
- }
828
-
829
- /// Emit ensure node
830
- fn emit_ensure(&mut self, node: &Node, indent_level: usize) -> Result<()> {
831
- // ensure keyword should be at same level as begin/rescue
832
- let ensure_indent = indent_level.saturating_sub(1);
833
-
834
- self.emit_comments_before(node.location.start_line, ensure_indent)?;
835
- self.emit_indent(ensure_indent)?;
836
- writeln!(self.buffer, "ensure")?;
837
-
838
- // Emit ensure body statements
839
- for child in &node.children {
840
- match &child.node_type {
841
- NodeType::StatementsNode => {
842
- self.emit_statements(child, indent_level)?;
843
- }
844
- _ => {
845
- self.emit_node(child, indent_level)?;
846
- }
847
- }
848
- }
849
-
850
- Ok(())
851
- }
852
-
853
- /// Emit lambda node
854
- fn emit_lambda(&mut self, node: &Node, indent_level: usize) -> Result<()> {
855
- self.emit_comments_before(node.location.start_line, indent_level)?;
856
-
857
- // Lambda syntax is complex (-> vs lambda, {} vs do-end)
858
- // Use source extraction to preserve original style
859
- self.emit_generic_without_comments(node, indent_level)
860
- }
861
-
862
- /// Emit case node
863
- fn emit_case(&mut self, node: &Node, indent_level: usize) -> Result<()> {
864
- self.emit_comments_before(node.location.start_line, indent_level)?;
865
- self.emit_indent(indent_level)?;
866
-
867
- // Write "case" keyword
868
- write!(self.buffer, "case")?;
869
-
870
- // Find predicate (first child that isn't WhenNode or ElseNode)
871
- let mut when_start_idx = 0;
872
- if let Some(first_child) = node.children.first() {
873
- if !matches!(
874
- first_child.node_type,
875
- NodeType::WhenNode | NodeType::ElseNode
876
- ) {
877
- // This is the predicate - extract from source
878
- let start = first_child.location.start_offset;
879
- let end = first_child.location.end_offset;
880
- if let Some(text) = self.source.get(start..end) {
881
- write!(self.buffer, " {}", text)?;
882
- }
883
- when_start_idx = 1;
884
- }
885
- }
886
-
887
- self.buffer.push('\n');
888
-
889
- // Emit when clauses and else
890
- for child in node.children.iter().skip(when_start_idx) {
891
- match &child.node_type {
892
- NodeType::WhenNode => {
893
- self.emit_when(child, indent_level)?;
894
- self.buffer.push('\n');
895
- }
896
- NodeType::ElseNode => {
897
- self.emit_indent(indent_level)?;
898
- writeln!(self.buffer, "else")?;
899
- // Emit else body
900
- for else_child in &child.children {
901
- if matches!(else_child.node_type, NodeType::StatementsNode) {
902
- self.emit_statements(else_child, indent_level + 1)?;
903
- } else {
904
- self.emit_node(else_child, indent_level + 1)?;
905
- }
906
- }
907
- self.buffer.push('\n');
908
- }
909
- _ => {}
910
- }
911
- }
912
-
913
- // Emit "end" keyword
914
- self.emit_indent(indent_level)?;
915
- write!(self.buffer, "end")?;
916
- // Emit trailing comments on end line
917
- self.emit_trailing_comments(node.location.end_line)?;
918
-
919
- Ok(())
920
- }
921
-
922
- /// Emit when node
923
- fn emit_when(&mut self, node: &Node, indent_level: usize) -> Result<()> {
924
- self.emit_comments_before(node.location.start_line, indent_level)?;
925
- self.emit_indent(indent_level)?;
926
-
927
- write!(self.buffer, "when ")?;
928
-
929
- // Collect conditions (all children except StatementsNode)
930
- let conditions: Vec<_> = node
931
- .children
932
- .iter()
933
- .filter(|c| !matches!(c.node_type, NodeType::StatementsNode))
934
- .collect();
935
-
936
- // Emit conditions with comma separator
937
- for (i, cond) in conditions.iter().enumerate() {
938
- self.write_source_text(cond)?;
939
- if i < conditions.len() - 1 {
940
- write!(self.buffer, ", ")?;
941
- }
942
- }
943
-
944
- let statements = node
945
- .children
946
- .iter()
947
- .find(|c| matches!(c.node_type, NodeType::StatementsNode));
948
-
949
- if self.is_single_line(node) {
950
- // Inline style: when X then Y
951
- if let Some(statements) = statements {
952
- write!(self.buffer, " then ")?;
953
- self.write_source_text(statements)?;
954
- }
955
- } else {
956
- // Multi-line style: when X\n Y
957
- self.buffer.push('\n');
958
-
959
- if let Some(statements) = statements {
960
- self.emit_statements(statements, indent_level + 1)?;
961
- }
962
- }
963
-
964
- Ok(())
965
- }
966
-
967
- /// Emit if/unless/elsif/else node
968
- /// is_elsif: true if this is an elsif clause (don't emit 'end')
969
- /// keyword: "if" or "unless"
970
- fn emit_if_unless(
971
- &mut self,
972
- node: &Node,
973
- indent_level: usize,
974
- is_elsif: bool,
975
- keyword: &str,
976
- ) -> Result<()> {
977
- // Check if this is a postfix if (modifier form)
978
- // In postfix if, the statements come before the if keyword in source
979
- let is_postfix = if let (Some(predicate), Some(statements)) =
980
- (node.children.first(), node.children.get(1))
981
- {
982
- statements.location.start_offset < predicate.location.start_offset
983
- } else {
984
- false
985
- };
986
-
987
- // Postfix if/unless: "statement if/unless condition"
988
- if is_postfix && !is_elsif {
989
- self.emit_comments_before(node.location.start_line, indent_level)?;
990
- self.emit_indent(indent_level)?;
991
-
992
- // Emit statement
993
- if let Some(statements) = node.children.get(1) {
994
- if matches!(statements.node_type, NodeType::StatementsNode) {
995
- self.write_source_text_trimmed(statements)?;
996
- }
997
- }
998
-
999
- write!(self.buffer, " {} ", keyword)?;
1000
-
1001
- // Emit condition
1002
- if let Some(predicate) = node.children.first() {
1003
- self.write_source_text(predicate)?;
1004
- }
1005
-
1006
- self.emit_trailing_comments(node.location.end_line)?;
1007
- return Ok(());
1008
- }
1009
-
1010
- // Check for ternary operator
1011
- let is_ternary = node
1012
- .metadata
1013
- .get("is_ternary")
1014
- .map(|v| v == "true")
1015
- .unwrap_or(false);
1016
-
1017
- if is_ternary && !is_elsif {
1018
- self.emit_comments_before(node.location.start_line, indent_level)?;
1019
- self.emit_indent(indent_level)?;
1020
-
1021
- // Emit condition
1022
- if let Some(predicate) = node.children.first() {
1023
- self.write_source_text(predicate)?;
1024
- }
1025
-
1026
- write!(self.buffer, " ? ")?;
1027
-
1028
- // Emit then expression
1029
- if let Some(statements) = node.children.get(1) {
1030
- self.write_source_text_trimmed(statements)?;
1031
- }
1032
-
1033
- write!(self.buffer, " : ")?;
1034
-
1035
- // Emit else expression
1036
- if let Some(else_node) = node.children.get(2) {
1037
- if let Some(else_statements) = else_node.children.first() {
1038
- self.write_source_text_trimmed(else_statements)?;
1039
- }
1040
- }
1041
-
1042
- self.emit_trailing_comments(node.location.end_line)?;
1043
- return Ok(());
1044
- }
1045
-
1046
- // Check for inline then style: "if true then 1 end"
1047
- // Single line, not postfix, not ternary, no else clause
1048
- let is_inline_then =
1049
- !is_elsif && self.is_single_line(node) && node.children.get(2).is_none();
1050
-
1051
- if is_inline_then {
1052
- self.emit_comments_before(node.location.start_line, indent_level)?;
1053
- self.emit_indent(indent_level)?;
1054
- write!(self.buffer, "{} ", keyword)?;
1055
-
1056
- // Emit condition
1057
- if let Some(predicate) = node.children.first() {
1058
- self.write_source_text(predicate)?;
1059
- }
1060
-
1061
- write!(self.buffer, " then ")?;
1062
-
1063
- // Emit statement
1064
- if let Some(statements) = node.children.get(1) {
1065
- self.write_source_text_trimmed(statements)?;
1066
- }
1067
-
1068
- write!(self.buffer, " end")?;
1069
- self.emit_trailing_comments(node.location.end_line)?;
1070
- return Ok(());
1071
- }
1072
-
1073
- // Normal if/unless/elsif
1074
- if !is_elsif {
1075
- self.emit_comments_before(node.location.start_line, indent_level)?;
1076
- }
1077
-
1078
- // Emit 'if'/'unless' or 'elsif' keyword
1079
- self.emit_indent(indent_level)?;
1080
- if is_elsif {
1081
- write!(self.buffer, "elsif ")?;
1082
- } else {
1083
- write!(self.buffer, "{} ", keyword)?;
1084
- }
1085
-
1086
- // Emit predicate (condition) - first child
1087
- if let Some(predicate) = node.children.first() {
1088
- self.write_source_text(predicate)?;
1089
- }
1090
-
1091
- // Emit trailing comment on same line as if/unless/elsif
1092
- self.emit_trailing_comments(node.location.start_line)?;
1093
- self.buffer.push('\n');
1094
-
1095
- // Emit then clause (second child is StatementsNode)
1096
- if let Some(statements) = node.children.get(1) {
1097
- if matches!(statements.node_type, NodeType::StatementsNode) {
1098
- self.emit_statements(statements, indent_level + 1)?;
1099
- self.buffer.push('\n');
1100
- }
1101
- }
1102
-
1103
- // Check for elsif/else (third child)
1104
- if let Some(consequent) = node.children.get(2) {
1105
- match &consequent.node_type {
1106
- NodeType::IfNode => {
1107
- // This is an elsif clause (only valid for if, not unless)
1108
- self.emit_if_unless(consequent, indent_level, true, "if")?;
1109
- }
1110
- NodeType::ElseNode => {
1111
- // This is an else clause
1112
- self.emit_indent(indent_level)?;
1113
- writeln!(self.buffer, "else")?;
1114
-
1115
- // Emit else body (first child of ElseNode)
1116
- if let Some(else_statements) = consequent.children.first() {
1117
- if matches!(else_statements.node_type, NodeType::StatementsNode) {
1118
- self.emit_statements(else_statements, indent_level + 1)?;
1119
- self.buffer.push('\n');
1120
- }
1121
- }
1122
- }
1123
- _ => {}
1124
- }
1125
- }
1126
-
1127
- // Only emit 'end' for the outermost if (not for elsif)
1128
- if !is_elsif {
1129
- self.emit_indent(indent_level)?;
1130
- write!(self.buffer, "end")?;
1131
- // Emit trailing comments on end line
1132
- self.emit_trailing_comments(node.location.end_line)?;
1133
- }
1134
-
1135
- Ok(())
1136
- }
1137
-
1138
- /// Emit method call, handling blocks specially for proper indentation
1139
- fn emit_call(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1140
- // Emit any comments before this call
1141
- self.emit_comments_before(node.location.start_line, indent_level)?;
1142
-
1143
- // Check if this call has a block (last child is BlockNode)
1144
- let has_block = node
1145
- .children
1146
- .last()
1147
- .map(|c| matches!(c.node_type, NodeType::BlockNode))
1148
- .unwrap_or(false);
1149
-
1150
- if !has_block {
1151
- // No block - use generic emission (extracts from source)
1152
- return self.emit_generic_without_comments(node, indent_level);
1153
- }
1154
-
1155
- // Has block - need to handle specially
1156
- let block_node = node.children.last().unwrap();
1157
-
1158
- // Determine block style from source (do...end vs { })
1159
- let block_style = self.detect_block_style(block_node);
1160
-
1161
- // Emit the call part (receiver.method(args)) from source
1162
- self.emit_call_without_block(node, block_node, indent_level)?;
1163
-
1164
- match block_style {
1165
- BlockStyle::DoEnd => self.emit_do_end_block(block_node, indent_level)?,
1166
- BlockStyle::Braces => self.emit_brace_block(block_node, indent_level)?,
1167
- }
1168
-
1169
- Ok(())
1170
- }
1171
-
1172
- /// Detect whether block uses do...end or { } style
1173
- fn detect_block_style(&self, block_node: &Node) -> BlockStyle {
1174
- if self.source.is_empty() {
1175
- return BlockStyle::DoEnd; // Default fallback
1176
- }
1177
-
1178
- let start = block_node.location.start_offset;
1179
- if let Some(first_char) = self.source.get(start..start + 1) {
1180
- if first_char == "{" {
1181
- return BlockStyle::Braces;
1182
- }
1183
- }
1184
-
1185
- BlockStyle::DoEnd // Default (includes 'do' keyword)
1186
- }
1187
-
1188
- /// Emit the method call part without the block
1189
- fn emit_call_without_block(
1190
- &mut self,
1191
- call_node: &Node,
1192
- block_node: &Node,
1193
- indent_level: usize,
1194
- ) -> Result<()> {
1195
- self.emit_indent(indent_level)?;
1196
-
1197
- if !self.source.is_empty() {
1198
- let start = call_node.location.start_offset;
1199
- let end = block_node.location.start_offset;
1200
-
1201
- if let Some(text) = self.source.get(start..end) {
1202
- // Trim trailing whitespace but preserve the content
1203
- write!(self.buffer, "{}", text.trim_end())?;
1204
-
1205
- // Mark comments within the extracted range as emitted
1206
- for (idx, comment) in self.all_comments.iter().enumerate() {
1207
- if !self.emitted_comment_indices.contains(&idx)
1208
- && comment.location.start_offset >= start
1209
- && comment.location.end_offset <= end
1210
- {
1211
- self.emitted_comment_indices.insert(idx);
1212
- }
1213
- }
1214
- }
1215
- }
1216
-
1217
- Ok(())
1218
- }
1219
-
1220
- /// Emit a do...end style block with proper indentation
1221
- fn emit_do_end_block(&mut self, block_node: &Node, indent_level: usize) -> Result<()> {
1222
- // Add space before 'do' and emit 'do'
1223
- write!(self.buffer, " do")?;
1224
-
1225
- // Emit block parameters if present (|x, y|)
1226
- self.emit_block_parameters(block_node)?;
1227
-
1228
- // Emit trailing comment on same line as do |...|
1229
- self.emit_trailing_comments(block_node.location.start_line)?;
1230
- self.buffer.push('\n');
1231
-
1232
- // Find and emit the body (StatementsNode among children)
1233
- let block_start_line = block_node.location.start_line;
1234
- let block_end_line = block_node.location.end_line;
1235
- let mut last_stmt_end_line = block_start_line;
1236
-
1237
- for child in &block_node.children {
1238
- match &child.node_type {
1239
- NodeType::StatementsNode => {
1240
- self.emit_statements(child, indent_level + 1)?;
1241
- // Track the last statement's end line for blank line preservation
1242
- if let Some(last_child) = child.children.last() {
1243
- last_stmt_end_line = last_child.location.end_line;
1244
- }
1245
- self.buffer.push('\n');
1246
- break;
1247
- }
1248
- NodeType::BeginNode => {
1249
- // Block with rescue/ensure/else - delegate to emit_begin
1250
- // which handles implicit begin (no "begin" keyword)
1251
- self.emit_begin(child, indent_level + 1)?;
1252
- self.buffer.push('\n');
1253
- last_stmt_end_line = child.location.end_line;
1254
- break;
1255
- }
1256
- _ => {
1257
- // Skip parameter nodes
1258
- }
1259
- }
1260
- }
1261
-
1262
- // Emit comments that are inside the block but not attached to any node
1263
- // (comments between last statement and 'end')
1264
- let had_internal_comments =
1265
- self.has_comments_in_range(block_start_line + 1, block_end_line);
1266
- if had_internal_comments {
1267
- // Preserve blank line between last statement and first comment
1268
- self.emit_comments_in_range_with_prev_line(
1269
- block_start_line + 1,
1270
- block_end_line,
1271
- indent_level + 1,
1272
- last_stmt_end_line,
1273
- )?;
1274
- }
1275
-
1276
- // Add newline if there were internal comments
1277
- if had_internal_comments && !self.buffer.ends_with('\n') {
1278
- self.buffer.push('\n');
1279
- }
1280
-
1281
- // Emit 'end'
1282
- self.emit_indent(indent_level)?;
1283
- write!(self.buffer, "end")?;
1284
- // Emit trailing comments on end line
1285
- self.emit_trailing_comments(block_end_line)?;
1286
-
1287
- Ok(())
1288
- }
1289
-
1290
- /// Emit a { } style block
1291
- fn emit_brace_block(&mut self, block_node: &Node, indent_level: usize) -> Result<()> {
1292
- // Determine if block should be inline or multiline
1293
- let is_multiline = block_node.location.start_line != block_node.location.end_line;
1294
- let block_end_line = block_node.location.end_line;
1295
-
1296
- if is_multiline {
1297
- // Multiline brace block
1298
- write!(self.buffer, " {{")?;
1299
- self.emit_block_parameters(block_node)?;
1300
- self.buffer.push('\n');
1301
-
1302
- // Emit body
1303
- for child in &block_node.children {
1304
- if matches!(child.node_type, NodeType::StatementsNode) {
1305
- self.emit_statements(child, indent_level + 1)?;
1306
- self.buffer.push('\n');
1307
- break;
1308
- }
1309
- }
1310
-
1311
- self.emit_indent(indent_level)?;
1312
- write!(self.buffer, "}}")?;
1313
- self.emit_trailing_comments(block_end_line)?;
1314
- } else {
1315
- // Inline brace block - extract from source to preserve spacing
1316
- write!(self.buffer, " ")?;
1317
- if let Some(text) = self
1318
- .source
1319
- .get(block_node.location.start_offset..block_node.location.end_offset)
1320
- {
1321
- write!(self.buffer, "{}", text)?;
1322
- }
1323
- self.emit_trailing_comments(block_end_line)?;
1324
- }
1325
-
1326
- Ok(())
1327
- }
1328
-
1329
- /// Emit block parameters (|x, y|)
1330
- fn emit_block_parameters(&mut self, block_node: &Node) -> Result<()> {
1331
- if self.source.is_empty() {
1332
- return Ok(());
1333
- }
1334
-
1335
- let start = block_node.location.start_offset;
1336
- let end = block_node.location.end_offset;
1337
-
1338
- if let Some(block_source) = self.source.get(start..end) {
1339
- // Only look at the first line of the block for parameters
1340
- let first_line = block_source.lines().next().unwrap_or("");
1341
-
1342
- // Find |...| pattern in the first line only
1343
- if let Some(pipe_start) = first_line.find('|') {
1344
- // Find matching pipe after first one
1345
- if let Some(pipe_end) = first_line[pipe_start + 1..].find('|') {
1346
- let params = &first_line[pipe_start..=pipe_start + 1 + pipe_end];
1347
- write!(self.buffer, " {}", params)?;
1348
- }
1349
- }
1350
- }
1351
-
1352
- Ok(())
1353
- }
1354
-
1355
- /// Emit generic node without re-emitting comments (for use when comments already handled)
1356
- fn emit_generic_without_comments(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1357
- if !self.source.is_empty() {
1358
- let start = node.location.start_offset;
1359
- let end = node.location.end_offset;
1360
-
1361
- let text_owned = self.source.get(start..end).map(|s| s.to_string());
1362
-
1363
- if let Some(text) = text_owned {
1364
- self.emit_indent(indent_level)?;
1365
- write!(self.buffer, "{}", text)?;
1366
-
1367
- // Mark comments that are strictly inside this node's line range as emitted
1368
- // (they are included in the source extraction)
1369
- // Don't mark trailing comments on the last line (they come after the node ends)
1370
- for (idx, comment) in self.all_comments.iter().enumerate() {
1371
- if !self.emitted_comment_indices.contains(&idx)
1372
- && comment.location.start_line >= node.location.start_line
1373
- && comment.location.end_line < node.location.end_line
1374
- {
1375
- self.emitted_comment_indices.insert(idx);
1376
- }
1377
- }
1378
-
1379
- // Emit trailing comments on the same line (after the node ends)
1380
- self.emit_trailing_comments(node.location.end_line)?;
1381
- }
1382
- }
1383
- Ok(())
1384
- }
1385
-
1386
- /// Emit variable write node (LocalVariableWriteNode, InstanceVariableWriteNode)
1387
- /// Handles `x = value` and `@x = value` patterns
1388
- fn emit_variable_write(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1389
- self.emit_comments_before(node.location.start_line, indent_level)?;
1390
-
1391
- let name = node.metadata.get("name").map(|s| s.as_str()).unwrap_or("_");
1392
-
1393
- // Get value node (first child)
1394
- let value = match node.children.first() {
1395
- Some(v) => v,
1396
- None => {
1397
- // No value: fallback to source extraction
1398
- return self.emit_generic(node, indent_level);
1399
- }
1400
- };
1401
-
1402
- let is_block_value = matches!(
1403
- value.node_type,
1404
- NodeType::IfNode
1405
- | NodeType::UnlessNode
1406
- | NodeType::CaseNode
1407
- | NodeType::CaseMatchNode
1408
- | NodeType::BeginNode
1409
- | NodeType::WhileNode
1410
- | NodeType::UntilNode
1411
- | NodeType::ForNode
1412
- );
1413
-
1414
- self.emit_indent(indent_level)?;
1415
- if is_block_value {
1416
- writeln!(self.buffer, "{} =", name)?;
1417
- self.emit_node(value, indent_level + 1)?;
1418
- } else {
1419
- write!(self.buffer, "{} = ", name)?;
1420
- self.write_source_text_trimmed(value)?;
1421
- }
1422
-
1423
- self.emit_trailing_comments(node.location.end_line)?;
1424
-
1425
- Ok(())
1426
- }
1427
-
1428
- /// Emit generic node by extracting from source
1429
- fn emit_generic(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1430
- self.emit_comments_before(node.location.start_line, indent_level)?;
1431
-
1432
- if !self.source.is_empty() {
1433
- let start = node.location.start_offset;
1434
- let end = node.location.end_offset;
1435
-
1436
- let text_owned = self.source.get(start..end).map(|s| s.to_string());
1437
-
1438
- if let Some(text) = text_owned {
1439
- self.emit_indent(indent_level)?;
1440
- write!(self.buffer, "{}", text)?;
1441
-
1442
- // Mark comments that are strictly inside this node's line range as emitted
1443
- // (they are included in the source extraction)
1444
- // Don't mark trailing comments on the last line (they come after the node ends)
1445
- for (idx, comment) in self.all_comments.iter().enumerate() {
1446
- if !self.emitted_comment_indices.contains(&idx)
1447
- && comment.location.start_line >= node.location.start_line
1448
- && comment.location.end_line < node.location.end_line
1449
- {
1450
- self.emitted_comment_indices.insert(idx);
1451
- }
1452
- }
1453
-
1454
- self.emit_trailing_comments(node.location.end_line)?;
1455
- }
1456
- }
1457
- Ok(())
1458
- }
1459
-
1460
- /// Ensure indent cache has entries up to and including the given level
1461
- /// This allows pre-building the cache before borrowing self.indent_cache
1462
- fn ensure_indent_cache(&mut self, level: usize) {
1463
- while self.indent_cache.len() <= level {
1464
- let len = self.indent_cache.len();
1465
- let indent = match self.config.formatting.indent_style {
1466
- IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * len),
1467
- IndentStyle::Tabs => "\t".repeat(len),
1468
- };
1469
- self.indent_cache.push(indent);
1470
- }
1471
- }
1472
-
1473
- /// Emit indentation
1474
- fn emit_indent(&mut self, level: usize) -> Result<()> {
1475
- self.ensure_indent_cache(level);
1476
- write!(self.buffer, "{}", &self.indent_cache[level])?;
1477
- Ok(())
1478
- }
1479
-
1480
- /// Emit while/until loop
1481
- fn emit_while_until(&mut self, node: &Node, indent_level: usize, keyword: &str) -> Result<()> {
1482
- // Check if this is a postfix while/until (modifier form)
1483
- // In postfix form: "statement while/until condition"
1484
- // Check if body starts before predicate in source
1485
- let is_postfix = if node.children.len() >= 2 {
1486
- let predicate = &node.children[0];
1487
- let body = &node.children[1];
1488
- body.location.start_offset < predicate.location.start_offset
1489
- } else {
1490
- false
1491
- };
1492
-
1493
- if is_postfix {
1494
- // Postfix form: extract from source as-is
1495
- return self.emit_generic(node, indent_level);
1496
- }
1497
-
1498
- // Normal while/until with do...end
1499
- self.emit_comments_before(node.location.start_line, indent_level)?;
1500
- self.emit_indent(indent_level)?;
1501
- write!(self.buffer, "{} ", keyword)?;
1502
-
1503
- // Emit predicate (condition) - first child
1504
- if let Some(predicate) = node.children.first() {
1505
- if !self.source.is_empty() {
1506
- let start = predicate.location.start_offset;
1507
- let end = predicate.location.end_offset;
1508
- if let Some(text) = self.source.get(start..end) {
1509
- write!(self.buffer, "{}", text)?;
1510
- }
1511
- }
1512
- }
1513
-
1514
- // Emit trailing comment on same line as while/until
1515
- self.emit_trailing_comments(node.location.start_line)?;
1516
- self.buffer.push('\n');
1517
-
1518
- // Emit body - second child (StatementsNode)
1519
- if let Some(body) = node.children.get(1) {
1520
- if matches!(body.node_type, NodeType::StatementsNode) {
1521
- self.emit_statements(body, indent_level + 1)?;
1522
- self.buffer.push('\n');
1523
- }
1524
- }
1525
-
1526
- self.emit_indent(indent_level)?;
1527
- write!(self.buffer, "end")?;
1528
- // Emit trailing comments on end line
1529
- self.emit_trailing_comments(node.location.end_line)?;
1530
-
1531
- Ok(())
1532
- }
1533
-
1534
- /// Emit for loop
1535
- fn emit_for(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1536
- self.emit_comments_before(node.location.start_line, indent_level)?;
1537
- self.emit_indent(indent_level)?;
1538
- write!(self.buffer, "for ")?;
1539
-
1540
- // node.children: [index, collection, statements]
1541
- // index: LocalVariableTargetNode or MultiTargetNode
1542
- // collection: expression
1543
- // statements: StatementsNode
1544
-
1545
- // Emit index variable - first child
1546
- if let Some(index) = node.children.first() {
1547
- if !self.source.is_empty() {
1548
- let start = index.location.start_offset;
1549
- let end = index.location.end_offset;
1550
- if let Some(text) = self.source.get(start..end) {
1551
- write!(self.buffer, "{}", text)?;
1552
- }
1553
- }
1554
- }
1555
-
1556
- write!(self.buffer, " in ")?;
1557
-
1558
- // Emit collection - second child
1559
- if let Some(collection) = node.children.get(1) {
1560
- if !self.source.is_empty() {
1561
- let start = collection.location.start_offset;
1562
- let end = collection.location.end_offset;
1563
- if let Some(text) = self.source.get(start..end) {
1564
- write!(self.buffer, "{}", text)?;
1565
- }
1566
- }
1567
- }
1568
-
1569
- self.buffer.push('\n');
1570
-
1571
- // Emit body - third child (StatementsNode)
1572
- if let Some(body) = node.children.get(2) {
1573
- if matches!(body.node_type, NodeType::StatementsNode) {
1574
- self.emit_statements(body, indent_level + 1)?;
1575
- self.buffer.push('\n');
1576
- }
1577
- }
1578
-
1579
- self.emit_indent(indent_level)?;
1580
- write!(self.buffer, "end")?;
1581
- // Emit trailing comments on end line
1582
- self.emit_trailing_comments(node.location.end_line)?;
1583
-
1584
- Ok(())
1585
- }
1586
-
1587
- /// Emit singleton class definition (class << self / class << object)
1588
- fn emit_singleton_class(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1589
- self.emit_comments_before(node.location.start_line, indent_level)?;
1590
- self.emit_indent(indent_level)?;
1591
-
1592
- write!(self.buffer, "class << ")?;
1593
-
1594
- // First child is the expression (self or an object)
1595
- if let Some(expression) = node.children.first() {
1596
- if !self.source.is_empty() {
1597
- let start = expression.location.start_offset;
1598
- let end = expression.location.end_offset;
1599
- if let Some(text) = self.source.get(start..end) {
1600
- write!(self.buffer, "{}", text)?;
1601
- }
1602
- }
1603
- }
1604
-
1605
- // Emit trailing comments on the class << line
1606
- self.emit_trailing_comments(node.location.start_line)?;
1607
- self.buffer.push('\n');
1608
-
1609
- let class_start_line = node.location.start_line;
1610
- let class_end_line = node.location.end_line;
1611
- let mut has_body_content = false;
1612
-
1613
- // Emit body (skip the first child which is the expression)
1614
- for (i, child) in node.children.iter().enumerate() {
1615
- if i == 0 {
1616
- // Skip the expression (self or object)
1617
- continue;
1618
- }
1619
- if matches!(child.node_type, NodeType::StatementsNode) {
1620
- has_body_content = true;
1621
- self.emit_statements(child, indent_level + 1)?;
1622
- } else if !self.is_structural_node(&child.node_type) {
1623
- has_body_content = true;
1624
- self.emit_node(child, indent_level + 1)?;
1625
- }
1626
- }
1627
-
1628
- // Emit comments that appear before the end statement while preserving their position
1629
- self.emit_comments_before_end(class_start_line, class_end_line, indent_level + 1)?;
1630
-
1631
- // Add newline before end if there was body content
1632
- if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
1633
- && !self.buffer.ends_with('\n')
1634
- {
1635
- self.buffer.push('\n');
1636
- }
1637
-
1638
- self.emit_indent(indent_level)?;
1639
- write!(self.buffer, "end")?;
1640
- self.emit_trailing_comments(node.location.end_line)?;
1641
-
1642
- Ok(())
1643
- }
1644
-
1645
- /// Emit case match (Ruby 3.0+ pattern matching with case...in)
1646
- fn emit_case_match(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1647
- self.emit_comments_before(node.location.start_line, indent_level)?;
1648
- self.emit_indent(indent_level)?;
1649
-
1650
- // Write "case" keyword
1651
- write!(self.buffer, "case")?;
1652
-
1653
- // Find predicate (first child that isn't InNode or ElseNode)
1654
- let mut in_start_idx = 0;
1655
- if let Some(first_child) = node.children.first() {
1656
- if !matches!(first_child.node_type, NodeType::InNode | NodeType::ElseNode) {
1657
- // This is the predicate - extract from source
1658
- let start = first_child.location.start_offset;
1659
- let end = first_child.location.end_offset;
1660
- if let Some(text) = self.source.get(start..end) {
1661
- write!(self.buffer, " {}", text)?;
1662
- }
1663
- in_start_idx = 1;
1664
- }
1665
- }
1666
-
1667
- self.buffer.push('\n');
1668
-
1669
- // Emit in clauses and else
1670
- for child in node.children.iter().skip(in_start_idx) {
1671
- match &child.node_type {
1672
- NodeType::InNode => {
1673
- self.emit_in(child, indent_level)?;
1674
- }
1675
- NodeType::ElseNode => {
1676
- self.emit_indent(indent_level)?;
1677
- writeln!(self.buffer, "else")?;
1678
- // Emit else body
1679
- for else_child in &child.children {
1680
- if matches!(else_child.node_type, NodeType::StatementsNode) {
1681
- self.emit_statements(else_child, indent_level + 1)?;
1682
- } else {
1683
- self.emit_node(else_child, indent_level + 1)?;
1684
- }
1685
- }
1686
- self.buffer.push('\n');
1687
- }
1688
- _ => {}
1689
- }
1690
- }
1691
-
1692
- // Emit "end" keyword
1693
- self.emit_indent(indent_level)?;
1694
- write!(self.buffer, "end")?;
1695
- self.emit_trailing_comments(node.location.end_line)?;
1696
-
1697
- Ok(())
1698
- }
1699
-
1700
- /// Emit in node (pattern matching clause)
1701
- fn emit_in(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1702
- self.emit_comments_before(node.location.start_line, indent_level)?;
1703
- self.emit_indent(indent_level)?;
1704
-
1705
- write!(self.buffer, "in ")?;
1706
-
1707
- // First child is the pattern
1708
- if let Some(pattern) = node.children.first() {
1709
- self.write_source_text(pattern)?;
1710
- }
1711
-
1712
- if self.is_single_line(node) {
1713
- // Inline style: in X then Y
1714
- if let Some(statements) = node.children.get(1) {
1715
- write!(self.buffer, " then ")?;
1716
- self.write_source_text(statements)?;
1717
- }
1718
- self.buffer.push('\n');
1719
- } else {
1720
- // Multi-line style: in X\n Y
1721
- self.buffer.push('\n');
1722
-
1723
- // Second child is the statements body
1724
- if let Some(statements) = node.children.get(1) {
1725
- if matches!(statements.node_type, NodeType::StatementsNode) {
1726
- self.emit_statements(statements, indent_level + 1)?;
1727
- }
1728
- }
1729
- }
1730
-
1731
- Ok(())
1732
- }
1733
-
1734
- /// Check if node is structural (part of definition syntax, not body)
1735
- /// These nodes are part of class/module/method definitions and should not be emitted as body
1736
- fn is_structural_node(&self, node_type: &NodeType) -> bool {
1737
- matches!(
1738
- node_type,
1739
- NodeType::ConstantReadNode
1740
- | NodeType::ConstantWriteNode
1741
- | NodeType::ConstantPathNode
1742
- | NodeType::RequiredParameterNode
1743
- | NodeType::OptionalParameterNode
1744
- | NodeType::RestParameterNode
1745
- | NodeType::KeywordParameterNode
1746
- | NodeType::RequiredKeywordParameterNode
1747
- | NodeType::OptionalKeywordParameterNode
1748
- | NodeType::KeywordRestParameterNode
1749
- | NodeType::BlockParameterNode
1750
- | NodeType::ForwardingParameterNode
1751
- | NodeType::NoKeywordsParameterNode
1752
- )
1753
- }
1754
- }
1755
-
1756
- impl Default for Emitter {
1757
- fn default() -> Self {
1758
- Self::new(Config::default())
1759
- }
1760
- }