rfmt 1.3.0 → 1.3.1

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: c46c2c5129ef89717e76c7d187a0739389814d2ee01a1c50a5b64173d9b728f4
4
- data.tar.gz: 63982b7a966f9a64b9b01ea9d5d22e55e3c020fa7f76c4d63333bb7114591a6c
3
+ metadata.gz: 3711e11ba344ee0b9e1e76d0885fd99bd7ccbd7901ab5ba819619ae40647365f
4
+ data.tar.gz: 95f1b17c4c9a542cc24d9b89df6dee44398f59d27da0446961ac320b338a6bb4
5
5
  SHA512:
6
- metadata.gz: a1f3c26b649ea73a7a01c41c8d06e8d00137eb1b48946e8043ee632d9f5ea6465649999558a1c101779638d0383dadbbd610174de34c68ffcefb6a095c214b01
7
- data.tar.gz: 8c6c55859f132cd0cf5cfedf8b3af6b785cbd52f73f9f6ae9d8493717068724357abec0ad82c64ba31fefd99d798154f0df6d9136f52ffff675f848aa57f7500
6
+ metadata.gz: 7bb8aff9825e2a8d44f3161786b7952a2b2e3b679d0e607eb748102971d4c8a417ca8dcaac80e171a97ef223814c229b14b63af505321309393edc9432550fe6
7
+ data.tar.gz: 110a42ff056ac3dd6939d0f67b50a4784bbdafd513c4c0a1ab6e9eb8d0a040a0aa65bc39d0b37975ffb1972cf81d8396868d33e627b71ed017894cf1674d3cc4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.1] - 2026-01-08
4
+
5
+ ### Added
6
+ - Dedicated emitters for SingletonClassNode and pattern matching (CaseMatchNode, InNode)
7
+ - Literal and pattern match node support
8
+ - NoKeywordsParameterNode support
9
+
10
+ ### Changed
11
+ - Performance: Comment lookup optimized with BTreeMap index (O(n) → O(log n))
12
+ - Performance: HashSet for comment tracking (O(n) → O(1) contains check)
13
+ - Performance: Cached indent strings to avoid repeated allocations
14
+
15
+ ### Fixed
16
+ - DefNode parameter handling and missing node types
17
+ - Deep nest JSON parse error (max_nesting: false)
18
+
3
19
  ## [1.3.0] - 2026-01-07
4
20
 
5
21
  ### Added
data/Cargo.lock CHANGED
@@ -1214,7 +1214,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
1214
1214
 
1215
1215
  [[package]]
1216
1216
  name = "rfmt"
1217
- version = "1.3.0"
1217
+ version = "1.3.1"
1218
1218
  dependencies = [
1219
1219
  "anyhow",
1220
1220
  "clap",
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "1.3.0"
3
+ version = "1.3.1"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -180,6 +180,58 @@ pub enum NodeType {
180
180
  KeywordRestParameterNode,
181
181
  BlockParameterNode,
182
182
 
183
+ // Source metadata nodes
184
+ SourceFileNode,
185
+ SourceLineNode,
186
+ SourceEncodingNode,
187
+
188
+ // Pre/Post execution
189
+ PreExecutionNode,
190
+ PostExecutionNode,
191
+
192
+ // Numeric literals
193
+ RationalNode,
194
+ ImaginaryNode,
195
+
196
+ // String interpolation
197
+ EmbeddedVariableNode,
198
+
199
+ // Pattern matching patterns
200
+ ArrayPatternNode,
201
+ HashPatternNode,
202
+ FindPatternNode,
203
+ CapturePatternNode,
204
+ AlternationPatternNode,
205
+ PinnedExpressionNode,
206
+ PinnedVariableNode,
207
+
208
+ // Forwarding
209
+ ForwardingArgumentsNode,
210
+ ForwardingParameterNode,
211
+ NoKeywordsParameterNode,
212
+
213
+ // References
214
+ BackReferenceReadNode,
215
+ NumberedReferenceReadNode,
216
+
217
+ // Call/Index compound assignment
218
+ CallAndWriteNode,
219
+ CallOrWriteNode,
220
+ CallOperatorWriteNode,
221
+ IndexAndWriteNode,
222
+ IndexOrWriteNode,
223
+ IndexOperatorWriteNode,
224
+
225
+ // Match
226
+ MatchWriteNode,
227
+ MatchLastLineNode,
228
+ InterpolatedMatchLastLineNode,
229
+
230
+ // Other
231
+ FlipFlopNode,
232
+ ImplicitNode,
233
+ ImplicitRestNode,
234
+
183
235
  Unknown(String),
184
236
  }
185
237
 
@@ -290,6 +342,46 @@ impl NodeType {
290
342
  "block_argument_node" => Self::BlockArgumentNode,
291
343
  "multi_write_node" => Self::MultiWriteNode,
292
344
  "multi_target_node" => Self::MultiTargetNode,
345
+ "source_file_node" => Self::SourceFileNode,
346
+ "source_line_node" => Self::SourceLineNode,
347
+ "source_encoding_node" => Self::SourceEncodingNode,
348
+ "pre_execution_node" => Self::PreExecutionNode,
349
+ "post_execution_node" => Self::PostExecutionNode,
350
+ // Numeric literals
351
+ "rational_node" => Self::RationalNode,
352
+ "imaginary_node" => Self::ImaginaryNode,
353
+ // String interpolation
354
+ "embedded_variable_node" => Self::EmbeddedVariableNode,
355
+ // Pattern matching patterns
356
+ "array_pattern_node" => Self::ArrayPatternNode,
357
+ "hash_pattern_node" => Self::HashPatternNode,
358
+ "find_pattern_node" => Self::FindPatternNode,
359
+ "capture_pattern_node" => Self::CapturePatternNode,
360
+ "alternation_pattern_node" => Self::AlternationPatternNode,
361
+ "pinned_expression_node" => Self::PinnedExpressionNode,
362
+ "pinned_variable_node" => Self::PinnedVariableNode,
363
+ // Forwarding
364
+ "forwarding_arguments_node" => Self::ForwardingArgumentsNode,
365
+ "forwarding_parameter_node" => Self::ForwardingParameterNode,
366
+ "no_keywords_parameter_node" => Self::NoKeywordsParameterNode,
367
+ // References
368
+ "back_reference_read_node" => Self::BackReferenceReadNode,
369
+ "numbered_reference_read_node" => Self::NumberedReferenceReadNode,
370
+ // Call/Index compound assignment
371
+ "call_and_write_node" => Self::CallAndWriteNode,
372
+ "call_or_write_node" => Self::CallOrWriteNode,
373
+ "call_operator_write_node" => Self::CallOperatorWriteNode,
374
+ "index_and_write_node" => Self::IndexAndWriteNode,
375
+ "index_or_write_node" => Self::IndexOrWriteNode,
376
+ "index_operator_write_node" => Self::IndexOperatorWriteNode,
377
+ // Match
378
+ "match_write_node" => Self::MatchWriteNode,
379
+ "match_last_line_node" => Self::MatchLastLineNode,
380
+ "interpolated_match_last_line_node" => Self::InterpolatedMatchLastLineNode,
381
+ // Other
382
+ "flip_flop_node" => Self::FlipFlopNode,
383
+ "implicit_node" => Self::ImplicitNode,
384
+ "implicit_rest_node" => Self::ImplicitRestNode,
293
385
  _ => Self::Unknown(s.to_string()),
294
386
  }
295
387
  }
@@ -1,6 +1,7 @@
1
1
  use crate::ast::{Comment, Node, NodeType};
2
2
  use crate::config::{Config, IndentStyle};
3
3
  use crate::error::Result;
4
+ use std::collections::{BTreeMap, HashSet};
4
5
  use std::fmt::Write;
5
6
 
6
7
  /// Block style for Ruby blocks
@@ -16,7 +17,12 @@ pub struct Emitter {
16
17
  source: String,
17
18
  buffer: String,
18
19
  all_comments: Vec<Comment>,
19
- emitted_comment_indices: Vec<usize>,
20
+ emitted_comment_indices: HashSet<usize>,
21
+ /// Cached indent strings by level (index = level, value = indent string)
22
+ indent_cache: Vec<String>,
23
+ /// Index of comment indices by start line for O(log n) lookup
24
+ /// Key: start_line, Value: Vec of comment indices that start on that line
25
+ comments_by_line: BTreeMap<usize, Vec<usize>>,
20
26
  }
21
27
 
22
28
  impl Emitter {
@@ -26,7 +32,9 @@ impl Emitter {
26
32
  source: String::new(),
27
33
  buffer: String::new(),
28
34
  all_comments: Vec::new(),
29
- emitted_comment_indices: Vec::new(),
35
+ emitted_comment_indices: HashSet::new(),
36
+ indent_cache: Vec::new(),
37
+ comments_by_line: BTreeMap::new(),
30
38
  }
31
39
  }
32
40
 
@@ -37,7 +45,9 @@ impl Emitter {
37
45
  source,
38
46
  buffer: String::new(),
39
47
  all_comments: Vec::new(),
40
- emitted_comment_indices: Vec::new(),
48
+ emitted_comment_indices: HashSet::new(),
49
+ indent_cache: Vec::new(),
50
+ comments_by_line: BTreeMap::new(),
41
51
  }
42
52
  }
43
53
 
@@ -45,8 +55,10 @@ impl Emitter {
45
55
  pub fn emit(&mut self, ast: &Node) -> Result<String> {
46
56
  self.buffer.clear();
47
57
  self.emitted_comment_indices.clear();
58
+ self.comments_by_line.clear();
48
59
 
49
60
  self.collect_comments(ast);
61
+ self.build_comment_index();
50
62
 
51
63
  self.emit_node(ast, 0)?;
52
64
 
@@ -61,7 +73,7 @@ impl Emitter {
61
73
  self.buffer.push('\n');
62
74
  }
63
75
 
64
- Ok(self.buffer.clone())
76
+ Ok(std::mem::take(&mut self.buffer))
65
77
  }
66
78
 
67
79
  /// Find the last line of code in the AST (excluding comments)
@@ -107,7 +119,7 @@ impl Emitter {
107
119
  }
108
120
 
109
121
  writeln!(self.buffer, "{}", comment.text)?;
110
- self.emitted_comment_indices.push(idx);
122
+ self.emitted_comment_indices.insert(idx);
111
123
  last_end_line = Some(comment.location.end_line);
112
124
  is_first_comment = false;
113
125
  }
@@ -122,28 +134,75 @@ impl Emitter {
122
134
  }
123
135
  }
124
136
 
137
+ /// Build the comment index by start line for O(log n) range lookups
138
+ fn build_comment_index(&mut self) {
139
+ for (idx, comment) in self.all_comments.iter().enumerate() {
140
+ self.comments_by_line
141
+ .entry(comment.location.start_line)
142
+ .or_default()
143
+ .push(idx);
144
+ }
145
+ }
146
+
147
+ /// Get comment indices in the given line range [start_line, end_line)
148
+ /// Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
149
+ fn get_comment_indices_in_range(&self, start_line: usize, end_line: usize) -> Vec<usize> {
150
+ self.comments_by_line
151
+ .range(start_line..end_line)
152
+ .flat_map(|(_, indices)| indices.iter().copied())
153
+ .filter(|&idx| !self.emitted_comment_indices.contains(&idx))
154
+ .collect()
155
+ }
156
+
157
+ /// Get comment indices before a given line (exclusive)
158
+ /// Uses BTreeMap range for O(log n) lookup
159
+ fn get_comment_indices_before(&self, line: usize) -> Vec<usize> {
160
+ self.comments_by_line
161
+ .range(..line)
162
+ .flat_map(|(_, indices)| indices.iter().copied())
163
+ .filter(|&idx| {
164
+ !self.emitted_comment_indices.contains(&idx)
165
+ && self.all_comments[idx].location.end_line < line
166
+ })
167
+ .collect()
168
+ }
169
+
170
+ /// Get comment indices on a specific line (for trailing comments)
171
+ /// Uses BTreeMap get for O(log n) lookup
172
+ fn get_comment_indices_on_line(&self, line: usize) -> Vec<usize> {
173
+ self.comments_by_line
174
+ .get(&line)
175
+ .map(|indices| {
176
+ indices
177
+ .iter()
178
+ .copied()
179
+ .filter(|&idx| !self.emitted_comment_indices.contains(&idx))
180
+ .collect()
181
+ })
182
+ .unwrap_or_default()
183
+ }
184
+
125
185
  /// Emit comments that appear before a given line
186
+ /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
126
187
  fn emit_comments_before(&mut self, line: usize, indent_level: usize) -> Result<()> {
127
- let indent_str = match self.config.formatting.indent_style {
128
- IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
129
- IndentStyle::Tabs => "\t".repeat(indent_level),
130
- };
188
+ let indent_str = self.get_indent(indent_level).to_string();
131
189
 
132
- let mut comments_to_emit = Vec::new();
133
- for (idx, comment) in self.all_comments.iter().enumerate() {
134
- if self.emitted_comment_indices.contains(&idx) {
135
- continue;
136
- }
190
+ // Use indexed lookup instead of iterating all comments
191
+ let indices = self.get_comment_indices_before(line);
137
192
 
138
- if comment.location.end_line < line {
139
- comments_to_emit.push((
193
+ // Build list of comments to emit with their data
194
+ let mut comments_to_emit: Vec<_> = indices
195
+ .into_iter()
196
+ .map(|idx| {
197
+ let comment = &self.all_comments[idx];
198
+ (
140
199
  idx,
141
200
  comment.text.clone(),
142
201
  comment.location.start_line,
143
202
  comment.location.end_line,
144
- ));
145
- }
146
- }
203
+ )
204
+ })
205
+ .collect();
147
206
 
148
207
  // Sort by start_line to emit in order
149
208
  comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
@@ -163,7 +222,7 @@ impl Emitter {
163
222
  }
164
223
 
165
224
  writeln!(self.buffer, "{}{}", indent_str, text)?;
166
- self.emitted_comment_indices.push(idx);
225
+ self.emitted_comment_indices.insert(idx);
167
226
  last_comment_end_line = Some(comment_end_line);
168
227
 
169
228
  // Add blank line after the LAST comment if there was a gap to the code
@@ -176,41 +235,44 @@ impl Emitter {
176
235
  }
177
236
 
178
237
  /// Check if there are any unemitted comments in the given line range
238
+ /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
179
239
  fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
180
- self.all_comments.iter().enumerate().any(|(idx, comment)| {
181
- !self.emitted_comment_indices.contains(&idx)
182
- && comment.location.start_line >= start_line
183
- && comment.location.end_line < end_line
184
- })
240
+ self.comments_by_line
241
+ .range(start_line..end_line)
242
+ .flat_map(|(_, indices)| indices.iter())
243
+ .any(|&idx| {
244
+ !self.emitted_comment_indices.contains(&idx)
245
+ && self.all_comments[idx].location.end_line < end_line
246
+ })
185
247
  }
186
248
 
187
249
  /// Emit comments that are within a given line range (exclusive of end_line)
250
+ /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
188
251
  fn emit_comments_in_range(
189
252
  &mut self,
190
253
  start_line: usize,
191
254
  end_line: usize,
192
255
  indent_level: usize,
193
256
  ) -> Result<()> {
194
- let indent_str = match self.config.formatting.indent_style {
195
- IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
196
- IndentStyle::Tabs => "\t".repeat(indent_level),
197
- };
198
-
199
- let mut comments_to_emit = Vec::new();
200
- for (idx, comment) in self.all_comments.iter().enumerate() {
201
- if self.emitted_comment_indices.contains(&idx) {
202
- continue;
203
- }
204
-
205
- if comment.location.start_line >= start_line && comment.location.end_line < end_line {
206
- comments_to_emit.push((
257
+ let indent_str = self.get_indent(indent_level).to_string();
258
+
259
+ // Use indexed lookup instead of iterating all comments
260
+ let indices = self.get_comment_indices_in_range(start_line, end_line);
261
+
262
+ // Build list of comments to emit, filtering by end_line
263
+ let mut comments_to_emit: Vec<_> = indices
264
+ .into_iter()
265
+ .filter(|&idx| self.all_comments[idx].location.end_line < end_line)
266
+ .map(|idx| {
267
+ let comment = &self.all_comments[idx];
268
+ (
207
269
  idx,
208
270
  comment.text.clone(),
209
271
  comment.location.start_line,
210
272
  comment.location.end_line,
211
- ));
212
- }
213
- }
273
+ )
274
+ })
275
+ .collect();
214
276
 
215
277
  // Sort by start_line to emit in order
216
278
  comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
@@ -227,7 +289,7 @@ impl Emitter {
227
289
  }
228
290
 
229
291
  writeln!(self.buffer, "{}{}", indent_str, text)?;
230
- self.emitted_comment_indices.push(idx);
292
+ self.emitted_comment_indices.insert(idx);
231
293
  last_comment_end_line = Some(comment_end_line);
232
294
  }
233
295
 
@@ -235,6 +297,7 @@ impl Emitter {
235
297
  }
236
298
 
237
299
  /// Emit comments that are within a given line range, preserving blank lines from prev_line
300
+ /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
238
301
  fn emit_comments_in_range_with_prev_line(
239
302
  &mut self,
240
303
  start_line: usize,
@@ -242,26 +305,25 @@ impl Emitter {
242
305
  indent_level: usize,
243
306
  prev_line: usize,
244
307
  ) -> Result<()> {
245
- let indent_str = match self.config.formatting.indent_style {
246
- IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
247
- IndentStyle::Tabs => "\t".repeat(indent_level),
248
- };
249
-
250
- let mut comments_to_emit = Vec::new();
251
- for (idx, comment) in self.all_comments.iter().enumerate() {
252
- if self.emitted_comment_indices.contains(&idx) {
253
- continue;
254
- }
255
-
256
- if comment.location.start_line >= start_line && comment.location.end_line < end_line {
257
- comments_to_emit.push((
308
+ let indent_str = self.get_indent(indent_level).to_string();
309
+
310
+ // Use indexed lookup instead of iterating all comments
311
+ let indices = self.get_comment_indices_in_range(start_line, end_line);
312
+
313
+ // Build list of comments to emit, filtering by end_line
314
+ let mut comments_to_emit: Vec<_> = indices
315
+ .into_iter()
316
+ .filter(|&idx| self.all_comments[idx].location.end_line < end_line)
317
+ .map(|idx| {
318
+ let comment = &self.all_comments[idx];
319
+ (
258
320
  idx,
259
321
  comment.text.clone(),
260
322
  comment.location.start_line,
261
323
  comment.location.end_line,
262
- ));
263
- }
264
- }
324
+ )
325
+ })
326
+ .collect();
265
327
 
266
328
  // Sort by start_line to emit in order
267
329
  comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
@@ -276,7 +338,7 @@ impl Emitter {
276
338
  }
277
339
 
278
340
  writeln!(self.buffer, "{}{}", indent_str, text)?;
279
- self.emitted_comment_indices.push(idx);
341
+ self.emitted_comment_indices.insert(idx);
280
342
  last_end_line = comment_end_line;
281
343
  }
282
344
 
@@ -284,23 +346,21 @@ impl Emitter {
284
346
  }
285
347
 
286
348
  /// Emit comments that appear on the same line (trailing comments)
349
+ /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
287
350
  fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
288
- let mut indices_to_emit = Vec::new();
289
- for (idx, comment) in self.all_comments.iter().enumerate() {
290
- if self.emitted_comment_indices.contains(&idx) {
291
- continue;
292
- }
351
+ // Use indexed lookup for O(log n) access
352
+ let indices = self.get_comment_indices_on_line(line);
293
353
 
294
- // Collect comments on the same line (trailing)
295
- if comment.location.start_line == line {
296
- indices_to_emit.push((idx, comment.text.clone()));
297
- }
298
- }
354
+ // Build list of comments to emit
355
+ let indices_to_emit: Vec<_> = indices
356
+ .into_iter()
357
+ .map(|idx| (idx, self.all_comments[idx].text.clone()))
358
+ .collect();
299
359
 
300
360
  // Now emit the collected comments
301
361
  for (idx, text) in indices_to_emit {
302
362
  write!(self.buffer, " {}", text)?;
303
- self.emitted_comment_indices.push(idx);
363
+ self.emitted_comment_indices.insert(idx);
304
364
  }
305
365
 
306
366
  Ok(())
@@ -326,6 +386,9 @@ impl Emitter {
326
386
  NodeType::WhileNode => self.emit_while_until(node, indent_level, "while")?,
327
387
  NodeType::UntilNode => self.emit_while_until(node, indent_level, "until")?,
328
388
  NodeType::ForNode => self.emit_for(node, indent_level)?,
389
+ NodeType::SingletonClassNode => self.emit_singleton_class(node, indent_level)?,
390
+ NodeType::CaseMatchNode => self.emit_case_match(node, indent_level)?,
391
+ NodeType::InNode => self.emit_in(node, indent_level)?,
329
392
  _ => self.emit_generic(node, indent_level)?,
330
393
  }
331
394
  Ok(())
@@ -508,31 +571,17 @@ impl Emitter {
508
571
  write!(self.buffer, "{}", name)?;
509
572
  }
510
573
 
511
- // TODO: Handle parameters properly
512
- // For now, extract from source if method has parameters
513
- if node
514
- .metadata
515
- .get("parameters_count")
516
- .and_then(|s| s.parse::<usize>().ok())
517
- .unwrap_or(0)
518
- > 0
519
- {
520
- // Extract parameter part from source
521
- if !self.source.is_empty() && node.location.end_offset <= self.source.len() {
522
- if let Some(source_text) = self
523
- .source
524
- .get(node.location.start_offset..node.location.end_offset)
525
- {
526
- // Find parameters in source (between def name and \n or ;)
527
- if let Some(def_line) = source_text.lines().next() {
528
- if let Some(params_start) = def_line.find('(') {
529
- if let Some(params_end) = def_line.find(')') {
530
- let params = &def_line[params_start..=params_end];
531
- write!(self.buffer, "{}", params)?;
532
- }
533
- }
534
- }
535
- }
574
+ // Emit parameters using metadata from prism_bridge
575
+ if let Some(params_text) = node.metadata.get("parameters_text") {
576
+ let has_parens = node
577
+ .metadata
578
+ .get("has_parens")
579
+ .map(|v| v == "true")
580
+ .unwrap_or(false);
581
+ if has_parens {
582
+ write!(self.buffer, "({})", params_text)?;
583
+ } else {
584
+ write!(self.buffer, " {}", params_text)?;
536
585
  }
537
586
  }
538
587
 
@@ -1221,7 +1270,7 @@ impl Emitter {
1221
1270
  && comment.location.start_line >= node.location.start_line
1222
1271
  && comment.location.end_line < node.location.end_line
1223
1272
  {
1224
- self.emitted_comment_indices.push(idx);
1273
+ self.emitted_comment_indices.insert(idx);
1225
1274
  }
1226
1275
  }
1227
1276
 
@@ -1254,7 +1303,7 @@ impl Emitter {
1254
1303
  && comment.location.start_line >= node.location.start_line
1255
1304
  && comment.location.end_line < node.location.end_line
1256
1305
  {
1257
- self.emitted_comment_indices.push(idx);
1306
+ self.emitted_comment_indices.insert(idx);
1258
1307
  }
1259
1308
  }
1260
1309
 
@@ -1264,13 +1313,23 @@ impl Emitter {
1264
1313
  Ok(())
1265
1314
  }
1266
1315
 
1316
+ /// Get cached indent string for a given level
1317
+ fn get_indent(&mut self, level: usize) -> &str {
1318
+ // Extend cache if needed
1319
+ while self.indent_cache.len() <= level {
1320
+ let len = self.indent_cache.len();
1321
+ let indent = match self.config.formatting.indent_style {
1322
+ IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * len),
1323
+ IndentStyle::Tabs => "\t".repeat(len),
1324
+ };
1325
+ self.indent_cache.push(indent);
1326
+ }
1327
+ &self.indent_cache[level]
1328
+ }
1329
+
1267
1330
  /// Emit indentation
1268
1331
  fn emit_indent(&mut self, level: usize) -> Result<()> {
1269
- let indent_str = match self.config.formatting.indent_style {
1270
- IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * level),
1271
- IndentStyle::Tabs => "\t".repeat(level),
1272
- };
1273
-
1332
+ let indent_str = self.get_indent(level).to_string();
1274
1333
  write!(self.buffer, "{}", indent_str)?;
1275
1334
  Ok(())
1276
1335
  }
@@ -1382,6 +1441,148 @@ impl Emitter {
1382
1441
  Ok(())
1383
1442
  }
1384
1443
 
1444
+ /// Emit singleton class definition (class << self / class << object)
1445
+ fn emit_singleton_class(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1446
+ self.emit_comments_before(node.location.start_line, indent_level)?;
1447
+ self.emit_indent(indent_level)?;
1448
+
1449
+ write!(self.buffer, "class << ")?;
1450
+
1451
+ // First child is the expression (self or an object)
1452
+ if let Some(expression) = node.children.first() {
1453
+ if !self.source.is_empty() {
1454
+ let start = expression.location.start_offset;
1455
+ let end = expression.location.end_offset;
1456
+ if let Some(text) = self.source.get(start..end) {
1457
+ write!(self.buffer, "{}", text)?;
1458
+ }
1459
+ }
1460
+ }
1461
+
1462
+ // Emit trailing comments on the class << line
1463
+ self.emit_trailing_comments(node.location.start_line)?;
1464
+ self.buffer.push('\n');
1465
+
1466
+ let class_start_line = node.location.start_line;
1467
+ let class_end_line = node.location.end_line;
1468
+ let mut has_body_content = false;
1469
+
1470
+ // Emit body (skip the first child which is the expression)
1471
+ for (i, child) in node.children.iter().enumerate() {
1472
+ if i == 0 {
1473
+ // Skip the expression (self or object)
1474
+ continue;
1475
+ }
1476
+ if matches!(child.node_type, NodeType::StatementsNode) {
1477
+ has_body_content = true;
1478
+ self.emit_statements(child, indent_level + 1)?;
1479
+ } else if !self.is_structural_node(&child.node_type) {
1480
+ has_body_content = true;
1481
+ self.emit_node(child, indent_level + 1)?;
1482
+ }
1483
+ }
1484
+
1485
+ // Emit comments inside the singleton class body
1486
+ self.emit_comments_in_range(class_start_line + 1, class_end_line, indent_level + 1)?;
1487
+
1488
+ // Add newline before end if there was body content
1489
+ if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
1490
+ && !self.buffer.ends_with('\n')
1491
+ {
1492
+ self.buffer.push('\n');
1493
+ }
1494
+
1495
+ self.emit_indent(indent_level)?;
1496
+ write!(self.buffer, "end")?;
1497
+ self.emit_trailing_comments(node.location.end_line)?;
1498
+
1499
+ Ok(())
1500
+ }
1501
+
1502
+ /// Emit case match (Ruby 3.0+ pattern matching with case...in)
1503
+ fn emit_case_match(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1504
+ self.emit_comments_before(node.location.start_line, indent_level)?;
1505
+ self.emit_indent(indent_level)?;
1506
+
1507
+ // Write "case" keyword
1508
+ write!(self.buffer, "case")?;
1509
+
1510
+ // Find predicate (first child that isn't InNode or ElseNode)
1511
+ let mut in_start_idx = 0;
1512
+ if let Some(first_child) = node.children.first() {
1513
+ if !matches!(first_child.node_type, NodeType::InNode | NodeType::ElseNode) {
1514
+ // This is the predicate - extract from source
1515
+ let start = first_child.location.start_offset;
1516
+ let end = first_child.location.end_offset;
1517
+ if let Some(text) = self.source.get(start..end) {
1518
+ write!(self.buffer, " {}", text)?;
1519
+ }
1520
+ in_start_idx = 1;
1521
+ }
1522
+ }
1523
+
1524
+ self.buffer.push('\n');
1525
+
1526
+ // Emit in clauses and else
1527
+ for child in node.children.iter().skip(in_start_idx) {
1528
+ match &child.node_type {
1529
+ NodeType::InNode => {
1530
+ self.emit_in(child, indent_level)?;
1531
+ self.buffer.push('\n');
1532
+ }
1533
+ NodeType::ElseNode => {
1534
+ self.emit_indent(indent_level)?;
1535
+ writeln!(self.buffer, "else")?;
1536
+ // Emit else body
1537
+ for else_child in &child.children {
1538
+ if matches!(else_child.node_type, NodeType::StatementsNode) {
1539
+ self.emit_statements(else_child, indent_level + 1)?;
1540
+ } else {
1541
+ self.emit_node(else_child, indent_level + 1)?;
1542
+ }
1543
+ }
1544
+ self.buffer.push('\n');
1545
+ }
1546
+ _ => {}
1547
+ }
1548
+ }
1549
+
1550
+ // Emit "end" keyword
1551
+ self.emit_indent(indent_level)?;
1552
+ write!(self.buffer, "end")?;
1553
+ self.emit_trailing_comments(node.location.end_line)?;
1554
+
1555
+ Ok(())
1556
+ }
1557
+
1558
+ /// Emit in node (pattern matching clause)
1559
+ fn emit_in(&mut self, node: &Node, indent_level: usize) -> Result<()> {
1560
+ self.emit_comments_before(node.location.start_line, indent_level)?;
1561
+ self.emit_indent(indent_level)?;
1562
+
1563
+ write!(self.buffer, "in ")?;
1564
+
1565
+ // First child is the pattern
1566
+ if let Some(pattern) = node.children.first() {
1567
+ let start = pattern.location.start_offset;
1568
+ let end = pattern.location.end_offset;
1569
+ if let Some(text) = self.source.get(start..end) {
1570
+ write!(self.buffer, "{}", text)?;
1571
+ }
1572
+ }
1573
+
1574
+ self.buffer.push('\n');
1575
+
1576
+ // Second child is the statements body
1577
+ if let Some(statements) = node.children.get(1) {
1578
+ if matches!(statements.node_type, NodeType::StatementsNode) {
1579
+ self.emit_statements(statements, indent_level + 1)?;
1580
+ }
1581
+ }
1582
+
1583
+ Ok(())
1584
+ }
1585
+
1385
1586
  /// Check if node is structural (part of definition syntax, not body)
1386
1587
  /// These nodes are part of class/module/method definitions and should not be emitted as body
1387
1588
  fn is_structural_node(&self, node_type: &NodeType) -> bool {
@@ -1398,6 +1599,8 @@ impl Emitter {
1398
1599
  | NodeType::OptionalKeywordParameterNode
1399
1600
  | NodeType::KeywordRestParameterNode
1400
1601
  | NodeType::BlockParameterNode
1602
+ | NodeType::ForwardingParameterNode
1603
+ | NodeType::NoKeywordsParameterNode
1401
1604
  )
1402
1605
  }
1403
1606
  }
