rfmt 1.2.3 → 1.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5c30419e6083cc4be5e665f6cd4f5d40a725a1790df70ef78cf68638681459c
4
- data.tar.gz: 066f373ebe30238a91b34a3d01acccdfbbd270256f8c0e7ba5c909e280e009b0
3
+ metadata.gz: 67e0ceb39e5630237f92ac2d78fcf2438313dcd883c72ec44066ede9e9868735
4
+ data.tar.gz: 69b818c97c55f1ffc4f09518393c2249b71e0e084207249ba8fa631679d66167
5
5
  SHA512:
6
- metadata.gz: c17af15dfa819de322a14bde2c318f85d7db27f8d2446f1793550dac256e3309bf4cc3b30269c86cf5bd8aef4269f4666040d7e163b078902c4f75197e72424e
7
- data.tar.gz: c3d5a19ed768dcbd8616321dcdfb4374645e933d1eb89e0530683ff28a4f6aec848870f263d94b69a123315c5f505587d183e8c8fca13c6cbc2e406278c8e3c7
6
+ metadata.gz: 2e7cc3e550d2506c0f6691f6b4823f76a2c88e94519d46ca080c2d3b57a7330d5e3df4bca1ab4e18ae1551ea6821c92210638d657997cb6a72631512e47d2bc5
7
+ data.tar.gz: '0491cb8029834d053692a9cc251da952c5bbbf6e38045bcfe3a90dfb2a9ebf31cb3dfb09bfa640b0c22b427d1ab47f0e801ecd44a88c79626f05ab4898d19c64'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.5] - 2026-01-04
4
+
5
+ ### Fixed
6
+ - Fix trailing comments on `end` keyword (e.g., `end # rubocop:disable`)
7
+ - Fix block internal comments being moved outside the block
8
+ - Fix blank line preservation between code and comments inside blocks
9
+
10
+ ## [1.2.4] - 2026-01-04
11
+
12
+ ### Fixed
13
+ - Fix comment indent space handling
14
+
3
15
  ## [1.2.3] - 2026-01-04
4
16
 
5
17
  ### Fixed
data/Cargo.lock CHANGED
@@ -1219,7 +1219,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
1219
1219
 
1220
1220
  [[package]]
1221
1221
  name = "rfmt"
