rfmt 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/Cargo.lock +266 -92
- data/README.md +22 -18
- data/ext/rfmt/Cargo.toml +4 -1
- data/ext/rfmt/src/doc/builders.rs +528 -0
- data/ext/rfmt/src/doc/mod.rs +220 -0
- data/ext/rfmt/src/doc/printer.rs +684 -0
- data/ext/rfmt/src/format/context.rs +448 -0
- data/ext/rfmt/src/format/formatter.rs +226 -0
- data/ext/rfmt/src/format/mod.rs +35 -0
- data/ext/rfmt/src/format/registry.rs +195 -0
- data/ext/rfmt/src/format/rule.rs +555 -0
- data/ext/rfmt/src/format/rules/begin.rs +295 -0
- data/ext/rfmt/src/format/rules/body_end.rs +109 -0
- data/ext/rfmt/src/format/rules/call.rs +409 -0
- data/ext/rfmt/src/format/rules/case.rs +359 -0
- data/ext/rfmt/src/format/rules/class.rs +160 -0
- data/ext/rfmt/src/format/rules/def.rs +216 -0
- data/ext/rfmt/src/format/rules/fallback.rs +116 -0
- data/ext/rfmt/src/format/rules/if_unless.rs +407 -0
- data/ext/rfmt/src/format/rules/loops.rs +325 -0
- data/ext/rfmt/src/format/rules/mod.rs +31 -0
- data/ext/rfmt/src/format/rules/module.rs +150 -0
- data/ext/rfmt/src/format/rules/singleton_class.rs +202 -0
- data/ext/rfmt/src/format/rules/statements.rs +122 -0
- data/ext/rfmt/src/format/rules/variable_write.rs +296 -0
- data/ext/rfmt/src/lib.rs +8 -5
- data/ext/rfmt/src/parser/prism_adapter.rs +157 -2
- data/lib/rfmt/version.rb +1 -1
- data/lib/ruby_lsp/rfmt/formatter_runner.rb +2 -0
- metadata +23 -2
- data/ext/rfmt/src/emitter/mod.rs +0 -1760
data/ext/rfmt/src/emitter/mod.rs
DELETED
|
@@ -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
|
-
}
|