@@ -56,7 +56,7 @@ module Rfmt
56
56
 
57
57
  # Serialize the Prism AST to JSON
58
58
  def self.serialize_ast(node)
59
- JSON.generate(convert_node(node))
59
+ JSON.generate(convert_node(node), max_nesting: false)
60
60
  end
61
61
 
62
62
  # Serialize the Prism AST with comments to JSON
@@ -80,7 +80,7 @@ module Rfmt
80
80
  JSON.generate({
81
81
  ast: convert_node(result.value),
82
82
  comments: comments
83
- })
83
+ }, max_nesting: false)
84
84
  end
85
85
 
86
86
  # Convert a Prism node to our internal representation
@@ -241,8 +241,6 @@ module Rfmt
241
241
  [node.expression].compact
242
242
  when Prism::AndNode
243
243
  [node.left, node.right].compact
244
- when Prism::NotNode
245
- [node.expression].compact
246
244
  when Prism::InterpolatedRegularExpressionNode, Prism::InterpolatedSymbolNode,
247
245
  Prism::InterpolatedXStringNode
248
246
  node.parts || []
@@ -293,6 +291,55 @@ module Rfmt
293
291
  [*node.lefts, node.rest, *node.rights, node.value].compact
294
292
  when Prism::MultiTargetNode