1222
- version = "1.2.3"
1222
+ version = "1.2.5"
1223
1223
  dependencies = [
1224
1224
  "anyhow",
1225
1225
  "clap",
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "1.2.3"
3
+ version = "1.2.5"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -50,8 +50,11 @@ 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
+
53
56
  // Emit any remaining comments that weren't emitted
54
- self.emit_remaining_comments()?;
57
+ self.emit_remaining_comments(last_code_line)?;
55
58
 
56
59
  // Ensure file ends with a newline
57
60
  if !self.buffer.ends_with('\n') {
@@ -61,14 +64,30 @@ impl Emitter {
61
64
  Ok(self.buffer.clone())
62
65
  }
63
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
+
64
79
  /// Emit all comments that haven't been emitted yet
65
- fn emit_remaining_comments(&mut self) -> Result<()> {
66
- let mut last_end_line: Option<usize> = None;
80
+ fn emit_remaining_comments(&mut self, last_code_line: usize) -> Result<()> {
81
+ let mut last_end_line: Option<usize> = Some(last_code_line);
67
82
  for (idx, comment) in self.all_comments.iter().enumerate() {
68
83
  if self.emitted_comment_indices.contains(&idx) {
69
84
  continue;
70
85
  }
71
- // Preserve blank lines between comments
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
72
91
  if let Some(prev_line) = last_end_line {
73
92
  let gap = comment.location.start_line.saturating_sub(prev_line);
74
93
  for _ in 1..gap {
@@ -104,16 +123,37 @@ impl Emitter {
104
123
  }
105
124
 
106
125
  if comment.location.end_line < line {
107
- comments_to_emit.push((idx, comment.text.clone(), comment.location.end_line));
126
+ comments_to_emit.push((
127
+ idx,
128
+ comment.text.clone(),
129
+ comment.location.start_line,
130
+ comment.location.end_line,
131
+ ));
108
132
  }
109
133
  }
110
134
 
135
+ // Sort by start_line to emit in order
136
+ comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
137
+
111
138
  let comments_count = comments_to_emit.len();
112
- for (i, (idx, text, comment_end_line)) in comments_to_emit.into_iter().enumerate() {
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
+
113
152
  writeln!(self.buffer, "{}{}", indent_str, text)?;
114
153
  self.emitted_comment_indices.push(idx);
154
+ last_comment_end_line = Some(comment_end_line);
115
155
 
116
- // Only add blank line after the LAST comment if there was a gap in the original
156
+ // Add blank line after the LAST comment if there was a gap to the code
117
157
  if i == comments_count - 1 && line > comment_end_line + 1 {
118
158
  self.buffer.push('\n');
119
159
  }
@@ -122,6 +162,114 @@ impl Emitter {
122
162
  Ok(())
123
163
  }
124
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
+
224
+ /// Emit comments that are within a given line range, preserving blank lines from prev_line
225
+ fn emit_comments_in_range_with_prev_line(
226
+ &mut self,
227
+ start_line: usize,
228
+ end_line: usize,
229
+ indent_level: usize,
230
+ prev_line: usize,
231
+ ) -> Result<()> {
232
+ let indent_str = match self.config.formatting.indent_style {
233
+ IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
234
+ IndentStyle::Tabs => "\t".repeat(indent_level),
235
+ };
236
+
237
+ let mut comments_to_emit = Vec::new();
238
+ for (idx, comment) in self.all_comments.iter().enumerate() {
239
+ if self.emitted_comment_indices.contains(&idx) {
240
+ continue;
241
+ }
242
+
243
+ if comment.location.start_line >= start_line && comment.location.end_line < end_line {
244
+ comments_to_emit.push((
245
+ idx,
246
+ comment.text.clone(),
247
+ comment.location.start_line,
248
+ comment.location.end_line,
249
+ ));
250
+ }
251
+ }
252
+
253
+ // Sort by start_line to emit in order
254
+ comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
255
+
256
+ let mut last_end_line: usize = prev_line;
257
+
258
+ for (idx, text, comment_start_line, comment_end_line) in comments_to_emit {
259
+ // Preserve blank lines between previous content and this comment
260
+ let gap = comment_start_line.saturating_sub(last_end_line);
261
+ for _ in 1..gap {
262
+ self.buffer.push('\n');
263
+ }
264
+
265
+ writeln!(self.buffer, "{}{}", indent_str, text)?;
266
+ self.emitted_comment_indices.push(idx);
267
+ last_end_line = comment_end_line;
268
+ }
269
+
270
+ Ok(())
271
+ }
272
+
125
273
  /// Emit comments that appear on the same line (trailing comments)
126
274
  fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
127
275
  let mut indices_to_emit = Vec::new();
@@ -249,6 +397,9 @@ impl Emitter {
249
397
  // Emit body (children), but skip structural nodes (class name, superclass)
250
398
  // Use start_line check to properly handle CallNode superclasses like ActiveRecord::Migration[8.0]
251
399
  let class_start_line = node.location.start_line;
400
+ let class_end_line = node.location.end_line;
401
+ let mut has_body_content = false;
402
+
252
403
  for child in &node.children {
253
404
  // Skip nodes on the same line as class definition (name, superclass)
254
405
  if child.location.start_line == class_start_line {
@@ -257,18 +408,25 @@ impl Emitter {
257
408
  if self.is_structural_node(&child.node_type) {
258
409
  continue;
259
410
  }
411
+ has_body_content = true;
260
412
  self.emit_node(child, indent_level + 1)?;
261
413
  }
262
414
 
263
- // Add newline before end if there was body content
264
- if node.children.iter().any(|c| {
265
- c.location.start_line != class_start_line && !self.is_structural_node(&c.node_type)
266
- }) {
415
+ // Emit comments that are inside the class body but not attached to any node
416
+ // These are comments between class_start_line and class_end_line
417
+ self.emit_comments_in_range(class_start_line + 1, class_end_line, indent_level + 1)?;
418
+
419
+ // Add newline before end if there was body content or internal comments
420
+ if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
421
+ && !self.buffer.ends_with('\n')
422
+ {
267
423
  self.buffer.push('\n');
268
424
  }
269
425
 
270
426
  self.emit_indent(indent_level)?;
271
427
  write!(self.buffer, "end")?;
428
+ // Emit trailing comments on end line (e.g., `end # rubocop:disable`)
429
+ self.emit_trailing_comments(node.location.end_line)?;
272
430
 
273
431
  Ok(())
274
432
  }
@@ -289,25 +447,33 @@ impl Emitter {
289
447
  self.emit_trailing_comments(node.location.start_line)?;
290
448
  self.buffer.push('\n');
291
449
 
450
+ let module_start_line = node.location.start_line;
451
+ let module_end_line = node.location.end_line;
452
+ let mut has_body_content = false;
453
+
292
454
  // Emit body (children), but skip structural nodes
293
455
  for child in &node.children {
294
456
  if self.is_structural_node(&child.node_type) {
295
457
  continue;
296
458
  }
459
+ has_body_content = true;
297
460
  self.emit_node(child, indent_level + 1)?;
298
461
  }
299
462
 
300
- // Add newline before end if there was body content
301
- if node
302
- .children
303
- .iter()
304
- .any(|c| !self.is_structural_node(&c.node_type))
463
+ // Emit comments that are inside the module body but not attached to any node
464
+ self.emit_comments_in_range(module_start_line + 1, module_end_line, indent_level + 1)?;
465
+
466
+ // Add newline before end if there was body content or internal comments
467
+ if (has_body_content || self.has_comments_in_range(module_start_line + 1, module_end_line))
468
+ && !self.buffer.ends_with('\n')
305
469
  {
306
470
  self.buffer.push('\n');
307
471
  }
308
472
 
309
473
  self.emit_indent(indent_level)?;
310
474
  write!(self.buffer, "end")?;
475
+ // Emit trailing comments on end line (e.g., `end # rubocop:disable`)
476
+ self.emit_trailing_comments(node.location.end_line)?;
311
477
 
312
478
  Ok(())
313
479
  }
@@ -380,6 +546,8 @@ impl Emitter {
380
546
 
381
547
  self.emit_indent(indent_level)?;
382
548
  write!(self.buffer, "end")?;
549
+ // Emit trailing comments on end line (e.g., `end # rubocop:disable`)
550
+ self.emit_trailing_comments(node.location.end_line)?;
383
551
 
384
552
  Ok(())
385
553
  }
@@ -411,6 +579,8 @@ impl Emitter {
411
579
 
412
580
  self.emit_indent(indent_level)?;
413
581
  write!(self.buffer, "end")?;
582
+ // Emit trailing comments on end line
583
+ self.emit_trailing_comments(node.location.end_line)?;
414
584
  } else {
415
585
  // Implicit begin - emit children directly
416
586
  for (i, child) in node.children.iter().enumerate() {
@@ -463,7 +633,8 @@ impl Emitter {
463
633
  }
464
634
  } else if expect_continuation {
465
635
  // Continuation line after trailing comma or backslash
466
- if !rescue_decl.ends_with(' ') && !rescue_decl.ends_with(',') {
636
+ // Add space after comma or if no trailing space
637
+ if !rescue_decl.ends_with(' ') {
467
638
  rescue_decl.push(' ');
468
639
  }
469
640
  let content = trimmed.trim_end_matches('\\').trim();
@@ -600,6 +771,8 @@ impl Emitter {
600
771
  // Emit "end" keyword
601
772
  self.emit_indent(indent_level)?;
602
773
  write!(self.buffer, "end")?;
774
+ // Emit trailing comments on end line
775
+ self.emit_trailing_comments(node.location.end_line)?;
603
776
 
604
777
  Ok(())
605
778
  }
@@ -817,6 +990,8 @@ impl Emitter {
817
990
  if !is_elsif {
818
991
  self.emit_indent(indent_level)?;
819
992
  write!(self.buffer, "end")?;
993
+ // Emit trailing comments on end line
994
+ self.emit_trailing_comments(node.location.end_line)?;
820
995
  }
821
996
 
822
997
  Ok(())
@@ -907,17 +1082,46 @@ impl Emitter {
907
1082
  self.buffer.push('\n');
908
1083
 
909
1084
  // Find and emit the body (StatementsNode among children)
1085
+ let block_start_line = block_node.location.start_line;
1086
+ let block_end_line = block_node.location.end_line;
1087
+ let mut last_stmt_end_line = block_start_line;
1088
+
910
1089
  for child in &block_node.children {
911
1090
  if matches!(child.node_type, NodeType::StatementsNode) {
912
1091
  self.emit_statements(child, indent_level + 1)?;
1092
+ // Track the last statement's end line for blank line preservation
1093
+ if let Some(last_child) = child.children.last() {
1094
+ last_stmt_end_line = last_child.location.end_line;
1095
+ }
913
1096
  self.buffer.push('\n');
914
1097
  break;
915
1098
  }
916
1099
  }
917
1100
 
1101
+ // Emit comments that are inside the block but not attached to any node
1102
+ // (comments between last statement and 'end')
1103
+ let had_internal_comments =
1104
+ self.has_comments_in_range(block_start_line + 1, block_end_line);
1105
+ if had_internal_comments {
1106
+ // Preserve blank line between last statement and first comment
1107
+ self.emit_comments_in_range_with_prev_line(
1108
+ block_start_line + 1,
1109
+ block_end_line,
1110
+ indent_level + 1,
1111
+ last_stmt_end_line,
1112
+ )?;
1113
+ }
1114
+
1115
+ // Add newline if there were internal comments
1116
+ if had_internal_comments && !self.buffer.ends_with('\n') {
1117
+ self.buffer.push('\n');
1118
+ }
1119
+
918
1120
  // Emit 'end'
919
1121
  self.emit_indent(indent_level)?;
920
1122
  write!(self.buffer, "end")?;
1123
+ // Emit trailing comments on end line
1124
+ self.emit_trailing_comments(block_end_line)?;
921
1125
 
922
1126
  Ok(())
923
1127
  }
@@ -995,6 +1199,20 @@ impl Emitter {
995
1199
  if let Some(text) = text_owned {
996
1200
  self.emit_indent(indent_level)?;
997
1201
  write!(self.buffer, "{}", text)?;
1202
+
1203
+ // Mark comments that are strictly inside this node's line range as emitted
1204
+ // (they are included in the source extraction)
1205
+ // Don't mark trailing comments on the last line (they come after the node ends)
1206
+ for (idx, comment) in self.all_comments.iter().enumerate() {
1207
+ if !self.emitted_comment_indices.contains(&idx)
1208
+ && comment.location.start_line >= node.location.start_line
1209
+ && comment.location.end_line < node.location.end_line
1210
+ {
1211
+ self.emitted_comment_indices.push(idx);
1212
+ }
1213
+ }
1214
+
1215
+ // Emit trailing comments on the same line (after the node ends)
998
1216
  self.emit_trailing_comments(node.location.end_line)?;
999
1217
  }
1000
1218
  }
@@ -1015,12 +1233,13 @@ impl Emitter {
1015
1233
  self.emit_indent(indent_level)?;
1016
1234
  write!(self.buffer, "{}", text)?;
1017
1235
 
1018
- // Mark comments within this node's range as emitted
1236
+ // Mark comments that are strictly inside this node's line range as emitted
1019
1237
  // (they are included in the source extraction)
1238
+ // Don't mark trailing comments on the last line (they come after the node ends)
1020
1239
  for (idx, comment) in self.all_comments.iter().enumerate() {
1021
1240
  if !self.emitted_comment_indices.contains(&idx)
1022
1241
  && comment.location.start_line >= node.location.start_line
1023
- && comment.location.end_line <= node.location.end_line
1242
+ && comment.location.end_line < node.location.end_line
1024
1243
  {
1025
1244
  self.emitted_comment_indices.push(idx);
1026
1245
  }
@@ -1091,6 +1310,8 @@ impl Emitter {
1091
1310
 
1092
1311
  self.emit_indent(indent_level)?;
1093
1312
  write!(self.buffer, "end")?;
1313
+ // Emit trailing comments on end line
1314
+ self.emit_trailing_comments(node.location.end_line)?;
1094
1315
 
1095
1316
  Ok(())
1096
1317
  }
@@ -1142,6 +1363,8 @@ impl Emitter {
1142
1363
 
1143
1364
  self.emit_indent(indent_level)?;
1144
1365
  write!(self.buffer, "end")?;
1366
+ // Emit trailing comments on end line
1367
+ self.emit_trailing_comments(node.location.end_line)?;
1145
1368
 
1146
1369
  Ok(())
1147
1370
  }
data/lib/rfmt/rfmt.so CHANGED
Binary file
data/lib/rfmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rfmt
4
- VERSION = '1.2.3'
4
+ VERSION = '1.2.5'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-03 00:00:00.000000000 Z
11
+ date: 2026-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys