rfmt 1.2.2 → 1.2.4
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 +10 -0
- data/Cargo.lock +1 -1
- data/ext/rfmt/Cargo.toml +1 -1
- data/ext/rfmt/src/emitter/mod.rs +303 -27
- data/lib/rfmt/prism_bridge.rb +14 -2
- data/lib/rfmt/prism_node_extractor.rb +25 -0
- data/lib/rfmt/rfmt.so +0 -0
- data/lib/rfmt/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c95517c6abaad1c4bf47425dd4853dcd78bf5a2e47ab2bdc0cae1a3d3698c54
|
|
4
|
+
data.tar.gz: a207798a5811f989a5ebb3405e48e0c86ac60d40b208f141e6d31a83de62dc0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86307164b77b1836c57718c62ac84bbc920fbfc36a7f0367656bc4fadd65cd846df9f45e5d8985a3eff2f49a548a576a8a73cb05e8e78679fe666589e904762c
|
|
7
|
+
data.tar.gz: 69a7f17c2e547616ebea1a109628df5d3695fb9d9359715d90707a51c4d1c2d72095bad48ffc1d370e7229a01f193392235c3c7c2a0f13fd211dc0885ef2c447
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.2.4] - 2026-01-04
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fix comment indent space handling
|
|
7
|
+
|
|
8
|
+
## [1.2.3] - 2026-01-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fix migration file formatting (`emit_rescue` handling for rescue blocks)
|
|
12
|
+
|
|
3
13
|
## [1.2.2] - 2026-01-04
|
|
4
14
|
|
|
5
15
|
### Fixed
|
data/Cargo.lock
CHANGED
data/ext/rfmt/Cargo.toml
CHANGED
data/ext/rfmt/src/emitter/mod.rs
CHANGED
|
@@ -50,6 +50,12 @@ impl Emitter {
|
|
|
50
50
|
|
|
51
51
|
self.emit_node(ast, 0)?;
|
|
52
52
|
|
|
53
|
+
// Find the last emitted code line for proper blank line handling
|
|
54
|
+
let last_code_line = Self::find_last_code_line(ast);
|
|
55
|
+
|
|
56
|
+
// Emit any remaining comments that weren't emitted
|
|
57
|
+
self.emit_remaining_comments(last_code_line)?;
|
|
58
|
+
|
|
53
59
|
// Ensure file ends with a newline
|
|
54
60
|
if !self.buffer.ends_with('\n') {
|
|
55
61
|
self.buffer.push('\n');
|
|
@@ -58,6 +64,43 @@ impl Emitter {
|
|
|
58
64
|
Ok(self.buffer.clone())
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
/// Find the last line of code in the AST (excluding comments)
|
|
68
|
+
fn find_last_code_line(ast: &Node) -> usize {
|
|
69
|
+
let mut max_line = ast.location.end_line;
|
|
70
|
+
for child in &ast.children {
|
|
71
|
+
let child_end = Self::find_last_code_line(child);
|
|
72
|
+
if child_end > max_line {
|
|
73
|
+
max_line = child_end;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
max_line
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Emit all comments that haven't been emitted yet
|
|
80
|
+
fn emit_remaining_comments(&mut self, last_code_line: usize) -> Result<()> {
|
|
81
|
+
let mut last_end_line: Option<usize> = Some(last_code_line);
|
|
82
|
+
for (idx, comment) in self.all_comments.iter().enumerate() {
|
|
83
|
+
if self.emitted_comment_indices.contains(&idx) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Ensure we start on a new line for remaining comments
|
|
87
|
+
if !self.buffer.ends_with('\n') {
|
|
88
|
+
self.buffer.push('\n');
|
|
89
|
+
}
|
|
90
|
+
// Preserve blank lines between code/comments
|
|
91
|
+
if let Some(prev_line) = last_end_line {
|
|
92
|
+
let gap = comment.location.start_line.saturating_sub(prev_line);
|
|
93
|
+
for _ in 1..gap {
|
|
94
|
+
self.buffer.push('\n');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
writeln!(self.buffer, "{}", comment.text)?;
|
|
98
|
+
self.emitted_comment_indices.push(idx);
|
|
99
|
+
last_end_line = Some(comment.location.end_line);
|
|
100
|
+
}
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
|
|
61
104
|
/// Recursively collect all comments from the AST
|
|
62
105
|
fn collect_comments(&mut self, node: &Node) {
|
|
63
106
|
self.all_comments.extend(node.comments.clone());
|
|
@@ -80,16 +123,37 @@ impl Emitter {
|
|
|
80
123
|
}
|
|
81
124
|
|
|
82
125
|
if comment.location.end_line < line {
|
|
83
|
-
comments_to_emit.push((
|
|
126
|
+
comments_to_emit.push((
|
|
127
|
+
idx,
|
|
128
|
+
comment.text.clone(),
|
|
129
|
+
comment.location.start_line,
|
|
130
|
+
comment.location.end_line,
|
|
131
|
+
));
|
|
84
132
|
}
|
|
85
133
|
}
|
|
86
134
|
|
|
135
|
+
// Sort by start_line to emit in order
|
|
136
|
+
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
137
|
+
|
|
87
138
|
let comments_count = comments_to_emit.len();
|
|
88
|
-
|
|
139
|
+
let mut last_comment_end_line: Option<usize> = None;
|
|
140
|
+
|
|
141
|
+
for (i, (idx, text, comment_start_line, comment_end_line)) in
|
|
142
|
+
comments_to_emit.into_iter().enumerate()
|
|
143
|
+
{
|
|
144
|
+
// Preserve blank lines between comments
|
|
145
|
+
if let Some(prev_end) = last_comment_end_line {
|
|
146
|
+
let gap = comment_start_line.saturating_sub(prev_end);
|
|
147
|
+
for _ in 1..gap {
|
|
148
|
+
self.buffer.push('\n');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
89
152
|
writeln!(self.buffer, "{}{}", indent_str, text)?;
|
|
90
153
|
self.emitted_comment_indices.push(idx);
|
|
154
|
+
last_comment_end_line = Some(comment_end_line);
|
|
91
155
|
|
|
92
|
-
//
|
|
156
|
+
// Add blank line after the LAST comment if there was a gap to the code
|
|
93
157
|
if i == comments_count - 1 && line > comment_end_line + 1 {
|
|
94
158
|
self.buffer.push('\n');
|
|
95
159
|
}
|
|
@@ -98,6 +162,65 @@ impl Emitter {
|
|
|
98
162
|
Ok(())
|
|
99
163
|
}
|
|
100
164
|
|
|
165
|
+
/// Check if there are any unemitted comments in the given line range
|
|
166
|
+
fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
|
|
167
|
+
self.all_comments.iter().enumerate().any(|(idx, comment)| {
|
|
168
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
169
|
+
&& comment.location.start_line >= start_line
|
|
170
|
+
&& comment.location.end_line < end_line
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Emit comments that are within a given line range (exclusive of end_line)
|
|
175
|
+
fn emit_comments_in_range(
|
|
176
|
+
&mut self,
|
|
177
|
+
start_line: usize,
|
|
178
|
+
end_line: usize,
|
|
179
|
+
indent_level: usize,
|
|
180
|
+
) -> Result<()> {
|
|
181
|
+
let indent_str = match self.config.formatting.indent_style {
|
|
182
|
+
IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
|
|
183
|
+
IndentStyle::Tabs => "\t".repeat(indent_level),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
let mut comments_to_emit = Vec::new();
|
|
187
|
+
for (idx, comment) in self.all_comments.iter().enumerate() {
|
|
188
|
+
if self.emitted_comment_indices.contains(&idx) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if comment.location.start_line >= start_line && comment.location.end_line < end_line {
|
|
193
|
+
comments_to_emit.push((
|
|
194
|
+
idx,
|
|
195
|
+
comment.text.clone(),
|
|
196
|
+
comment.location.start_line,
|
|
197
|
+
comment.location.end_line,
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Sort by start_line to emit in order
|
|
203
|
+
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
204
|
+
|
|
205
|
+
let mut last_comment_end_line: Option<usize> = None;
|
|
206
|
+
|
|
207
|
+
for (idx, text, comment_start_line, comment_end_line) in comments_to_emit {
|
|
208
|
+
// Preserve blank lines between comments
|
|
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!(self.buffer, "{}{}", indent_str, text)?;
|
|
217
|
+
self.emitted_comment_indices.push(idx);
|
|
218
|
+
last_comment_end_line = Some(comment_end_line);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Ok(())
|
|
222
|
+
}
|
|
223
|
+
|
|
101
224
|
/// Emit comments that appear on the same line (trailing comments)
|
|
102
225
|
fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
|
|
103
226
|
let mut indices_to_emit = Vec::new();
|
|
@@ -218,22 +341,35 @@ impl Emitter {
|
|
|
218
341
|
write!(self.buffer, " < {}", superclass)?;
|
|
219
342
|
}
|
|
220
343
|
|
|
344
|
+
// Emit trailing comments on the class definition line (e.g., # rubocop:disable)
|
|
345
|
+
self.emit_trailing_comments(node.location.start_line)?;
|
|
221
346
|
self.buffer.push('\n');
|
|
222
347
|
|
|
223
|
-
// Emit body (children), but skip structural nodes
|
|
348
|
+
// Emit body (children), but skip structural nodes (class name, superclass)
|
|
349
|
+
// Use start_line check to properly handle CallNode superclasses like ActiveRecord::Migration[8.0]
|
|
350
|
+
let class_start_line = node.location.start_line;
|
|
351
|
+
let class_end_line = node.location.end_line;
|
|
352
|
+
let mut has_body_content = false;
|
|
353
|
+
|
|
224
354
|
for child in &node.children {
|
|
355
|
+
// Skip nodes on the same line as class definition (name, superclass)
|
|
356
|
+
if child.location.start_line == class_start_line {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
225
359
|
if self.is_structural_node(&child.node_type) {
|
|
226
360
|
continue;
|
|
227
361
|
}
|
|
362
|
+
has_body_content = true;
|
|
228
363
|
self.emit_node(child, indent_level + 1)?;
|
|
229
|
-
// Note: don't add newline here, statements node will handle it
|
|
230
364
|
}
|
|
231
365
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
366
|
+
// Emit comments that are inside the class body but not attached to any node
|
|
367
|
+
// These are comments between class_start_line and class_end_line
|
|
368
|
+
self.emit_comments_in_range(class_start_line + 1, class_end_line, indent_level + 1)?;
|
|
369
|
+
|
|
370
|
+
// Add newline before end if there was body content or internal comments
|
|
371
|
+
if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
|
|
372
|
+
&& !self.buffer.ends_with('\n')
|
|
237
373
|
{
|
|
238
374
|
self.buffer.push('\n');
|
|
239
375
|
}
|
|
@@ -256,21 +392,29 @@ impl Emitter {
|
|
|
256
392
|
write!(self.buffer, "{}", name)?;
|
|
257
393
|
}
|
|
258
394
|
|
|
395
|
+
// Emit trailing comments on the module definition line
|
|
396
|
+
self.emit_trailing_comments(node.location.start_line)?;
|
|
259
397
|
self.buffer.push('\n');
|
|
260
398
|
|
|
399
|
+
let module_start_line = node.location.start_line;
|
|
400
|
+
let module_end_line = node.location.end_line;
|
|
401
|
+
let mut has_body_content = false;
|
|
402
|
+
|
|
261
403
|
// Emit body (children), but skip structural nodes
|
|
262
404
|
for child in &node.children {
|
|
263
405
|
if self.is_structural_node(&child.node_type) {
|
|
264
406
|
continue;
|
|
265
407
|
}
|
|
408
|
+
has_body_content = true;
|
|
266
409
|
self.emit_node(child, indent_level + 1)?;
|
|
267
410
|
}
|
|
268
411
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
412
|
+
// Emit comments that are inside the module body but not attached to any node
|
|
413
|
+
self.emit_comments_in_range(module_start_line + 1, module_end_line, indent_level + 1)?;
|
|
414
|
+
|
|
415
|
+
// Add newline before end if there was body content or internal comments
|
|
416
|
+
if (has_body_content || self.has_comments_in_range(module_start_line + 1, module_end_line))
|
|
417
|
+
&& !self.buffer.ends_with('\n')
|
|
274
418
|
{
|
|
275
419
|
self.buffer.push('\n');
|
|
276
420
|
}
|
|
@@ -289,6 +433,11 @@ impl Emitter {
|
|
|
289
433
|
self.emit_indent(indent_level)?;
|
|
290
434
|
write!(self.buffer, "def ")?;
|
|
291
435
|
|
|
436
|
+
// Handle class methods (def self.method_name)
|
|
437
|
+
if let Some(receiver) = node.metadata.get("receiver") {
|
|
438
|
+
write!(self.buffer, "{}.", receiver)?;
|
|
439
|
+
}
|
|
440
|
+
|
|
292
441
|
if let Some(name) = node.metadata.get("name") {
|
|
293
442
|
write!(self.buffer, "{}", name)?;
|
|
294
443
|
}
|
|
@@ -321,6 +470,8 @@ impl Emitter {
|
|
|
321
470
|
}
|
|
322
471
|
}
|
|
323
472
|
|
|
473
|
+
// Emit trailing comment on same line as def
|
|
474
|
+
self.emit_trailing_comments(node.location.start_line)?;
|
|
324
475
|
self.buffer.push('\n');
|
|
325
476
|
|
|
326
477
|
// Emit body (children), but skip structural nodes like parameter nodes
|
|
@@ -398,28 +549,78 @@ impl Emitter {
|
|
|
398
549
|
write!(self.buffer, "rescue")?;
|
|
399
550
|
|
|
400
551
|
// Extract exception classes and variable from source
|
|
552
|
+
// Handle multi-line rescue clauses (e.g., multiple exception classes spanning lines)
|
|
401
553
|
if !self.source.is_empty() && node.location.end_offset <= self.source.len() {
|
|
402
554
|
if let Some(source_text) = self
|
|
403
555
|
.source
|
|
404
556
|
.get(node.location.start_offset..node.location.end_offset)
|
|
405
557
|
{
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
558
|
+
// Find the rescue declaration part (first line only, unless trailing comma/backslash)
|
|
559
|
+
let mut rescue_decl = String::new();
|
|
560
|
+
let mut expect_continuation = false;
|
|
561
|
+
|
|
562
|
+
for line in source_text.lines() {
|
|
563
|
+
let trimmed = line.trim();
|
|
564
|
+
|
|
565
|
+
if rescue_decl.is_empty() {
|
|
566
|
+
// First line - remove "rescue" prefix
|
|
567
|
+
let after_rescue = trimmed.trim_start_matches("rescue").trim();
|
|
568
|
+
if !after_rescue.is_empty() {
|
|
569
|
+
// Check if line ends with continuation marker
|
|
570
|
+
expect_continuation =
|
|
571
|
+
after_rescue.ends_with(',') || after_rescue.ends_with('\\');
|
|
572
|
+
rescue_decl.push_str(after_rescue.trim_end_matches('\\').trim());
|
|
573
|
+
}
|
|
574
|
+
if !expect_continuation {
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
} else if expect_continuation {
|
|
578
|
+
// Continuation line after trailing comma or backslash
|
|
579
|
+
// Add space after comma or if no trailing space
|
|
580
|
+
if !rescue_decl.ends_with(' ') {
|
|
581
|
+
rescue_decl.push(' ');
|
|
582
|
+
}
|
|
583
|
+
let content = trimmed.trim_end_matches('\\').trim();
|
|
584
|
+
rescue_decl.push_str(content);
|
|
585
|
+
expect_continuation = trimmed.ends_with(',') || trimmed.ends_with('\\');
|
|
586
|
+
if !expect_continuation {
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
break;
|
|
412
591
|
}
|
|
413
592
|
}
|
|
593
|
+
|
|
594
|
+
if !rescue_decl.is_empty() {
|
|
595
|
+
write!(self.buffer, " {}", rescue_decl)?;
|
|
596
|
+
}
|
|
414
597
|
}
|
|
415
598
|
}
|
|
416
599
|
|
|
417
600
|
self.buffer.push('\n');
|
|
418
601
|
|
|
419
|
-
// Emit rescue body
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
602
|
+
// Emit rescue body and handle subsequent rescue nodes
|
|
603
|
+
// Children structure:
|
|
604
|
+
// - ConstantReadNode/ConstantPathNode (exception classes)
|
|
605
|
+
// - LocalVariableTargetNode (optional, exception variable)
|
|
606
|
+
// - StatementsNode (rescue body)
|
|
607
|
+
// - RescueNode (optional, subsequent rescue clause)
|
|
608
|
+
for child in &node.children {
|
|
609
|
+
match &child.node_type {
|
|
610
|
+
NodeType::StatementsNode => {
|
|
611
|
+
self.emit_node(child, indent_level)?;
|
|
612
|
+
}
|
|
613
|
+
NodeType::RescueNode => {
|
|
614
|
+
// Emit subsequent rescue clause
|
|
615
|
+
// Ensure newline before subsequent rescue
|
|
616
|
+
if !self.buffer.ends_with('\n') {
|
|
617
|
+
self.buffer.push('\n');
|
|
618
|
+
}
|
|
619
|
+
self.emit_rescue(child, indent_level)?;
|
|
620
|
+
}
|
|
621
|
+
_ => {
|
|
622
|
+
// Skip exception classes and variable (already handled above)
|
|
623
|
+
}
|
|
423
624
|
}
|
|
424
625
|
}
|
|
425
626
|
|
|
@@ -612,6 +813,59 @@ impl Emitter {
|
|
|
612
813
|
return Ok(());
|
|
613
814
|
}
|
|
614
815
|
|
|
816
|
+
// Check for ternary operator
|
|
817
|
+
let is_ternary = node
|
|
818
|
+
.metadata
|
|
819
|
+
.get("is_ternary")
|
|
820
|
+
.map(|v| v == "true")
|
|
821
|
+
.unwrap_or(false);
|
|
822
|
+
|
|
823
|
+
if is_ternary && !is_elsif {
|
|
824
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
825
|
+
self.emit_indent(indent_level)?;
|
|
826
|
+
|
|
827
|
+
// Emit condition
|
|
828
|
+
if let Some(predicate) = node.children.first() {
|
|
829
|
+
if !self.source.is_empty() {
|
|
830
|
+
let start = predicate.location.start_offset;
|
|
831
|
+
let end = predicate.location.end_offset;
|
|
832
|
+
if let Some(text) = self.source.get(start..end) {
|
|
833
|
+
write!(self.buffer, "{}", text)?;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
write!(self.buffer, " ? ")?;
|
|
839
|
+
|
|
840
|
+
// Emit then expression
|
|
841
|
+
if let Some(statements) = node.children.get(1) {
|
|
842
|
+
if !self.source.is_empty() {
|
|
843
|
+
let start = statements.location.start_offset;
|
|
844
|
+
let end = statements.location.end_offset;
|
|
845
|
+
if let Some(text) = self.source.get(start..end) {
|
|
846
|
+
write!(self.buffer, "{}", text.trim())?;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
write!(self.buffer, " : ")?;
|
|
852
|
+
|
|
853
|
+
// Emit else expression
|
|
854
|
+
if let Some(else_node) = node.children.get(2) {
|
|
855
|
+
if let Some(else_statements) = else_node.children.first() {
|
|
856
|
+
if !self.source.is_empty() {
|
|
857
|
+
let start = else_statements.location.start_offset;
|
|
858
|
+
let end = else_statements.location.end_offset;
|
|
859
|
+
if let Some(text) = self.source.get(start..end) {
|
|
860
|
+
write!(self.buffer, "{}", text.trim())?;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return Ok(());
|
|
867
|
+
}
|
|
868
|
+
|
|
615
869
|
// Normal if/unless/elsif
|
|
616
870
|
if !is_elsif {
|
|
617
871
|
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
@@ -637,6 +891,8 @@ impl Emitter {
|
|
|
637
891
|
}
|
|
638
892
|
}
|
|
639
893
|
|
|
894
|
+
// Emit trailing comment on same line as if/unless/elsif
|
|
895
|
+
self.emit_trailing_comments(node.location.start_line)?;
|
|
640
896
|
self.buffer.push('\n');
|
|
641
897
|
|
|
642
898
|
// Emit then clause (second child is StatementsNode)
|
|
@@ -760,6 +1016,8 @@ impl Emitter {
|
|
|
760
1016
|
// Emit block parameters if present (|x, y|)
|
|
761
1017
|
self.emit_block_parameters(block_node)?;
|
|
762
1018
|
|
|
1019
|
+
// Emit trailing comment on same line as do |...|
|
|
1020
|
+
self.emit_trailing_comments(block_node.location.start_line)?;
|
|
763
1021
|
self.buffer.push('\n');
|
|
764
1022
|
|
|
765
1023
|
// Find and emit the body (StatementsNode among children)
|
|
@@ -851,6 +1109,20 @@ impl Emitter {
|
|
|
851
1109
|
if let Some(text) = text_owned {
|
|
852
1110
|
self.emit_indent(indent_level)?;
|
|
853
1111
|
write!(self.buffer, "{}", text)?;
|
|
1112
|
+
|
|
1113
|
+
// Mark comments that are strictly inside this node's line range as emitted
|
|
1114
|
+
// (they are included in the source extraction)
|
|
1115
|
+
// Don't mark trailing comments on the last line (they come after the node ends)
|
|
1116
|
+
for (idx, comment) in self.all_comments.iter().enumerate() {
|
|
1117
|
+
if !self.emitted_comment_indices.contains(&idx)
|
|
1118
|
+
&& comment.location.start_line >= node.location.start_line
|
|
1119
|
+
&& comment.location.end_line < node.location.end_line
|
|
1120
|
+
{
|
|
1121
|
+
self.emitted_comment_indices.push(idx);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Emit trailing comments on the same line (after the node ends)
|
|
854
1126
|
self.emit_trailing_comments(node.location.end_line)?;
|
|
855
1127
|
}
|
|
856
1128
|
}
|
|
@@ -871,12 +1143,13 @@ impl Emitter {
|
|
|
871
1143
|
self.emit_indent(indent_level)?;
|
|
872
1144
|
write!(self.buffer, "{}", text)?;
|
|
873
1145
|
|
|
874
|
-
// Mark comments
|
|
1146
|
+
// Mark comments that are strictly inside this node's line range as emitted
|
|
875
1147
|
// (they are included in the source extraction)
|
|
1148
|
+
// Don't mark trailing comments on the last line (they come after the node ends)
|
|
876
1149
|
for (idx, comment) in self.all_comments.iter().enumerate() {
|
|
877
1150
|
if !self.emitted_comment_indices.contains(&idx)
|
|
878
1151
|
&& comment.location.start_line >= node.location.start_line
|
|
879
|
-
&& comment.location.end_line
|
|
1152
|
+
&& comment.location.end_line < node.location.end_line
|
|
880
1153
|
{
|
|
881
1154
|
self.emitted_comment_indices.push(idx);
|
|
882
1155
|
}
|
|
@@ -933,6 +1206,8 @@ impl Emitter {
|
|
|
933
1206
|
}
|
|
934
1207
|
}
|
|
935
1208
|
|
|
1209
|
+
// Emit trailing comment on same line as while/until
|
|
1210
|
+
self.emit_trailing_comments(node.location.start_line)?;
|
|
936
1211
|
self.buffer.push('\n');
|
|
937
1212
|
|
|
938
1213
|
// Emit body - second child (StatementsNode)
|
|
@@ -1001,6 +1276,7 @@ impl Emitter {
|
|
|
1001
1276
|
}
|
|
1002
1277
|
|
|
1003
1278
|
/// Check if node is structural (part of definition syntax, not body)
|
|
1279
|
+
/// These nodes are part of class/module/method definitions and should not be emitted as body
|
|
1004
1280
|
fn is_structural_node(&self, node_type: &NodeType) -> bool {
|
|
1005
1281
|
matches!(
|
|
1006
1282
|
node_type,
|
data/lib/rfmt/prism_bridge.rb
CHANGED
|
@@ -312,14 +312,14 @@ module Rfmt
|
|
|
312
312
|
|
|
313
313
|
case node
|
|
314
314
|
when Prism::ClassNode
|
|
315
|
-
if (name =
|
|
315
|
+
if (name = extract_class_or_module_name(node))
|
|
316
316
|
metadata['name'] = name
|
|
317
317
|
end
|
|
318
318
|
if (superclass = extract_superclass_name(node))
|
|
319
319
|
metadata['superclass'] = superclass
|
|
320
320
|
end
|
|
321
321
|
when Prism::ModuleNode
|
|
322
|
-
if (name =
|
|
322
|
+
if (name = extract_class_or_module_name(node))
|
|
323
323
|
metadata['name'] = name
|
|
324
324
|
end
|
|
325
325
|
when Prism::DefNode
|
|
@@ -327,6 +327,15 @@ module Rfmt
|
|
|
327
327
|
metadata['name'] = name
|
|
328
328
|
end
|
|
329
329
|
metadata['parameters_count'] = extract_parameter_count(node).to_s
|
|
330
|
+
# Check if this is a class method (def self.method_name)
|
|
331
|
+
if node.respond_to?(:receiver) && node.receiver
|
|
332
|
+
receiver = node.receiver
|
|
333
|
+
if receiver.is_a?(Prism::SelfNode)
|
|
334
|
+
metadata['receiver'] = 'self'
|
|
335
|
+
elsif receiver.respond_to?(:slice)
|
|
336
|
+
metadata['receiver'] = receiver.slice
|
|
337
|
+
end
|
|
338
|
+
end
|
|
330
339
|
when Prism::CallNode
|
|
331
340
|
if (name = extract_node_name(node))
|
|
332
341
|
metadata['name'] = name
|
|
@@ -350,6 +359,9 @@ module Rfmt
|
|
|
350
359
|
if (value = extract_literal_value(node))
|
|
351
360
|
metadata['value'] = value
|
|
352
361
|
end
|
|
362
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
363
|
+
# Detect ternary operator: if_keyword_loc is nil for ternary
|
|
364
|
+
metadata['is_ternary'] = node.if_keyword_loc.nil?.to_s if node.respond_to?(:if_keyword_loc)
|
|
353
365
|
end
|
|
354
366
|
|
|
355
367
|
metadata
|
|
@@ -14,6 +14,31 @@ module Rfmt
|
|
|
14
14
|
node.name.to_s
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Extract full name from class or module node (handles namespaced names like Foo::Bar::Baz)
|
|
18
|
+
# @param node [Prism::ClassNode, Prism::ModuleNode] The class or module node
|
|
19
|
+
# @return [String, nil] The full name or nil if not available
|
|
20
|
+
def extract_class_or_module_name(node)
|
|
21
|
+
return nil unless node.respond_to?(:constant_path)
|
|
22
|
+
|
|
23
|
+
cp = node.constant_path
|
|
24
|
+
return node.name.to_s if cp.nil?
|
|
25
|
+
|
|
26
|
+
case cp
|
|
27
|
+
when Prism::ConstantReadNode
|
|
28
|
+
cp.name.to_s
|
|
29
|
+
when Prism::ConstantPathNode
|
|
30
|
+
if cp.respond_to?(:full_name)
|
|
31
|
+
cp.full_name.to_s
|
|
32
|
+
elsif cp.respond_to?(:slice)
|
|
33
|
+
cp.slice
|
|
34
|
+
else
|
|
35
|
+
cp.location.slice
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
node.name.to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
17
42
|
# Extract superclass name from a class node
|
|
18
43
|
# @param class_node [Prism::ClassNode] The class node
|
|
19
44
|
# @return [String, nil] The superclass name or nil if not available
|
data/lib/rfmt/rfmt.so
CHANGED
|
Binary file
|
data/lib/rfmt/version.rb
CHANGED