295
293
  [*node.lefts, node.rest, *node.rights].compact
294
+ when Prism::SourceFileNode, Prism::SourceLineNode, Prism::SourceEncodingNode
295
+ []
296
+ when Prism::PreExecutionNode, Prism::PostExecutionNode
297
+ [node.statements].compact
298
+ # Numeric literals
299
+ when Prism::RationalNode, Prism::ImaginaryNode
300
+ [node.numeric].compact
301
+ # String interpolation
302
+ when Prism::EmbeddedVariableNode
303
+ [node.variable].compact
304
+ # Pattern matching patterns
305
+ when Prism::ArrayPatternNode
306
+ [*node.requireds, node.rest, *node.posts].compact
307
+ when Prism::HashPatternNode
308
+ [*node.elements, node.rest].compact
309
+ when Prism::FindPatternNode
310
+ [node.left, *node.requireds, node.right].compact
311
+ when Prism::CapturePatternNode
312
+ [node.value, node.target].compact
313
+ when Prism::AlternationPatternNode
314
+ [node.left, node.right].compact
315
+ when Prism::PinnedExpressionNode
316
+ [node.expression].compact
317
+ when Prism::PinnedVariableNode
318
+ [node.variable].compact
319
+ # Forwarding and special parameters
320
+ when Prism::ForwardingArgumentsNode, Prism::ForwardingParameterNode,
321
+ Prism::NoKeywordsParameterNode
322
+ []
323
+ # References
324
+ when Prism::BackReferenceReadNode, Prism::NumberedReferenceReadNode
325
+ []
326
+ # Call/Index compound assignment
327
+ when Prism::CallAndWriteNode, Prism::CallOrWriteNode, Prism::CallOperatorWriteNode
328
+ [node.receiver, node.value].compact
329
+ when Prism::IndexAndWriteNode, Prism::IndexOrWriteNode, Prism::IndexOperatorWriteNode
330
+ [node.receiver, node.arguments, node.value].compact
331
+ # Match
332
+ when Prism::MatchWriteNode
333
+ [node.call, *node.targets].compact
334
+ when Prism::MatchLastLineNode, Prism::InterpolatedMatchLastLineNode
335
+ []
336
+ # Other
337
+ when Prism::FlipFlopNode
338
+ [node.left, node.right].compact
339
+ when Prism::ImplicitNode
340
+ [node.value].compact
341
+ when Prism::ImplicitRestNode
342
+ []
296
343
  else
297
344
  # For unknown types, try to get child nodes if they exist
298
345
  []
@@ -327,6 +374,11 @@ module Rfmt
327
374
  metadata['name'] = name
328
375
  end
329
376
  metadata['parameters_count'] = extract_parameter_count(node).to_s
377
+ # Extract parameters text directly from source
378
+ if node.parameters
379
+ metadata['parameters_text'] = node.parameters.location.slice
380
+ metadata['has_parens'] = (!node.lparen_loc.nil?).to_s
381
+ end
330
382
  # Check if this is a class method (def self.method_name)
331
383
  if node.respond_to?(:receiver) && node.receiver
332
384
  receiver = node.receiver
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.3.0'
4
+ VERSION = '1.3.1'
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.3.0
4
+ version: 1.3.1
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-06 00:00:00.000000000 Z
11
+ date: 2026-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys