rfmt 1.4.0 → 1.5.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 +25 -0
- data/Cargo.lock +7 -7
- data/ext/rfmt/Cargo.toml +3 -3
- data/ext/rfmt/src/emitter/mod.rs +208 -92
- data/ext/rfmt/src/lib.rs +3 -3
- data/lib/rfmt/prism_bridge.rb +33 -3
- data/lib/rfmt/version.rb +1 -1
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2df39db6eb525d4ca6bc42d55e01057e5a12772f0b61a7a8e3101e3a8fff76ec
|
|
4
|
+
data.tar.gz: b34bbf251457c8eb0d9174497c124f71d6a7b098ad506712acaec54812bcc674
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d341d108c8875f683a2ef3654e6e540b1fe9840a4f908abc75725da544c0b8be2b27cf5534cdf634eb2e9eaaa36bed0df9500f978a6fffc95ec7379e2485cf3
|
|
7
|
+
data.tar.gz: 9cbce53637c758998c79c77af60d10130dd293dac67c4a7918d882e816d848ef525400bd6fcebc030523a2407e08140dc1bc9227cbd9c1cfc3f579f0e5b477be
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-01-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Docker Compose test environment setup (#84)
|
|
7
|
+
- Support for `then` expression emission (#80)
|
|
8
|
+
- CI support for Ruby 3.4 and Ruby 4 (#82)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Upgrade Magnus (Rust-Ruby FFI library) (#83)
|
|
12
|
+
- Optimize Docker build with multi-stage and caching (#84)
|
|
13
|
+
- Upgrade unicode-emoji dependency
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Preserve heredoc content and closing identifier (#81)
|
|
17
|
+
- Fix `rescue`/`ensure` clauses being deleted inside `do...end` blocks (#78)
|
|
18
|
+
- Fix inline comment node handling (#77)
|
|
19
|
+
- Fix BTreeMap range error (Issue #71)
|
|
20
|
+
|
|
21
|
+
## [1.4.1] - 2026-01-17
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Fixed comment positioning issue where standalone comments before `end` statements were incorrectly attached to previous code lines
|
|
25
|
+
- Improved comment semantic preservation to maintain developer's original placement intent
|
|
26
|
+
- Enhanced standalone comment detection logic to distinguish between inline and independent comments
|
|
27
|
+
|
|
3
28
|
## [1.4.0] - 2026-01-17
|
|
4
29
|
|
|
5
30
|
### Added
|
data/Cargo.lock
CHANGED
|
@@ -838,9 +838,9 @@ dependencies = [
|
|
|
838
838
|
|
|
839
839
|
[[package]]
|
|
840
840
|
name = "magnus"
|
|
841
|
-
version = "0.
|
|
841
|
+
version = "0.8.2"
|
|
842
842
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
843
|
-
checksum = "
|
|
843
|
+
checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
|
|
844
844
|
dependencies = [
|
|
845
845
|
"magnus-macros",
|
|
846
846
|
"rb-sys",
|
|
@@ -850,9 +850,9 @@ dependencies = [
|
|
|
850
850
|
|
|
851
851
|
[[package]]
|
|
852
852
|
name = "magnus-macros"
|
|
853
|
-
version = "0.
|
|
853
|
+
version = "0.8.0"
|
|
854
854
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
855
|
-
checksum = "
|
|
855
|
+
checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
|
|
856
856
|
dependencies = [
|
|
857
857
|
"proc-macro2",
|
|
858
858
|
"quote",
|
|
@@ -1168,9 +1168,9 @@ dependencies = [
|
|
|
1168
1168
|
|
|
1169
1169
|
[[package]]
|
|
1170
1170
|
name = "rb-sys-env"
|
|
1171
|
-
version = "0.
|
|
1171
|
+
version = "0.2.3"
|
|
1172
1172
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1173
|
-
checksum = "
|
|
1173
|
+
checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
|
|
1174
1174
|
|
|
1175
1175
|
[[package]]
|
|
1176
1176
|
name = "redox_users"
|
|
@@ -1214,7 +1214,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
|
|
1214
1214
|
|
|
1215
1215
|
[[package]]
|
|
1216
1216
|
name = "rfmt"
|
|
1217
|
-
version = "1.
|
|
1217
|
+
version = "1.5.0"
|
|
1218
1218
|
dependencies = [
|
|
1219
1219
|
"anyhow",
|
|
1220
1220
|
"clap",
|
data/ext/rfmt/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "rfmt"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.5.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -12,8 +12,8 @@ crate-type = ["cdylib"]
|
|
|
12
12
|
|
|
13
13
|
[dependencies]
|
|
14
14
|
# Ruby FFI
|
|
15
|
-
magnus = { version = "0.
|
|
16
|
-
rb-sys = "0.9.
|
|
15
|
+
magnus = { version = "0.8.2" }
|
|
16
|
+
rb-sys = "0.9.124"
|
|
17
17
|
|
|
18
18
|
# Serialization
|
|
19
19
|
serde = { version = "1.0", features = ["derive"] }
|
data/ext/rfmt/src/emitter/mod.rs
CHANGED
|
@@ -144,6 +144,10 @@ impl Emitter {
|
|
|
144
144
|
/// Get comment indices in the given line range [start_line, end_line)
|
|
145
145
|
/// Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
|
|
146
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
|
+
}
|
|
147
151
|
self.comments_by_line
|
|
148
152
|
.range(start_line..end_line)
|
|
149
153
|
.flat_map(|(_, indices)| indices.iter().copied())
|
|
@@ -228,6 +232,10 @@ impl Emitter {
|
|
|
228
232
|
/// Check if there are any unemitted comments in the given line range
|
|
229
233
|
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
230
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
|
+
}
|
|
231
239
|
self.comments_by_line
|
|
232
240
|
.range(start_line..end_line)
|
|
233
241
|
.flat_map(|(_, indices)| indices.iter())
|
|
@@ -237,34 +245,58 @@ impl Emitter {
|
|
|
237
245
|
})
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
/// Emit comments that
|
|
241
|
-
///
|
|
242
|
-
fn
|
|
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(
|
|
243
251
|
&mut self,
|
|
244
|
-
|
|
245
|
-
|
|
252
|
+
construct_start_line: usize,
|
|
253
|
+
construct_end_line: usize,
|
|
246
254
|
indent_level: usize,
|
|
247
255
|
) -> Result<()> {
|
|
248
256
|
self.ensure_indent_cache(indent_level);
|
|
249
257
|
|
|
250
|
-
|
|
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);
|
|
251
266
|
|
|
252
267
|
let mut comments_to_emit: Vec<_> = indices
|
|
253
268
|
.into_iter()
|
|
254
|
-
.filter(|&idx|
|
|
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
|
+
})
|
|
255
276
|
.map(|idx| {
|
|
256
277
|
let comment = &self.all_comments[idx];
|
|
257
278
|
(idx, comment.location.start_line, comment.location.end_line)
|
|
258
279
|
})
|
|
259
280
|
.collect();
|
|
260
281
|
|
|
282
|
+
if comments_to_emit.is_empty() {
|
|
283
|
+
return Ok(());
|
|
284
|
+
}
|
|
285
|
+
|
|
261
286
|
comments_to_emit.sort_by_key(|(_, start, _)| *start);
|
|
262
287
|
|
|
263
|
-
|
|
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;
|
|
264
294
|
|
|
295
|
+
// Emit comments while preserving their exact line positioning
|
|
265
296
|
for (idx, comment_start_line, comment_end_line) in comments_to_emit {
|
|
266
|
-
|
|
267
|
-
|
|
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);
|
|
268
300
|
for _ in 1..gap {
|
|
269
301
|
self.buffer.push('\n');
|
|
270
302
|
}
|
|
@@ -276,12 +308,75 @@ impl Emitter {
|
|
|
276
308
|
&self.indent_cache[indent_level], &self.all_comments[idx].text
|
|
277
309
|
)?;
|
|
278
310
|
self.emitted_comment_indices.insert(idx);
|
|
279
|
-
|
|
311
|
+
last_emitted_line = Some(comment_end_line);
|
|
280
312
|
}
|
|
281
313
|
|
|
282
314
|
Ok(())
|
|
283
315
|
}
|
|
284
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
|
+
|
|
285
380
|
/// Emit comments that are within a given line range, preserving blank lines from prev_line
|
|
286
381
|
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
287
382
|
fn emit_comments_in_range_with_prev_line(
|
|
@@ -464,9 +559,8 @@ impl Emitter {
|
|
|
464
559
|
self.emit_node(child, indent_level + 1)?;
|
|
465
560
|
}
|
|
466
561
|
|
|
467
|
-
// Emit comments that
|
|
468
|
-
|
|
469
|
-
self.emit_comments_in_range(class_start_line + 1, class_end_line, indent_level + 1)?;
|
|
562
|
+
// Emit comments that appear before the end statement while preserving their position
|
|
563
|
+
self.emit_comments_before_end(class_start_line, class_end_line, indent_level + 1)?;
|
|
470
564
|
|
|
471
565
|
// Add newline before end if there was body content or internal comments
|
|
472
566
|
if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
|
|
@@ -512,8 +606,8 @@ impl Emitter {
|
|
|
512
606
|
self.emit_node(child, indent_level + 1)?;
|
|
513
607
|
}
|
|
514
608
|
|
|
515
|
-
// Emit comments that
|
|
516
|
-
self.
|
|
609
|
+
// Emit comments that appear before the end statement while preserving their position
|
|
610
|
+
self.emit_comments_before_end(module_start_line, module_end_line, indent_level + 1)?;
|
|
517
611
|
|
|
518
612
|
// Add newline before end if there was body content or internal comments
|
|
519
613
|
if (has_body_content || self.has_comments_in_range(module_start_line + 1, module_end_line))
|
|
@@ -573,6 +667,13 @@ impl Emitter {
|
|
|
573
667
|
self.emit_node(child, indent_level + 1)?;
|
|
574
668
|
}
|
|
575
669
|
|
|
670
|
+
// Emit comments that appear before the end statement while preserving their position
|
|
671
|
+
self.emit_comments_before_end(
|
|
672
|
+
node.location.start_line,
|
|
673
|
+
node.location.end_line,
|
|
674
|
+
indent_level + 1,
|
|
675
|
+
)?;
|
|
676
|
+
|
|
576
677
|
// Add newline before end if there was body content
|
|
577
678
|
if node
|
|
578
679
|
.children
|
|
@@ -831,25 +932,30 @@ impl Emitter {
|
|
|
831
932
|
|
|
832
933
|
// Emit conditions with comma separator
|
|
833
934
|
for (i, cond) in conditions.iter().enumerate() {
|
|
834
|
-
|
|
835
|
-
let end = cond.location.end_offset;
|
|
836
|
-
if let Some(text) = self.source.get(start..end) {
|
|
837
|
-
write!(self.buffer, "{}", text)?;
|
|
838
|
-
}
|
|
935
|
+
self.write_source_text(cond)?;
|
|
839
936
|
if i < conditions.len() - 1 {
|
|
840
937
|
write!(self.buffer, ", ")?;
|
|
841
938
|
}
|
|
842
939
|
}
|
|
843
940
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
// Emit statements body
|
|
847
|
-
if let Some(statements) = node
|
|
941
|
+
let statements = node
|
|
848
942
|
.children
|
|
849
943
|
.iter()
|
|
850
|
-
.find(|c| matches!(c.node_type, NodeType::StatementsNode))
|
|
851
|
-
|
|
852
|
-
|
|
944
|
+
.find(|c| matches!(c.node_type, NodeType::StatementsNode));
|
|
945
|
+
|
|
946
|
+
if self.is_single_line(node) {
|
|
947
|
+
// Inline style: when X then Y
|
|
948
|
+
if let Some(statements) = statements {
|
|
949
|
+
write!(self.buffer, " then ")?;
|
|
950
|
+
self.write_source_text(statements)?;
|
|
951
|
+
}
|
|
952
|
+
} else {
|
|
953
|
+
// Multi-line style: when X\n Y
|
|
954
|
+
self.buffer.push('\n');
|
|
955
|
+
|
|
956
|
+
if let Some(statements) = statements {
|
|
957
|
+
self.emit_statements(statements, indent_level + 1)?;
|
|
958
|
+
}
|
|
853
959
|
}
|
|
854
960
|
|
|
855
961
|
Ok(())
|
|
@@ -883,14 +989,7 @@ impl Emitter {
|
|
|
883
989
|
// Emit statement
|
|
884
990
|
if let Some(statements) = node.children.get(1) {
|
|
885
991
|
if matches!(statements.node_type, NodeType::StatementsNode) {
|
|
886
|
-
|
|
887
|
-
if !self.source.is_empty() {
|
|
888
|
-
let start = statements.location.start_offset;
|
|
889
|
-
let end = statements.location.end_offset;
|
|
890
|
-
if let Some(text) = self.source.get(start..end) {
|
|
891
|
-
write!(self.buffer, "{}", text.trim())?;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
992
|
+
self.write_source_text_trimmed(statements)?;
|
|
894
993
|
}
|
|
895
994
|
}
|
|
896
995
|
|
|
@@ -898,13 +997,7 @@ impl Emitter {
|
|
|
898
997
|
|
|
899
998
|
// Emit condition
|
|
900
999
|
if let Some(predicate) = node.children.first() {
|
|
901
|
-
|
|
902
|
-
let start = predicate.location.start_offset;
|
|
903
|
-
let end = predicate.location.end_offset;
|
|
904
|
-
if let Some(text) = self.source.get(start..end) {
|
|
905
|
-
write!(self.buffer, "{}", text)?;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
1000
|
+
self.write_source_text(predicate)?;
|
|
908
1001
|
}
|
|
909
1002
|
|
|
910
1003
|
return Ok(());
|
|
@@ -923,26 +1016,14 @@ impl Emitter {
|
|
|
923
1016
|
|
|
924
1017
|
// Emit condition
|
|
925
1018
|
if let Some(predicate) = node.children.first() {
|
|
926
|
-
|
|
927
|
-
let start = predicate.location.start_offset;
|
|
928
|
-
let end = predicate.location.end_offset;
|
|
929
|
-
if let Some(text) = self.source.get(start..end) {
|
|
930
|
-
write!(self.buffer, "{}", text)?;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
1019
|
+
self.write_source_text(predicate)?;
|
|
933
1020
|
}
|
|
934
1021
|
|
|
935
1022
|
write!(self.buffer, " ? ")?;
|
|
936
1023
|
|
|
937
1024
|
// Emit then expression
|
|
938
1025
|
if let Some(statements) = node.children.get(1) {
|
|
939
|
-
|
|
940
|
-
let start = statements.location.start_offset;
|
|
941
|
-
let end = statements.location.end_offset;
|
|
942
|
-
if let Some(text) = self.source.get(start..end) {
|
|
943
|
-
write!(self.buffer, "{}", text.trim())?;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
1026
|
+
self.write_source_text_trimmed(statements)?;
|
|
946
1027
|
}
|
|
947
1028
|
|
|
948
1029
|
write!(self.buffer, " : ")?;
|
|
@@ -950,19 +1031,40 @@ impl Emitter {
|
|
|
950
1031
|
// Emit else expression
|
|
951
1032
|
if let Some(else_node) = node.children.get(2) {
|
|
952
1033
|
if let Some(else_statements) = else_node.children.first() {
|
|
953
|
-
|
|
954
|
-
let start = else_statements.location.start_offset;
|
|
955
|
-
let end = else_statements.location.end_offset;
|
|
956
|
-
if let Some(text) = self.source.get(start..end) {
|
|
957
|
-
write!(self.buffer, "{}", text.trim())?;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
1034
|
+
self.write_source_text_trimmed(else_statements)?;
|
|
960
1035
|
}
|
|
961
1036
|
}
|
|
962
1037
|
|
|
963
1038
|
return Ok(());
|
|
964
1039
|
}
|
|
965
1040
|
|
|
1041
|
+
// Check for inline then style: "if true then 1 end"
|
|
1042
|
+
// Single line, not postfix, not ternary, no else clause
|
|
1043
|
+
let is_inline_then =
|
|
1044
|
+
!is_elsif && self.is_single_line(node) && node.children.get(2).is_none();
|
|
1045
|
+
|
|
1046
|
+
if is_inline_then {
|
|
1047
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
1048
|
+
self.emit_indent(indent_level)?;
|
|
1049
|
+
write!(self.buffer, "{} ", keyword)?;
|
|
1050
|
+
|
|
1051
|
+
// Emit condition
|
|
1052
|
+
if let Some(predicate) = node.children.first() {
|
|
1053
|
+
self.write_source_text(predicate)?;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
write!(self.buffer, " then ")?;
|
|
1057
|
+
|
|
1058
|
+
// Emit statement
|
|
1059
|
+
if let Some(statements) = node.children.get(1) {
|
|
1060
|
+
self.write_source_text_trimmed(statements)?;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
write!(self.buffer, " end")?;
|
|
1064
|
+
self.emit_trailing_comments(node.location.end_line)?;
|
|
1065
|
+
return Ok(());
|
|
1066
|
+
}
|
|
1067
|
+
|
|
966
1068
|
// Normal if/unless/elsif
|
|
967
1069
|
if !is_elsif {
|
|
968
1070
|
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
@@ -978,14 +1080,7 @@ impl Emitter {
|
|
|
978
1080
|
|
|
979
1081
|
// Emit predicate (condition) - first child
|
|
980
1082
|
if let Some(predicate) = node.children.first() {
|
|
981
|
-
|
|
982
|
-
if !self.source.is_empty() {
|
|
983
|
-
let start = predicate.location.start_offset;
|
|
984
|
-
let end = predicate.location.end_offset;
|
|
985
|
-
if let Some(text) = self.source.get(start..end) {
|
|
986
|
-
write!(self.buffer, "{}", text)?;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
1083
|
+
self.write_source_text(predicate)?;
|
|
989
1084
|
}
|
|
990
1085
|
|
|
991
1086
|
// Emit trailing comment on same line as if/unless/elsif
|
|
@@ -1125,14 +1220,27 @@ impl Emitter {
|
|
|
1125
1220
|
let mut last_stmt_end_line = block_start_line;
|
|
1126
1221
|
|
|
1127
1222
|
for child in &block_node.children {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1223
|
+
match &child.node_type {
|
|
1224
|
+
NodeType::StatementsNode => {
|
|
1225
|
+
self.emit_statements(child, indent_level + 1)?;
|
|
1226
|
+
// Track the last statement's end line for blank line preservation
|
|
1227
|
+
if let Some(last_child) = child.children.last() {
|
|
1228
|
+
last_stmt_end_line = last_child.location.end_line;
|
|
1229
|
+
}
|
|
1230
|
+
self.buffer.push('\n');
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
NodeType::BeginNode => {
|
|
1234
|
+
// Block with rescue/ensure/else - delegate to emit_begin
|
|
1235
|
+
// which handles implicit begin (no "begin" keyword)
|
|
1236
|
+
self.emit_begin(child, indent_level + 1)?;
|
|
1237
|
+
self.buffer.push('\n');
|
|
1238
|
+
last_stmt_end_line = child.location.end_line;
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
_ => {
|
|
1242
|
+
// Skip parameter nodes
|
|
1133
1243
|
}
|
|
1134
|
-
self.buffer.push('\n');
|
|
1135
|
-
break;
|
|
1136
1244
|
}
|
|
1137
1245
|
}
|
|
1138
1246
|
|
|
@@ -1168,6 +1276,7 @@ impl Emitter {
|
|
|
1168
1276
|
fn emit_brace_block(&mut self, block_node: &Node, indent_level: usize) -> Result<()> {
|
|
1169
1277
|
// Determine if block should be inline or multiline
|
|
1170
1278
|
let is_multiline = block_node.location.start_line != block_node.location.end_line;
|
|
1279
|
+
let block_end_line = block_node.location.end_line;
|
|
1171
1280
|
|
|
1172
1281
|
if is_multiline {
|
|
1173
1282
|
// Multiline brace block
|
|
@@ -1186,6 +1295,7 @@ impl Emitter {
|
|
|
1186
1295
|
|
|
1187
1296
|
self.emit_indent(indent_level)?;
|
|
1188
1297
|
write!(self.buffer, "}}")?;
|
|
1298
|
+
self.emit_trailing_comments(block_end_line)?;
|
|
1189
1299
|
} else {
|
|
1190
1300
|
// Inline brace block - extract from source to preserve spacing
|
|
1191
1301
|
write!(self.buffer, " ")?;
|
|
@@ -1195,6 +1305,7 @@ impl Emitter {
|
|
|
1195
1305
|
{
|
|
1196
1306
|
write!(self.buffer, "{}", text)?;
|
|
1197
1307
|
}
|
|
1308
|
+
self.emit_trailing_comments(block_end_line)?;
|
|
1198
1309
|
}
|
|
1199
1310
|
|
|
1200
1311
|
Ok(())
|
|
@@ -1457,8 +1568,8 @@ impl Emitter {
|
|
|
1457
1568
|
}
|
|
1458
1569
|
}
|
|
1459
1570
|
|
|
1460
|
-
// Emit comments
|
|
1461
|
-
self.
|
|
1571
|
+
// Emit comments that appear before the end statement while preserving their position
|
|
1572
|
+
self.emit_comments_before_end(class_start_line, class_end_line, indent_level + 1)?;
|
|
1462
1573
|
|
|
1463
1574
|
// Add newline before end if there was body content
|
|
1464
1575
|
if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
|
|
@@ -1503,7 +1614,6 @@ impl Emitter {
|
|
|
1503
1614
|
match &child.node_type {
|
|
1504
1615
|
NodeType::InNode => {
|
|
1505
1616
|
self.emit_in(child, indent_level)?;
|
|
1506
|
-
self.buffer.push('\n');
|
|
1507
1617
|
}
|
|
1508
1618
|
NodeType::ElseNode => {
|
|
1509
1619
|
self.emit_indent(indent_level)?;
|
|
@@ -1539,19 +1649,25 @@ impl Emitter {
|
|
|
1539
1649
|
|
|
1540
1650
|
// First child is the pattern
|
|
1541
1651
|
if let Some(pattern) = node.children.first() {
|
|
1542
|
-
|
|
1543
|
-
let end = pattern.location.end_offset;
|
|
1544
|
-
if let Some(text) = self.source.get(start..end) {
|
|
1545
|
-
write!(self.buffer, "{}", text)?;
|
|
1546
|
-
}
|
|
1652
|
+
self.write_source_text(pattern)?;
|
|
1547
1653
|
}
|
|
1548
1654
|
|
|
1549
|
-
self.
|
|
1655
|
+
if self.is_single_line(node) {
|
|
1656
|
+
// Inline style: in X then Y
|
|
1657
|
+
if let Some(statements) = node.children.get(1) {
|
|
1658
|
+
write!(self.buffer, " then ")?;
|
|
1659
|
+
self.write_source_text(statements)?;
|
|
1660
|
+
}
|
|
1661
|
+
self.buffer.push('\n');
|
|
1662
|
+
} else {
|
|
1663
|
+
// Multi-line style: in X\n Y
|
|
1664
|
+
self.buffer.push('\n');
|
|
1550
1665
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1666
|
+
// Second child is the statements body
|
|
1667
|
+
if let Some(statements) = node.children.get(1) {
|
|
1668
|
+
if matches!(statements.node_type, NodeType::StatementsNode) {
|
|
1669
|
+
self.emit_statements(statements, indent_level + 1)?;
|
|
1670
|
+
}
|
|
1555
1671
|
}
|
|
1556
1672
|
}
|
|
1557
1673
|
|
data/ext/rfmt/src/lib.rs
CHANGED
|
@@ -10,7 +10,7 @@ use policy::SecurityPolicy;
|
|
|
10
10
|
|
|
11
11
|
use config::Config;
|
|
12
12
|
use emitter::Emitter;
|
|
13
|
-
use magnus::{
|
|
13
|
+
use magnus::{function, prelude::*, Error, Ruby};
|
|
14
14
|
use parser::{PrismAdapter, RubyParser};
|
|
15
15
|
|
|
16
16
|
fn format_ruby_code(ruby: &Ruby, source: String, json: String) -> Result<String, Error> {
|
|
@@ -45,10 +45,10 @@ fn rust_version() -> String {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
#[magnus::init]
|
|
48
|
-
fn init(
|
|
48
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
49
49
|
logging::RfmtLogger::init();
|
|
50
50
|
|
|
51
|
-
let module = define_module("Rfmt")?;
|
|
51
|
+
let module = ruby.define_module("Rfmt")?;
|
|
52
52
|
|
|
53
53
|
module.define_singleton_method("format_code", function!(format_ruby_code, 2))?;
|
|
54
54
|
module.define_singleton_method("parse_to_json", function!(parse_to_json, 1))?;
|
data/lib/rfmt/prism_bridge.rb
CHANGED
|
@@ -108,13 +108,42 @@ module Rfmt
|
|
|
108
108
|
# Extract location information from node
|
|
109
109
|
def self.extract_location(node)
|
|
110
110
|
loc = node.location
|
|
111
|
+
|
|
112
|
+
# For heredoc nodes, the location only covers the opening tag (<<~CSV)
|
|
113
|
+
# We need to find the maximum end_offset including closing_loc
|
|
114
|
+
end_offset = loc.end_offset
|
|
115
|
+
end_line = loc.end_line
|
|
116
|
+
end_column = loc.end_column
|
|
117
|
+
|
|
118
|
+
# Check this node's closing_loc
|
|
119
|
+
if node.respond_to?(:closing_loc) && node.closing_loc
|
|
120
|
+
closing = node.closing_loc
|
|
121
|
+
if closing.end_offset > end_offset
|
|
122
|
+
end_offset = closing.end_offset
|
|
123
|
+
end_line = closing.end_line
|
|
124
|
+
end_column = closing.end_column
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check child nodes for heredoc (e.g., LocalVariableWriteNode containing StringNode)
|
|
129
|
+
node.child_nodes.compact.each do |child|
|
|
130
|
+
next unless child.respond_to?(:closing_loc) && child.closing_loc
|
|
131
|
+
|
|
132
|
+
closing = child.closing_loc
|
|
133
|
+
next unless closing.end_offset > end_offset
|
|
134
|
+
|
|
135
|
+
end_offset = closing.end_offset
|
|
136
|
+
end_line = closing.end_line
|
|
137
|
+
end_column = closing.end_column
|
|
138
|
+
end
|
|
139
|
+
|
|
111
140
|
{
|
|
112
141
|
start_line: loc.start_line,
|
|
113
142
|
start_column: loc.start_column,
|
|
114
|
-
end_line:
|
|
115
|
-
end_column:
|
|
143
|
+
end_line: end_line,
|
|
144
|
+
end_column: end_column,
|
|
116
145
|
start_offset: loc.start_offset,
|
|
117
|
-
end_offset:
|
|
146
|
+
end_offset: end_offset
|
|
118
147
|
}
|
|
119
148
|
end
|
|
120
149
|
|
|
@@ -176,6 +205,7 @@ module Rfmt
|
|
|
176
205
|
[
|
|
177
206
|
node.statements,
|
|
178
207
|
node.rescue_clause,
|
|
208
|
+
node.else_clause,
|
|
179
209
|
node.ensure_clause
|
|
180
210
|
].compact
|
|
181
211
|
when Prism::EnsureNode
|
data/lib/rfmt/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rfmt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fujitani sora
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: diff-lcs
|
|
@@ -113,7 +112,6 @@ metadata:
|
|
|
113
112
|
source_code_uri: https://github.com/fs0414/rfmt
|
|
114
113
|
changelog_uri: https://github.com/fs0414/rfmt/releases
|
|
115
114
|
ruby_lsp_addon: 'true'
|
|
116
|
-
post_install_message:
|
|
117
115
|
rdoc_options: []
|
|
118
116
|
require_paths:
|
|
119
117
|
- lib
|
|
@@ -128,8 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
128
126
|
- !ruby/object:Gem::Version
|
|
129
127
|
version: 3.0.0
|
|
130
128
|
requirements: []
|
|
131
|
-
rubygems_version: 3.
|
|
132
|
-
signing_key:
|
|
129
|
+
rubygems_version: 3.6.9
|
|
133
130
|
specification_version: 4
|
|
134
131
|
summary: Ruby Formatter impl Rust lang.
|
|
135
132
|
test_files: []
|