rfmt 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7db146878fe1bbf37f4211f11ded67e98ebfb1830989a4fb64fbe1cc51a72fc7
4
- data.tar.gz: bb87cfab19f7513ddd99b939c03286827687a5da6071c317390dc990924dbe05
3
+ metadata.gz: 21876b488a79df7b3a136bfc563ea91312bc86569205b4349b5ccc0371338071
4
+ data.tar.gz: 7896e3c94fe83d0de8a3a4bebd9d10e0f277f92bfd069384ff05c4cb524d57b5
5
5
  SHA512:
6
- metadata.gz: 2018f31f65afd41842a51e21290448a753784b840dd67f9d8e7e41d223f509227143b18a15309b4a3cf19543b8638ce28045d0c1a68cf97dda8781811b058657
7
- data.tar.gz: 888d036c08f29f166d1089a2cce27d9c456d57c83fbbd1c518142bdfaa15b32c551ab1ccda12c97bd48c9ddf63f57545ef47b6cbdcf8a774f8ea4eb1e17d1729
6
+ metadata.gz: e50633a81f18a263109deac74be48dbb067f39a1139d166d2e6d5d02c271f7dba232ad5512d8a63d7a571c23dc246ee8d41c82be2e477493c89ed92b7949dd19
7
+ data.tar.gz: eae8574579a5b30cef07472ef90ce854162282c84bb379c507796ee2984f5f6ee2004ec82a9e8100c04b5e047b0e8a4894564ada404f789e2180d7302f5842fc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.0] - 2026-01-04
4
+
5
+ ### Added
6
+ - Loop node types support (`for`, `while`, `until`)
7
+ - Case/When statement support
8
+ - Ensure and Lambda node support
9
+ - Begin/End block handling for explicit `begin...end` blocks
10
+ - High-priority node types support
11
+ - Medium-priority node types support
12
+ - Prism supported node viewer task
13
+
14
+ ### Changed
15
+ - Consolidated and simplified test suite
16
+
17
+ ### Fixed
18
+ - Exclude `.DS_Store` from Git tracking (@topi0247)
19
+ - Repository URL changed from `fujitanisora` to `fs0414` (@topi0247)
20
+ - End line space handling
21
+ - Comment location fix
22
+ - End expression indent fix
23
+ - Begin formatting fix
24
+
3
25
  ## [1.1.0] - 2025-12-12
4
26
 
5
27
  ### Added
data/Cargo.lock CHANGED
@@ -1219,7 +1219,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
1219
1219
 
1220
1220
  [[package]]
1221
1221
  name = "rfmt"
1222
- version = "1.0.0"
1222
+ version = "1.2.0"
1223
1223
  dependencies = [
1224
1224
  "anyhow",
1225
1225
  "clap",
data/README.md CHANGED
@@ -102,7 +102,7 @@ bundle install
102
102
  ### From Source
103
103
 
104
104
  ```bash
105
- git clone https://github.com/fujitanisora/rfmt.git
105
+ git clone https://github.com/fs0414/rfmt.git
106
106
  cd rfmt
107
107
  bundle install
108
108
  bundle exec rake compile
@@ -379,7 +379,7 @@ Everyone interacting in the rfmt project's codebases, issue trackers, chat rooms
379
379
  ## Support
380
380
 
381
381
  - 📖 [Documentation](docs/)
382
- - 🐛 [Issues](https://github.com/fujitanisora/rfmt/issues)
382
+ - 🐛 [Issues](https://github.com/fs0414/rfmt/issues)
383
383
  - 📧 Email: fujitanisora0414@gmail.com
384
384
 
385
385
  ## Acknowledgments
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "1.0.0"
3
+ version = "1.2.0"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -43,6 +43,10 @@ pub enum NodeType {
43
43
  ElseNode,
44
44
  UnlessNode,
45
45
 
46
+ // Exception handling
47
+ BeginNode,
48
+ RescueNode,
49
+
46
50
  // Literals
47
51
  StringNode,
48
52
  IntegerNode,
@@ -52,10 +56,115 @@ pub enum NodeType {
52
56
  TrueNode,
53
57
  FalseNode,
54
58
  NilNode,
59
+ SymbolNode,
55
60
 
56
61
  // Blocks
57
62
  BlockNode,
58
63
 
64
+ // Case/When
65
+ CaseNode,
66
+ WhenNode,
67
+
68
+ // HashNode
69
+ AssocNode,
70
+ KeywordHashNode,
71
+
72
+ // Variables
73
+ LocalVariableReadNode,
74
+ LocalVariableWriteNode,
75
+ InstanceVariableReadNode,
76
+ InstanceVariableWriteNode,
77
+
78
+ // Lambda
79
+ LambdaNode,
80
+
81
+ // Control flow
82
+ ReturnNode,
83
+ EnsureNode,
84
+
85
+ // Strings
86
+ InterpolatedStringNode,
87
+ EmbeddedStatementsNode,
88
+
89
+ // Logical
90
+ OrNode,
91
+ AndNode,
92
+ NotNode,
93
+
94
+ // Loop constructs
95
+ WhileNode,
96
+ UntilNode,
97
+ ForNode,
98
+
99
+ // Control flow
100
+ BreakNode,
101
+ NextNode,
102
+ RedoNode,
103
+ RetryNode,
104
+ YieldNode,
105
+ SuperNode,
106
+ ForwardingSuperNode,
107
+ RescueModifierNode,
108
+
109
+ // Ranges
110
+ RangeNode,
111
+
112
+ // Other literals
113
+ RegularExpressionNode,
114
+ SplatNode,
115
+ InterpolatedRegularExpressionNode,
116
+ InterpolatedSymbolNode,
117
+ XStringNode,
118
+ InterpolatedXStringNode,
119
+
120
+ // Class variables
121
+ ClassVariableReadNode,
122
+ ClassVariableWriteNode,
123
+ ClassVariableOrWriteNode,
124
+ ClassVariableAndWriteNode,
125
+ ClassVariableOperatorWriteNode,
126
+
127
+ // Global variables
128
+ GlobalVariableReadNode,
129
+ GlobalVariableWriteNode,
130
+ GlobalVariableOrWriteNode,
131
+ GlobalVariableAndWriteNode,
132
+ GlobalVariableOperatorWriteNode,
133
+
134
+ // Compound assignment
135
+ LocalVariableOrWriteNode,
136
+ LocalVariableAndWriteNode,
137
+ LocalVariableOperatorWriteNode,
138
+ InstanceVariableOrWriteNode,
139
+ InstanceVariableAndWriteNode,
140
+ InstanceVariableOperatorWriteNode,
141
+ ConstantOrWriteNode,
142
+ ConstantAndWriteNode,
143
+ ConstantOperatorWriteNode,
144
+ ConstantPathOrWriteNode,
145
+ ConstantPathAndWriteNode,
146
+ ConstantPathOperatorWriteNode,
147
+ ConstantPathWriteNode,
148
+
149
+ // Pattern matching
150
+ CaseMatchNode,
151
+ InNode,
152
+ MatchPredicateNode,
153
+ MatchRequiredNode,
154
+
155
+ // Other common nodes
156
+ SelfNode,
157
+ ParenthesesNode,
158
+ DefinedNode,
159
+ SingletonClassNode,
160
+ AliasMethodNode,
161
+ AliasGlobalVariableNode,
162
+ UndefNode,
163
+ AssocSplatNode,
164
+ BlockArgumentNode,
165
+ MultiWriteNode,
166
+ MultiTargetNode,
167
+
59
168
  // Constants (structural nodes, part of definitions)
60
169
  ConstantReadNode,
61
170
  ConstantWriteNode,
@@ -75,8 +184,6 @@ pub enum NodeType {
75
184
  }
76
185
 
77
186
  impl NodeType {
78
- /// Parse node type from Prism type string
79
- /// Returns Unknown variant for unsupported node types
80
187
  pub fn from_str(s: &str) -> Self {
81
188
  match s {
82
189
  "program_node" => Self::ProgramNode,
@@ -88,6 +195,8 @@ impl NodeType {
88
195
  "if_node" => Self::IfNode,
89
196
  "else_node" => Self::ElseNode,
90
197
  "unless_node" => Self::UnlessNode,
198
+ "begin_node" => Self::BeginNode,
199
+ "rescue_node" => Self::RescueNode,
91
200
  "string_node" => Self::StringNode,
92
201
  "integer_node" => Self::IntegerNode,
93
202
  "float_node" => Self::FloatNode,
@@ -108,6 +217,79 @@ impl NodeType {
108
217
  "optional_keyword_parameter_node" => Self::OptionalKeywordParameterNode,
109
218
  "keyword_rest_parameter_node" => Self::KeywordRestParameterNode,
110
219
  "block_parameter_node" => Self::BlockParameterNode,
220
+ "symbol_node" => Self::SymbolNode,
221
+ "case_node" => Self::CaseNode,
222
+ "when_node" => Self::WhenNode,
223
+ "assoc_node" => Self::AssocNode,
224
+ "keyword_hash_node" => Self::KeywordHashNode,
225
+ "local_variable_read_node" => Self::LocalVariableReadNode,
226
+ "local_variable_write_node" => Self::LocalVariableWriteNode,
227
+ "instance_variable_read_node" => Self::InstanceVariableReadNode,
228
+ "instance_variable_write_node" => Self::InstanceVariableWriteNode,
229
+ "lambda_node" => Self::LambdaNode,
230
+ "return_node" => Self::ReturnNode,
231
+ "ensure_node" => Self::EnsureNode,
232
+ "interpolated_string_node" => Self::InterpolatedStringNode,
233
+ "embedded_statements_node" => Self::EmbeddedStatementsNode,
234
+ "or_node" => Self::OrNode,
235
+ "and_node" => Self::AndNode,
236
+ "not_node" => Self::NotNode,
237
+ "while_node" => Self::WhileNode,
238
+ "until_node" => Self::UntilNode,
239
+ "for_node" => Self::ForNode,
240
+ "break_node" => Self::BreakNode,
241
+ "next_node" => Self::NextNode,
242
+ "redo_node" => Self::RedoNode,
243
+ "retry_node" => Self::RetryNode,
244
+ "yield_node" => Self::YieldNode,
245
+ "super_node" => Self::SuperNode,
246
+ "forwarding_super_node" => Self::ForwardingSuperNode,
247
+ "rescue_modifier_node" => Self::RescueModifierNode,
248
+ "range_node" => Self::RangeNode,
249
+ "regular_expression_node" => Self::RegularExpressionNode,
250
+ "splat_node" => Self::SplatNode,
251
+ "interpolated_regular_expression_node" => Self::InterpolatedRegularExpressionNode,
252
+ "interpolated_symbol_node" => Self::InterpolatedSymbolNode,
253
+ "x_string_node" => Self::XStringNode,
254
+ "interpolated_x_string_node" => Self::InterpolatedXStringNode,
255
+ "class_variable_read_node" => Self::ClassVariableReadNode,
256
+ "class_variable_write_node" => Self::ClassVariableWriteNode,
257
+ "class_variable_or_write_node" => Self::ClassVariableOrWriteNode,
258
+ "class_variable_and_write_node" => Self::ClassVariableAndWriteNode,
259
+ "class_variable_operator_write_node" => Self::ClassVariableOperatorWriteNode,
260
+ "global_variable_read_node" => Self::GlobalVariableReadNode,
261
+ "global_variable_write_node" => Self::GlobalVariableWriteNode,
262
+ "global_variable_or_write_node" => Self::GlobalVariableOrWriteNode,
263
+ "global_variable_and_write_node" => Self::GlobalVariableAndWriteNode,
264
+ "global_variable_operator_write_node" => Self::GlobalVariableOperatorWriteNode,
265
+ "local_variable_or_write_node" => Self::LocalVariableOrWriteNode,
266
+ "local_variable_and_write_node" => Self::LocalVariableAndWriteNode,
267
+ "local_variable_operator_write_node" => Self::LocalVariableOperatorWriteNode,
268
+ "instance_variable_or_write_node" => Self::InstanceVariableOrWriteNode,
269
+ "instance_variable_and_write_node" => Self::InstanceVariableAndWriteNode,
270
+ "instance_variable_operator_write_node" => Self::InstanceVariableOperatorWriteNode,
271
+ "constant_or_write_node" => Self::ConstantOrWriteNode,
272
+ "constant_and_write_node" => Self::ConstantAndWriteNode,
273
+ "constant_operator_write_node" => Self::ConstantOperatorWriteNode,
274
+ "constant_path_or_write_node" => Self::ConstantPathOrWriteNode,
275
+ "constant_path_and_write_node" => Self::ConstantPathAndWriteNode,
276
+ "constant_path_operator_write_node" => Self::ConstantPathOperatorWriteNode,
277
+ "constant_path_write_node" => Self::ConstantPathWriteNode,
278
+ "case_match_node" => Self::CaseMatchNode,
279
+ "in_node" => Self::InNode,
280
+ "match_predicate_node" => Self::MatchPredicateNode,
281
+ "match_required_node" => Self::MatchRequiredNode,
282
+ "self_node" => Self::SelfNode,
283
+ "parentheses_node" => Self::ParenthesesNode,
284
+ "defined_node" => Self::DefinedNode,
285
+ "singleton_class_node" => Self::SingletonClassNode,
286
+ "alias_method_node" => Self::AliasMethodNode,
287
+ "alias_global_variable_node" => Self::AliasGlobalVariableNode,
288
+ "undef_node" => Self::UndefNode,
289
+ "assoc_splat_node" => Self::AssocSplatNode,
290
+ "block_argument_node" => Self::BlockArgumentNode,
291
+ "multi_write_node" => Self::MultiWriteNode,
292
+ "multi_target_node" => Self::MultiTargetNode,
111
293
  _ => Self::Unknown(s.to_string()),
112
294
  }
113
295
  }
@@ -46,10 +46,15 @@ impl Emitter {
46
46
  self.buffer.clear();
47
47
  self.emitted_comment_indices.clear();
48
48
 
49
- // Collect all comments from the AST
50
49
  self.collect_comments(ast);
51
50
 
52
51
  self.emit_node(ast, 0)?;
52
+
53
+ // Ensure file ends with a newline
54
+ if !self.buffer.ends_with('\n') {
55
+ self.buffer.push('\n');
56
+ }
57
+
53
58
  Ok(self.buffer.clone())
54
59
  }
55
60
 
@@ -68,22 +73,26 @@ impl Emitter {
68
73
  IndentStyle::Tabs => "\t".repeat(indent_level),
69
74
  };
70
75
 
71
- let mut indices_to_emit = Vec::new();
76
+ let mut comments_to_emit = Vec::new();
72
77
  for (idx, comment) in self.all_comments.iter().enumerate() {
73
78
  if self.emitted_comment_indices.contains(&idx) {
74
79
  continue;
75
80
  }
76
81
 
77
- // Collect comments that end before this line
78
82
  if comment.location.end_line < line {
79
- indices_to_emit.push((idx, comment.text.clone()));
83
+ comments_to_emit.push((idx, comment.text.clone(), comment.location.end_line));
80
84
  }
81
85
  }
82
86
 
83
- // Now emit the collected comments
84
- for (idx, text) in indices_to_emit {
87
+ let comments_count = comments_to_emit.len();
88
+ for (i, (idx, text, comment_end_line)) in comments_to_emit.into_iter().enumerate() {
85
89
  writeln!(self.buffer, "{}{}", indent_str, text)?;
86
90
  self.emitted_comment_indices.push(idx);
91
+
92
+ // Only add blank line after the LAST comment if there was a gap in the original
93
+ if i == comments_count - 1 && line > comment_end_line + 1 {
94
+ self.buffer.push('\n');
95
+ }
87
96
  }
88
97
 
89
98
  Ok(())
@@ -123,6 +132,15 @@ impl Emitter {
123
132
  NodeType::IfNode => self.emit_if_unless(node, indent_level, false, "if")?,
124
133
  NodeType::UnlessNode => self.emit_if_unless(node, indent_level, false, "unless")?,
125
134
  NodeType::CallNode => self.emit_call(node, indent_level)?,
135
+ NodeType::BeginNode => self.emit_begin(node, indent_level)?,
136
+ NodeType::RescueNode => self.emit_rescue(node, indent_level)?,
137
+ NodeType::EnsureNode => self.emit_ensure(node, indent_level)?,
138
+ NodeType::LambdaNode => self.emit_lambda(node, indent_level)?,
139
+ NodeType::CaseNode => self.emit_case(node, indent_level)?,
140
+ NodeType::WhenNode => self.emit_when(node, indent_level)?,
141
+ NodeType::WhileNode => self.emit_while_until(node, indent_level, "while")?,
142
+ NodeType::UntilNode => self.emit_while_until(node, indent_level, "until")?,
143
+ NodeType::ForNode => self.emit_for(node, indent_level)?,
126
144
  _ => self.emit_generic(node, indent_level)?,
127
145
  }
128
146
  Ok(())
@@ -154,14 +172,28 @@ impl Emitter {
154
172
  for (i, child) in node.children.iter().enumerate() {
155
173
  self.emit_node(child, indent_level)?;
156
174
 
157
- // Add newlines between statements, normalizing to max 1 blank line
158
175
  if i < node.children.len() - 1 {
159
176
  let current_end_line = child.location.end_line;
160
- let next_start_line = node.children[i + 1].location.start_line;
161
- let line_diff = next_start_line.saturating_sub(current_end_line);
177
+ let next_child = &node.children[i + 1];
178
+ let next_start_line = next_child.location.start_line;
179
+
180
+ // Find the first comment between current and next node (if any)
181
+ let first_comment_line = self
182
+ .all_comments
183
+ .iter()
184
+ .filter(|c| {
185
+ c.location.start_line > current_end_line
186
+ && c.location.end_line < next_start_line
187
+ })
188
+ .map(|c| c.location.start_line)
189
+ .min();
190
+
191
+ // Calculate line diff based on whether there's a comment
192
+ let effective_next_line = first_comment_line.unwrap_or(next_start_line);
193
+ let line_diff = effective_next_line.saturating_sub(current_end_line);
162
194
 
163
- // Add 1 newline if consecutive, 2 newlines (1 blank line) if there was a gap
164
195
  let newlines = if line_diff > 1 { 2 } else { 1 };
196
+
165
197
  for _ in 0..newlines {
166
198
  self.buffer.push('\n');
167
199
  }
@@ -314,6 +346,217 @@ impl Emitter {
314
346
  Ok(())
315
347
  }
316
348
 
349
+ /// Emit begin node
350
+ /// BeginNode can be either:
351
+ /// 1. Explicit begin...end block (source starts with "begin")
352
+ /// 2. Implicit begin wrapping method body with rescue/ensure
353
+ fn emit_begin(&mut self, node: &Node, indent_level: usize) -> Result<()> {
354
+ // Check if this is an explicit begin block by looking at source
355
+ let is_explicit_begin = if !self.source.is_empty() {
356
+ self.source
357
+ .get(node.location.start_offset..)
358
+ .map(|s| s.trim_start().starts_with("begin"))
359
+ .unwrap_or(false)
360
+ } else {
361
+ false
362
+ };
363
+
364
+ if is_explicit_begin {
365
+ self.emit_comments_before(node.location.start_line, indent_level)?;
366
+ self.emit_indent(indent_level)?;
367
+ writeln!(self.buffer, "begin")?;
368
+
369
+ for child in &node.children {
370
+ self.emit_node(child, indent_level + 1)?;
371
+ self.buffer.push('\n');
372
+ }
373
+
374
+ self.emit_indent(indent_level)?;
375
+ write!(self.buffer, "end")?;
376
+ } else {
377
+ // Implicit begin - emit children directly
378
+ for (i, child) in node.children.iter().enumerate() {
379
+ if i > 0 {
380
+ self.buffer.push('\n');
381
+ }
382
+ self.emit_node(child, indent_level)?;
383
+ }
384
+ }
385
+ Ok(())
386
+ }
387
+
388
+ /// Emit rescue node
389
+ fn emit_rescue(&mut self, node: &Node, indent_level: usize) -> Result<()> {
390
+ // Rescue node structure:
391
+ // - First children are exception class references (ConstantReadNode)
392
+ // - Then exception variable (LocalVariableTargetNode)
393
+ // - Last child is StatementsNode with the rescue body
394
+
395
+ // Dedent by 1 level since rescue is at the same level as method body
396
+ let rescue_indent = indent_level.saturating_sub(1);
397
+ self.emit_indent(rescue_indent)?;
398
+ write!(self.buffer, "rescue")?;
399
+
400
+ // Extract exception classes and variable from source
401
+ if !self.source.is_empty() && node.location.end_offset <= self.source.len() {
402
+ if let Some(source_text) = self
403
+ .source
404
+ .get(node.location.start_offset..node.location.end_offset)
405
+ {
406
+ // Get the rescue line to extract exception class and variable
407
+ if let Some(rescue_line) = source_text.lines().next() {
408
+ // Remove "rescue" prefix and get the rest (exception class => var)
409
+ let after_rescue = rescue_line.trim_start_matches("rescue").trim();
410
+ if !after_rescue.is_empty() {
411
+ write!(self.buffer, " {}", after_rescue)?;
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ self.buffer.push('\n');
418
+
419
+ // Emit rescue body (last child is typically StatementsNode)
420
+ if let Some(body) = node.children.last() {
421
+ if matches!(body.node_type, NodeType::StatementsNode) {
422
+ self.emit_node(body, indent_level)?;
423
+ }
424
+ }
425
+
426
+ Ok(())
427
+ }
428
+
429
+ /// Emit ensure node
430
+ fn emit_ensure(&mut self, node: &Node, indent_level: usize) -> Result<()> {
431
+ // ensure keyword should be at same level as begin/rescue
432
+ let ensure_indent = indent_level.saturating_sub(1);
433
+
434
+ self.emit_comments_before(node.location.start_line, ensure_indent)?;
435
+ self.emit_indent(ensure_indent)?;
436
+ writeln!(self.buffer, "ensure")?;
437
+
438
+ // Emit ensure body statements
439
+ for child in &node.children {
440
+ match &child.node_type {
441
+ NodeType::StatementsNode => {
442
+ self.emit_statements(child, indent_level)?;
443
+ }
444
+ _ => {
445
+ self.emit_node(child, indent_level)?;
446
+ }
447
+ }
448
+ }
449
+
450
+ Ok(())
451
+ }
452
+
453
+ /// Emit lambda node
454
+ fn emit_lambda(&mut self, node: &Node, indent_level: usize) -> Result<()> {
455
+ self.emit_comments_before(node.location.start_line, indent_level)?;
456
+
457
+ // Lambda syntax is complex (-> vs lambda, {} vs do-end)
458
+ // Use source extraction to preserve original style
459
+ self.emit_generic_without_comments(node, indent_level)
460
+ }
461
+
462
+ /// Emit case node
463
+ fn emit_case(&mut self, node: &Node, indent_level: usize) -> Result<()> {
464
+ self.emit_comments_before(node.location.start_line, indent_level)?;
465
+ self.emit_indent(indent_level)?;
466
+
467
+ // Write "case" keyword
468
+ write!(self.buffer, "case")?;
469
+
470
+ // Find predicate (first child that isn't WhenNode or ElseNode)
471
+ let mut when_start_idx = 0;
472
+ if let Some(first_child) = node.children.first() {
473
+ if !matches!(
474
+ first_child.node_type,
475
+ NodeType::WhenNode | NodeType::ElseNode
476
+ ) {
477
+ // This is the predicate - extract from source
478
+ let start = first_child.location.start_offset;
479
+ let end = first_child.location.end_offset;
480
+ if let Some(text) = self.source.get(start..end) {
481
+ write!(self.buffer, " {}", text)?;
482
+ }
483
+ when_start_idx = 1;
484
+ }
485
+ }
486
+
487
+ self.buffer.push('\n');
488
+
489
+ // Emit when clauses and else
490
+ for child in node.children.iter().skip(when_start_idx) {
491
+ match &child.node_type {
492
+ NodeType::WhenNode => {
493
+ self.emit_when(child, indent_level)?;
494
+ self.buffer.push('\n');
495
+ }
496
+ NodeType::ElseNode => {
497
+ self.emit_indent(indent_level)?;
498
+ writeln!(self.buffer, "else")?;
499
+ // Emit else body
500
+ for else_child in &child.children {
501
+ if matches!(else_child.node_type, NodeType::StatementsNode) {
502
+ self.emit_statements(else_child, indent_level + 1)?;
503
+ } else {
504
+ self.emit_node(else_child, indent_level + 1)?;
505
+ }
506
+ }
507
+ self.buffer.push('\n');
508
+ }
509
+ _ => {}
510
+ }
511
+ }
512
+
513
+ // Emit "end" keyword
514
+ self.emit_indent(indent_level)?;
515
+ write!(self.buffer, "end")?;
516
+
517
+ Ok(())
518
+ }
519
+
520
+ /// Emit when node
521
+ fn emit_when(&mut self, node: &Node, indent_level: usize) -> Result<()> {
522
+ self.emit_comments_before(node.location.start_line, indent_level)?;
523
+ self.emit_indent(indent_level)?;
524
+
525
+ write!(self.buffer, "when ")?;
526
+
527
+ // Collect conditions (all children except StatementsNode)
528
+ let conditions: Vec<_> = node
529
+ .children
530
+ .iter()
531
+ .filter(|c| !matches!(c.node_type, NodeType::StatementsNode))
532
+ .collect();
533
+
534
+ // Emit conditions with comma separator
535
+ for (i, cond) in conditions.iter().enumerate() {
536
+ let start = cond.location.start_offset;
537
+ let end = cond.location.end_offset;
538
+ if let Some(text) = self.source.get(start..end) {
539
+ write!(self.buffer, "{}", text)?;
540
+ }
541
+ if i < conditions.len() - 1 {
542
+ write!(self.buffer, ", ")?;
543
+ }
544
+ }
545
+
546
+ self.buffer.push('\n');
547
+
548
+ // Emit statements body
549
+ if let Some(statements) = node
550
+ .children
551
+ .iter()
552
+ .find(|c| matches!(c.node_type, NodeType::StatementsNode))
553
+ {
554
+ self.emit_statements(statements, indent_level + 1)?;
555
+ }
556
+
557
+ Ok(())
558
+ }
559
+
317
560
  /// Emit if/unless/elsif/else node
318
561
  /// is_elsif: true if this is an elsif clause (don't emit 'end')
319
562
  /// keyword: "if" or "unless"
@@ -616,22 +859,29 @@ impl Emitter {
616
859
 
617
860
  /// Emit generic node by extracting from source
618
861
  fn emit_generic(&mut self, node: &Node, indent_level: usize) -> Result<()> {
619
- // Emit any comments before this node
620
862
  self.emit_comments_before(node.location.start_line, indent_level)?;
621
863
 
622
864
  if !self.source.is_empty() {
623
865
  let start = node.location.start_offset;
624
866
  let end = node.location.end_offset;
625
867
 
626
- // Clone text first to avoid borrow conflict
627
868
  let text_owned = self.source.get(start..end).map(|s| s.to_string());
628
869
 
629
870
  if let Some(text) = text_owned {
630
- // Add indentation before the extracted text
631
871
  self.emit_indent(indent_level)?;
632
872
  write!(self.buffer, "{}", text)?;
633
873
 
634
- // Emit any trailing comments on the same line
874
+ // Mark comments within this node's range as emitted
875
+ // (they are included in the source extraction)
876
+ for (idx, comment) in self.all_comments.iter().enumerate() {
877
+ if !self.emitted_comment_indices.contains(&idx)
878
+ && comment.location.start_line >= node.location.start_line
879
+ && comment.location.end_line <= node.location.end_line
880
+ {
881
+ self.emitted_comment_indices.push(idx);
882
+ }
883
+ }
884
+
635
885
  self.emit_trailing_comments(node.location.end_line)?;
636
886
  }
637
887
  }
@@ -649,6 +899,107 @@ impl Emitter {
649
899
  Ok(())
650
900
  }
651
901
 
902
+ /// Emit while/until loop
903
+ fn emit_while_until(&mut self, node: &Node, indent_level: usize, keyword: &str) -> Result<()> {
904
+ // Check if this is a postfix while/until (modifier form)
905
+ // In postfix form: "statement while/until condition"
906
+ // Check if body starts before predicate in source
907
+ let is_postfix = if node.children.len() >= 2 {
908
+ let predicate = &node.children[0];
909
+ let body = &node.children[1];
910
+ body.location.start_offset < predicate.location.start_offset
911
+ } else {
912
+ false
913
+ };
914
+
915
+ if is_postfix {
916
+ // Postfix form: extract from source as-is
917
+ return self.emit_generic(node, indent_level);
918
+ }
919
+
920
+ // Normal while/until with do...end
921
+ self.emit_comments_before(node.location.start_line, indent_level)?;
922
+ self.emit_indent(indent_level)?;
923
+ write!(self.buffer, "{} ", keyword)?;
924
+
925
+ // Emit predicate (condition) - first child
926
+ if let Some(predicate) = node.children.first() {
927
+ if !self.source.is_empty() {
928
+ let start = predicate.location.start_offset;
929
+ let end = predicate.location.end_offset;
930
+ if let Some(text) = self.source.get(start..end) {
931
+ write!(self.buffer, "{}", text)?;
932
+ }
933
+ }
934
+ }
935
+
936
+ self.buffer.push('\n');
937
+
938
+ // Emit body - second child (StatementsNode)
939
+ if let Some(body) = node.children.get(1) {
940
+ if matches!(body.node_type, NodeType::StatementsNode) {
941
+ self.emit_statements(body, indent_level + 1)?;
942
+ self.buffer.push('\n');
943
+ }
944
+ }
945
+
946
+ self.emit_indent(indent_level)?;
947
+ write!(self.buffer, "end")?;
948
+
949
+ Ok(())
950
+ }
951
+
952
+ /// Emit for loop
953
+ fn emit_for(&mut self, node: &Node, indent_level: usize) -> Result<()> {
954
+ self.emit_comments_before(node.location.start_line, indent_level)?;
955
+ self.emit_indent(indent_level)?;
956
+ write!(self.buffer, "for ")?;
957
+
958
+ // node.children: [index, collection, statements]
959
+ // index: LocalVariableTargetNode or MultiTargetNode
960
+ // collection: expression
961
+ // statements: StatementsNode
962
+
963
+ // Emit index variable - first child
964
+ if let Some(index) = node.children.first() {
965
+ if !self.source.is_empty() {
966
+ let start = index.location.start_offset;
967
+ let end = index.location.end_offset;
968
+ if let Some(text) = self.source.get(start..end) {
969
+ write!(self.buffer, "{}", text)?;
970
+ }
971
+ }
972
+ }
973
+
974
+ write!(self.buffer, " in ")?;
975
+
976
+ // Emit collection - second child
977
+ if let Some(collection) = node.children.get(1) {
978
+ if !self.source.is_empty() {
979
+ let start = collection.location.start_offset;
980
+ let end = collection.location.end_offset;
981
+ if let Some(text) = self.source.get(start..end) {
982
+ write!(self.buffer, "{}", text)?;
983
+ }
984
+ }
985
+ }
986
+
987
+ self.buffer.push('\n');
988
+
989
+ // Emit body - third child (StatementsNode)
990
+ if let Some(body) = node.children.get(2) {
991
+ if matches!(body.node_type, NodeType::StatementsNode) {
992
+ self.emit_statements(body, indent_level + 1)?;
993
+ self.buffer.push('\n');
994
+ }
995
+ }
996
+
997
+ self.emit_indent(indent_level)?;
998
+ write!(self.buffer, "end")?;
999
+
1000
+ Ok(())
1001
+ }
1002
+
652
1003
  /// Check if node is structural (part of definition syntax, not body)
653
1004
  fn is_structural_node(&self, node_type: &NodeType) -> bool {
654
1005
  matches!(
@@ -172,6 +172,127 @@ module Rfmt
172
172
  []
173
173
  end
174
174
  params + [node.body].compact
175
+ when Prism::BeginNode
176
+ [
177
+ node.statements,
178
+ node.rescue_clause,
179
+ node.ensure_clause
180
+ ].compact
181
+ when Prism::EnsureNode
182
+ [node.statements].compact
183
+ when Prism::LambdaNode
184
+ params = if node.parameters
185
+ node.parameters.child_nodes.compact
186
+ else
187
+ []
188
+ end
189
+ params + [node.body].compact
190
+ when Prism::RescueNode
191
+ result = []
192
+ result.concat(node.exceptions) if node.exceptions
193
+ result << node.reference if node.reference
194
+ result << node.statements if node.statements
195
+ result << node.subsequent if node.subsequent
196
+ result
197
+ when Prism::SymbolNode, Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode
198
+ []
199
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
200
+ [node.value].compact
201
+ when Prism::ReturnNode
202
+ node.arguments ? node.arguments.child_nodes.compact : []
203
+ when Prism::OrNode
204
+ [node.left, node.right].compact
205
+ when Prism::AssocNode
206
+ [node.key, node.value].compact
207
+ when Prism::KeywordHashNode
208
+ node.elements || []
209
+ when Prism::InterpolatedStringNode
210
+ node.parts || []
211
+ when Prism::EmbeddedStatementsNode
212
+ [node.statements].compact
213
+ when Prism::CaseNode
214
+ [node.predicate, *node.conditions, node.else_clause].compact
215
+ when Prism::WhenNode
216
+ [*node.conditions, node.statements].compact
217
+ when Prism::WhileNode, Prism::UntilNode
218
+ [node.predicate, node.statements].compact
219
+ when Prism::ForNode
220
+ [node.index, node.collection, node.statements].compact
221
+ when Prism::BreakNode, Prism::NextNode
222
+ node.arguments ? node.arguments.child_nodes.compact : []
223
+ when Prism::RedoNode, Prism::RetryNode
224
+ []
225
+ when Prism::YieldNode
226
+ node.arguments ? node.arguments.child_nodes.compact : []
227
+ when Prism::SuperNode
228
+ result = []
229
+ result.concat(node.arguments.child_nodes.compact) if node.arguments
230
+ result << node.block if node.block
231
+ result
232
+ when Prism::ForwardingSuperNode
233
+ node.block ? [node.block] : []
234
+ when Prism::RescueModifierNode
235
+ [node.expression, node.rescue_expression].compact
236
+ when Prism::RangeNode
237
+ [node.left, node.right].compact
238
+ when Prism::RegularExpressionNode
239
+ []
240
+ when Prism::SplatNode
241
+ [node.expression].compact
242
+ when Prism::AndNode
243
+ [node.left, node.right].compact
244
+ when Prism::NotNode
245
+ [node.expression].compact
246
+ when Prism::InterpolatedRegularExpressionNode, Prism::InterpolatedSymbolNode,
247
+ Prism::InterpolatedXStringNode
248
+ node.parts || []
249
+ when Prism::XStringNode
250
+ []
251
+ when Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode, Prism::SelfNode
252
+ []
253
+ when Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
254
+ [node.value].compact
255
+ when Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
256
+ Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
257
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
258
+ Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
259
+ Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
260
+ [node.value].compact
261
+ when Prism::ClassVariableOperatorWriteNode, Prism::GlobalVariableOperatorWriteNode,
262
+ Prism::LocalVariableOperatorWriteNode, Prism::InstanceVariableOperatorWriteNode,
263
+ Prism::ConstantOperatorWriteNode
264
+ [node.value].compact
265
+ when Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode,
266
+ Prism::ConstantPathOperatorWriteNode
267
+ [node.target, node.value].compact
268
+ when Prism::ConstantPathWriteNode
269
+ [node.target, node.value].compact
270
+ when Prism::CaseMatchNode
271
+ [node.predicate, *node.conditions, node.else_clause].compact
272
+ when Prism::InNode
273
+ [node.pattern, node.statements].compact
274
+ when Prism::MatchPredicateNode, Prism::MatchRequiredNode
275
+ [node.value, node.pattern].compact
276
+ when Prism::ParenthesesNode
277
+ [node.body].compact
278
+ when Prism::DefinedNode
279
+ [node.value].compact
280
+ when Prism::SingletonClassNode
281
+ [node.expression, node.body].compact
282
+ when Prism::AliasMethodNode
283
+ [node.new_name, node.old_name].compact
284
+ when Prism::AliasGlobalVariableNode
285
+ [node.new_name, node.old_name].compact
286
+ when Prism::UndefNode
287
+ node.names || []
288
+ when Prism::AssocSplatNode
289
+ [node.value].compact
290
+ when Prism::BlockArgumentNode
291
+ [node.expression].compact
292
+ when Prism::MultiWriteNode
293
+ [*node.lefts, node.rest, *node.rights, node.value].compact
294
+ when Prism::MultiTargetNode
295
+ [*node.lefts, node.rest, *node.rights].compact
175
296
  else
176
297
  # For unknown types, try to get child nodes if they exist
177
298
  []
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.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
data/lib/rfmt.rb CHANGED
@@ -61,7 +61,7 @@ module Rfmt
61
61
  DEFAULT_CONFIG = <<~YAML
62
62
  # rfmt Configuration File
63
63
  # This file controls how rfmt formats your Ruby code.
64
- # See https://github.com/fujitanisora/rfmt for full documentation.
64
+ # See https://github.com/fs0414/rfmt for full documentation.
65
65
 
66
66
  version: "1.0"
67
67
 
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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-12-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rb_sys
@@ -58,7 +57,7 @@ files:
58
57
  - lib/rfmt/configuration.rb
59
58
  - lib/rfmt/prism_bridge.rb
60
59
  - lib/rfmt/prism_node_extractor.rb
61
- - lib/rfmt/rfmt.so
60
+ - lib/rfmt/rfmt.bundle
62
61
  - lib/rfmt/version.rb
63
62
  - lib/ruby_lsp/rfmt/addon.rb
64
63
  - lib/ruby_lsp/rfmt/formatter_runner.rb
@@ -71,7 +70,6 @@ metadata:
71
70
  source_code_uri: https://github.com/fs0414/rfmt
72
71
  changelog_uri: https://github.com/fs0414/rfmt/releases
73
72
  ruby_lsp_addon: 'true'
74
- post_install_message:
75
73
  rdoc_options: []
76
74
  require_paths:
77
75
  - lib
@@ -86,8 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
84
  - !ruby/object:Gem::Version
87
85
  version: 3.0.0
88
86
  requirements: []
89
- rubygems_version: 3.5.22
90
- signing_key:
87
+ rubygems_version: 3.7.2
91
88
  specification_version: 4
92
89
  summary: Ruby Formatter impl Rust lang.
93
90
  test_files: []
data/lib/rfmt/rfmt.so DELETED
Binary file