rfmt 1.0.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: 75dece5f16cef2420a328e75f674416858eef7871c99a6aec1136c73f4b3b44c
4
- data.tar.gz: f514b13a111e7113ed68beb1a1443b7815f8b11d47deee454ef352acf3e5ec7d
3
+ metadata.gz: 21876b488a79df7b3a136bfc563ea91312bc86569205b4349b5ccc0371338071
4
+ data.tar.gz: 7896e3c94fe83d0de8a3a4bebd9d10e0f277f92bfd069384ff05c4cb524d57b5
5
5
  SHA512:
6
- metadata.gz: 3a3575ed8e1e35770b6ac016521ee131fc1cf92b065c16ae91418a1835e7ab6695f30b9f1bb09c2b45c24801ae9427490ae835fffca228137c18ca1299416bf2
7
- data.tar.gz: a5459089d49f7b905e565a3289dae2076df6a3d472a1332c0cca8f15ebab8d712a571e202b7053fcbbaeb85431d2f14f586e272e70966a0eeec1b72002e3fe4a
6
+ metadata.gz: e50633a81f18a263109deac74be48dbb067f39a1139d166d2e6d5d02c271f7dba232ad5512d8a63d7a571c23dc246ee8d41c82be2e477493c89ed92b7949dd19
7
+ data.tar.gz: eae8574579a5b30cef07472ef90ce854162282c84bb379c507796ee2984f5f6ee2004ec82a9e8100c04b5e047b0e8a4894564ada404f789e2180d7302f5842fc
data/CHANGELOG.md CHANGED
@@ -1,10 +1,47 @@
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
+
25
+ ## [1.1.0] - 2025-12-12
26
+
27
+ ### Added
28
+ - Editor integration (Ruby LSP support)
29
+ - Required/Optional keyword parameter node type support
30
+
31
+ ### Fixed
32
+ - Migration file superclass corruption (ActiveRecord::Migration[8.1] etc.)
33
+
34
+ ### Changed
35
+ - Removed unused scripts and test files (reduced Ruby code by ~38%)
36
+
3
37
  ## [1.0.0] - 2025-12-11
4
38
 
5
39
  ### Breaking Changes
6
40
  - First stable release (v1.0.0)
7
41
 
42
+ ### Added
43
+ - Neovim integration: format-on-save support with autocmd configuration
44
+
8
45
  ### Changed
9
46
  - Set JSON as default output format
10
47
  - Updated Japanese documentation
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
@@ -10,6 +10,7 @@ A Ruby code formatter written in Rust
10
10
  [Installation](#installation) •
11
11
  [Usage](#usage) •
12
12
  [Features](#features) •
13
+ [Editor Integration](#editor-integration) •
13
14
  [Documentation](#documentation) •
14
15
  [Contributing](#contributing)
15
16
 
@@ -101,7 +102,7 @@ bundle install
101
102
  ### From Source
102
103
 
103
104
  ```bash
104
- git clone https://github.com/fujitanisora/rfmt.git
105
+ git clone https://github.com/fs0414/rfmt.git
105
106
  cd rfmt
106
107
  bundle install
107
108
  bundle exec rake compile
@@ -273,6 +274,33 @@ class User < ApplicationRecord
273
274
  end
274
275
  ```
275
276
 
277
+ ## Editor Integration
278
+
279
+ ### Neovim
280
+
281
+ Format Ruby files on save using autocmd:
282
+
283
+ ```lua
284
+ -- ~/.config/nvim/init.lua
285
+
286
+ vim.api.nvim_create_autocmd("BufWritePre", {
287
+ pattern = { "*.rb", "*.rake", "Gemfile", "Rakefile" },
288
+ callback = function()
289
+ local filepath = vim.fn.expand("%:p")
290
+ local result = vim.fn.system({ "rfmt", filepath })
291
+ if vim.v.shell_error == 0 then
292
+ vim.cmd("edit!")
293
+ end
294
+ end,
295
+ })
296
+ ```
297
+
298
+ ### Coming Soon
299
+
300
+ - **VS Code** - Extension in development
301
+ - **RubyMine** - Plugin in development
302
+ - **Zed** - Extension in development
303
+
276
304
  ## Development
277
305
 
278
306
  ### Setup
@@ -351,7 +379,7 @@ Everyone interacting in the rfmt project's codebases, issue trackers, chat rooms
351
379
  ## Support
352
380
 
353
381
  - 📖 [Documentation](docs/)
354
- - 🐛 [Issues](https://github.com/fujitanisora/rfmt/issues)
382
+ - 🐛 [Issues](https://github.com/fs0414/rfmt/issues)
355
383
  - 📧 Email: fujitanisora0414@gmail.com
356
384
 
357
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,
@@ -66,6 +175,8 @@ pub enum NodeType {
66
175
  OptionalParameterNode,
67
176
  RestParameterNode,
68
177
  KeywordParameterNode,
178
+ RequiredKeywordParameterNode,
179
+ OptionalKeywordParameterNode,
69
180
  KeywordRestParameterNode,
70
181
  BlockParameterNode,
71
182
 
@@ -73,8 +184,6 @@ pub enum NodeType {
73
184
  }
74
185
 
75
186
  impl NodeType {
76
- /// Parse node type from Prism type string
77
- /// Returns Unknown variant for unsupported node types
78
187
  pub fn from_str(s: &str) -> Self {
79
188
  match s {
80
189
  "program_node" => Self::ProgramNode,
@@ -86,6 +195,8 @@ impl NodeType {
86
195
  "if_node" => Self::IfNode,
87
196
  "else_node" => Self::ElseNode,
88
197
  "unless_node" => Self::UnlessNode,
198
+ "begin_node" => Self::BeginNode,
199
+ "rescue_node" => Self::RescueNode,
89
200
  "string_node" => Self::StringNode,
90
201
  "integer_node" => Self::IntegerNode,
91
202
  "float_node" => Self::FloatNode,
@@ -102,8 +213,83 @@ impl NodeType {
102
213
  "optional_parameter_node" => Self::OptionalParameterNode,
103
214
  "rest_parameter_node" => Self::RestParameterNode,
104
215
  "keyword_parameter_node" => Self::KeywordParameterNode,
216
+ "required_keyword_parameter_node" => Self::RequiredKeywordParameterNode,
217
+ "optional_keyword_parameter_node" => Self::OptionalKeywordParameterNode,
105
218
  "keyword_rest_parameter_node" => Self::KeywordRestParameterNode,
106
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,
107
293
  _ => Self::Unknown(s.to_string()),
108
294
  }
109
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!(
@@ -660,6 +1011,8 @@ impl Emitter {
660
1011
  | NodeType::OptionalParameterNode
661
1012
  | NodeType::RestParameterNode
662
1013
  | NodeType::KeywordParameterNode
1014
+ | NodeType::RequiredKeywordParameterNode
1015
+ | NodeType::OptionalKeywordParameterNode
663
1016
  | NodeType::KeywordRestParameterNode
664
1017
  | NodeType::BlockParameterNode
665
1018
  )
@@ -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
  []
@@ -27,16 +27,25 @@ module Rfmt
27
27
  when Prism::ConstantReadNode
28
28
  sc.name.to_s
29
29
  when Prism::ConstantPathNode
30
- # Try full_name first, fall back to name
30
+ # Try full_name first, fall back to slice for original source
31
31
  if sc.respond_to?(:full_name)
32
32
  sc.full_name.to_s
33
- elsif sc.respond_to?(:name)
34
- sc.name.to_s
33
+ elsif sc.respond_to?(:slice)
34
+ sc.slice
35
35
  else
36
- sc.to_s
36
+ sc.location.slice
37
37
  end
38
+ when Prism::CallNode
39
+ # Handle cases like ActiveRecord::Migration[8.1]
40
+ # Use slice to get the original source text
41
+ sc.slice
38
42
  else
39
- sc.to_s
43
+ # Fallback: try to get original source text
44
+ if sc.respond_to?(:slice)
45
+ sc.slice
46
+ else
47
+ sc.location.slice
48
+ end
40
49
  end
41
50
  end
42
51
 
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.0.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.0.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-11 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
@@ -41,8 +40,6 @@ files:
41
40
  - exe/rfmt
42
41
  - ext/rfmt/Cargo.toml
43
42
  - ext/rfmt/extconf.rb
44
- - ext/rfmt/spec/config_spec.rb
45
- - ext/rfmt/spec/spec_helper.rb
46
43
  - ext/rfmt/src/ast/mod.rs
47
44
  - ext/rfmt/src/config/mod.rs
48
45
  - ext/rfmt/src/emitter/mod.rs
@@ -60,7 +57,7 @@ files:
60
57
  - lib/rfmt/configuration.rb
61
58
  - lib/rfmt/prism_bridge.rb
62
59
  - lib/rfmt/prism_node_extractor.rb
63
- - lib/rfmt/rfmt.so
60
+ - lib/rfmt/rfmt.bundle
64
61
  - lib/rfmt/version.rb
65
62
  - lib/ruby_lsp/rfmt/addon.rb
66
63
  - lib/ruby_lsp/rfmt/formatter_runner.rb
@@ -73,7 +70,6 @@ metadata:
73
70
  source_code_uri: https://github.com/fs0414/rfmt
74
71
  changelog_uri: https://github.com/fs0414/rfmt/releases
75
72
  ruby_lsp_addon: 'true'
76
- post_install_message:
77
73
  rdoc_options: []
78
74
  require_paths:
79
75
  - lib
@@ -88,8 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
84
  - !ruby/object:Gem::Version
89
85
  version: 3.0.0
90
86
  requirements: []
91
- rubygems_version: 3.5.22
92
- signing_key:
87
+ rubygems_version: 3.7.2
93
88
  specification_version: 4
94
89
  summary: Ruby Formatter impl Rust lang.
95
90
  test_files: []
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe 'Configuration' do
6
- describe 'configuration system integration' do
7
- it 'can load YAML configuration' do
8
- require 'tempfile'
9
- require 'yaml'
10
-
11
- config = {
12
- 'version' => '1.0',
13
- 'formatting' => {
14
- 'line_length' => 120,
15
- 'indent_width' => 4,
16
- 'indent_style' => 'tabs',
17
- 'quote_style' => 'single'
18
- }
19
- }
20
-
21
- Tempfile.create(['test_config', '.yml']) do |file|
22
- file.write(YAML.dump(config))
23
- file.flush
24
-
25
- loaded_config = YAML.load_file(file.path)
26
- expect(loaded_config['formatting']['line_length']).to eq(120)
27
- expect(loaded_config['formatting']['indent_width']).to eq(4)
28
- expect(loaded_config['formatting']['indent_style']).to eq('tabs')
29
- expect(loaded_config['formatting']['quote_style']).to eq('single')
30
- end
31
- end
32
-
33
- it 'validates configuration values' do
34
- # Configuration validation is tested in Rust tests
35
- # 11 Rust tests verify all validation logic
36
- expect(true).to be true
37
- end
38
- end
39
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/setup'
4
- require 'rfmt'
5
-
6
- RSpec.configure do |config|
7
- config.expect_with :rspec do |expectations|
8
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
9
- end
10
-
11
- config.mock_with :rspec do |mocks|
12
- mocks.verify_partial_doubles = true
13
- end
14
-
15
- config.shared_context_metadata_behavior = :apply_to_host_groups
16
- end
data/lib/rfmt/rfmt.so DELETED
Binary file