rfmt 1.2.2 → 1.2.4

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: fe87cc473fd52b077095eda7b037967f646a97575fb4b9ad8ade73eb33052340
4
- data.tar.gz: 8793f7c0480e2a4bc6b62b8274887993f38932a3a17d712a4e4f91f1eec02eee
3
+ metadata.gz: 7c95517c6abaad1c4bf47425dd4853dcd78bf5a2e47ab2bdc0cae1a3d3698c54
4
+ data.tar.gz: a207798a5811f989a5ebb3405e48e0c86ac60d40b208f141e6d31a83de62dc0d
5
5
  SHA512:
6
- metadata.gz: da28021fbcabc8218b212797e5c6343b1f87b5769f30aeecacbf664788e82763d87c74a4edfc1db98e224b7980961c88267fc7e047e124c7bf5b195e802146e2
7
- data.tar.gz: 2d22d732bfdfc34b4a9c53db73a5117735b93ef02c6a724c379f5c54c3fd0600bd8e6ba6de9306dc5c157ecbc480b869b78903ba58f3e3ee5cfd6a973ed15dc2
6
+ metadata.gz: 86307164b77b1836c57718c62ac84bbc920fbfc36a7f0367656bc4fadd65cd846df9f45e5d8985a3eff2f49a548a576a8a73cb05e8e78679fe666589e904762c
7
+ data.tar.gz: 69a7f17c2e547616ebea1a109628df5d3695fb9d9359715d90707a51c4d1c2d72095bad48ffc1d370e7229a01f193392235c3c7c2a0f13fd211dc0885ef2c447
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.4] - 2026-01-04
4
+
5
+ ### Fixed
6
+ - Fix comment indent space handling
7
+
8
+ ## [1.2.3] - 2026-01-04
9
+
10
+ ### Fixed
11
+ - Fix migration file formatting (`emit_rescue` handling for rescue blocks)
12
+
3
13
  ## [1.2.2] - 2026-01-04
4
14
 
5
15
  ### Fixed
data/Cargo.lock CHANGED
@@ -1219,7 +1219,7 @@ checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
1219
1219
 
1220
1220
  [[package]]
1221
1221
  name = "rfmt"
1222
- version = "1.2.2"
1222
+ version = "1.2.4"
1223
1223
  dependencies = [
1224
1224
  "anyhow",
1225
1225
  "clap",
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "1.2.2"
3
+ version = "1.2.4"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -50,6 +50,12 @@ impl Emitter {
50
50
 
51
51
  self.emit_node(ast, 0)?;
52
52
 
53
+ // Find the last emitted code line for proper blank line handling
54
+ let last_code_line = Self::find_last_code_line(ast);
55
+
56
+ // Emit any remaining comments that weren't emitted
57
+ self.emit_remaining_comments(last_code_line)?;
58
+
53
59
  // Ensure file ends with a newline
54
60
  if !self.buffer.ends_with('\n') {
55
61
  self.buffer.push('\n');
@@ -58,6 +64,43 @@ impl Emitter {
58
64
  Ok(self.buffer.clone())
59
65
  }
60
66
 
67
+ /// Find the last line of code in the AST (excluding comments)
68
+ fn find_last_code_line(ast: &Node) -> usize {
69
+ let mut max_line = ast.location.end_line;
70
+ for child in &ast.children {
71
+ let child_end = Self::find_last_code_line(child);
72
+ if child_end > max_line {
73
+ max_line = child_end;
74
+ }
75
+ }
76
+ max_line
77
+ }
78
+
79
+ /// Emit all comments that haven't been emitted yet
80
+ fn emit_remaining_comments(&mut self, last_code_line: usize) -> Result<()> {
81
+ let mut last_end_line: Option<usize> = Some(last_code_line);
82
+ for (idx, comment) in self.all_comments.iter().enumerate() {
83
+ if self.emitted_comment_indices.contains(&idx) {
84
+ continue;
85
+ }
86
+ // Ensure we start on a new line for remaining comments
87
+ if !self.buffer.ends_with('\n') {
88
+ self.buffer.push('\n');
89
+ }
90
+ // Preserve blank lines between code/comments
91
+ if let Some(prev_line) = last_end_line {
92
+ let gap = comment.location.start_line.saturating_sub(prev_line);
93
+ for _ in 1..gap {
94
+ self.buffer.push('\n');
95
+ }
96
+ }
97
+ writeln!(self.buffer, "{}", comment.text)?;
98
+ self.emitted_comment_indices.push(idx);
99
+ last_end_line = Some(comment.location.end_line);
100
+ }
101
+ Ok(())
102
+ }
103
+
61
104
  /// Recursively collect all comments from the AST
62
105
  fn collect_comments(&mut self, node: &Node) {
63
106
  self.all_comments.extend(node.comments.clone());
@@ -80,16 +123,37 @@ impl Emitter {
80
123
  }
81
124
 
82
125
  if comment.location.end_line < line {
83
- comments_to_emit.push((idx, comment.text.clone(), comment.location.end_line));
126
+ comments_to_emit.push((
127
+ idx,
128
+ comment.text.clone(),
129
+ comment.location.start_line,
130
+ comment.location.end_line,
131
+ ));
84
132
  }
85
133
  }
86
134
 
135
+ // Sort by start_line to emit in order
136
+ comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
137
+
87
138
  let comments_count = comments_to_emit.len();
88
- for (i, (idx, text, comment_end_line)) in comments_to_emit.into_iter().enumerate() {
139
+ let mut last_comment_end_line: Option<usize> = None;
140
+
141
+ for (i, (idx, text, comment_start_line, comment_end_line)) in
142
+ comments_to_emit.into_iter().enumerate()
143
+ {
144
+ // Preserve blank lines between comments
145
+ if let Some(prev_end) = last_comment_end_line {
146
+ let gap = comment_start_line.saturating_sub(prev_end);
147
+ for _ in 1..gap {
148
+ self.buffer.push('\n');
149
+ }
150
+ }
151
+
89
152
  writeln!(self.buffer, "{}{}", indent_str, text)?;
90
153
  self.emitted_comment_indices.push(idx);
154
+ last_comment_end_line = Some(comment_end_line);
91
155
 
92
- // Only add blank line after the LAST comment if there was a gap in the original
156
+ // Add blank line after the LAST comment if there was a gap to the code
93
157
  if i == comments_count - 1 && line > comment_end_line + 1 {
94
158
  self.buffer.push('\n');
95
159
  }
@@ -98,6 +162,65 @@ impl Emitter {
98
162
  Ok(())
99
163
  }
100
164
 
165
+ /// Check if there are any unemitted comments in the given line range
166
+ fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
167
+ self.all_comments.iter().enumerate().any(|(idx, comment)| {
168
+ !self.emitted_comment_indices.contains(&idx)
169
+ && comment.location.start_line >= start_line
170
+ && comment.location.end_line < end_line
171
+ })
172
+ }
173
+
174
+ /// Emit comments that are within a given line range (exclusive of end_line)
175
+ fn emit_comments_in_range(
176
+ &mut self,
177
+ start_line: usize,
178
+ end_line: usize,
179
+ indent_level: usize,
180
+ ) -> Result<()> {
181
+ let indent_str = match self.config.formatting.indent_style {
182
+ IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
183
+ IndentStyle::Tabs => "\t".repeat(indent_level),
184
+ };
185
+
186
+ let mut comments_to_emit = Vec::new();
187
+ for (idx, comment) in self.all_comments.iter().enumerate() {
188
+ if self.emitted_comment_indices.contains(&idx) {
189
+ continue;
190
+ }
191
+
192
+ if comment.location.start_line >= start_line && comment.location.end_line < end_line {
193
+ comments_to_emit.push((
194
+ idx,
195
+ comment.text.clone(),
196
+ comment.location.start_line,
197
+ comment.location.end_line,
198
+ ));
199
+ }
200
+ }
201
+
202
+ // Sort by start_line to emit in order
203
+ comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
204
+
205
+ let mut last_comment_end_line: Option<usize> = None;
206
+
207
+ for (idx, text, comment_start_line, comment_end_line) in comments_to_emit {
208
+ // Preserve blank lines between comments
209
+ if let Some(prev_end) = last_comment_end_line {
210
+ let gap = comment_start_line.saturating_sub(prev_end);
211
+ for _ in 1..gap {
212
+ self.buffer.push('\n');
213
+ }
214
+ }
215
+
216
+ writeln!(self.buffer, "{}{}", indent_str, text)?;
217
+ self.emitted_comment_indices.push(idx);
218
+ last_comment_end_line = Some(comment_end_line);
219
+ }
220
+
221
+ Ok(())
222
+ }
223
+
101
224
  /// Emit comments that appear on the same line (trailing comments)
102
225
  fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
103
226
  let mut indices_to_emit = Vec::new();
@@ -218,22 +341,35 @@ impl Emitter {
218
341
  write!(self.buffer, " < {}", superclass)?;
219
342
  }
220
343
 
344
+ // Emit trailing comments on the class definition line (e.g., # rubocop:disable)
345
+ self.emit_trailing_comments(node.location.start_line)?;
221
346
  self.buffer.push('\n');
222
347
 
223
- // Emit body (children), but skip structural nodes like constant_read_node
348
+ // Emit body (children), but skip structural nodes (class name, superclass)
349
+ // Use start_line check to properly handle CallNode superclasses like ActiveRecord::Migration[8.0]
350
+ let class_start_line = node.location.start_line;
351
+ let class_end_line = node.location.end_line;
352
+ let mut has_body_content = false;
353
+
224
354
  for child in &node.children {
355
+ // Skip nodes on the same line as class definition (name, superclass)
356
+ if child.location.start_line == class_start_line {
357
+ continue;
358
+ }
225
359
  if self.is_structural_node(&child.node_type) {
226
360
  continue;
227
361
  }
362
+ has_body_content = true;
228
363
  self.emit_node(child, indent_level + 1)?;
229
- // Note: don't add newline here, statements node will handle it
230
364
  }
231
365
 
232
- // Add newline before end if there was body content
233
- if node
234
- .children
235
- .iter()
236
- .any(|c| !self.is_structural_node(&c.node_type))
366
+ // Emit comments that are inside the class body but not attached to any node
367
+ // These are comments between class_start_line and class_end_line
368
+ self.emit_comments_in_range(class_start_line + 1, class_end_line, indent_level + 1)?;
369
+
370
+ // Add newline before end if there was body content or internal comments
371
+ if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
372
+ && !self.buffer.ends_with('\n')
237
373
  {
238
374
  self.buffer.push('\n');
239
375
  }
@@ -256,21 +392,29 @@ impl Emitter {
256
392
  write!(self.buffer, "{}", name)?;
257
393
  }
258
394
 
395
+ // Emit trailing comments on the module definition line
396
+ self.emit_trailing_comments(node.location.start_line)?;
259
397
  self.buffer.push('\n');
260
398
 
399
+ let module_start_line = node.location.start_line;
400
+ let module_end_line = node.location.end_line;
401
+ let mut has_body_content = false;
402
+
261
403
  // Emit body (children), but skip structural nodes
262
404
  for child in &node.children {
263
405
  if self.is_structural_node(&child.node_type) {
264
406
  continue;
265
407
  }
408
+ has_body_content = true;
266
409
  self.emit_node(child, indent_level + 1)?;
267
410
  }
268
411
 
269
- // Add newline before end if there was body content
270
- if node
271
- .children
272
- .iter()
273
- .any(|c| !self.is_structural_node(&c.node_type))
412
+ // Emit comments that are inside the module body but not attached to any node
413
+ self.emit_comments_in_range(module_start_line + 1, module_end_line, indent_level + 1)?;
414
+
415
+ // Add newline before end if there was body content or internal comments
416
+ if (has_body_content || self.has_comments_in_range(module_start_line + 1, module_end_line))
417
+ && !self.buffer.ends_with('\n')
274
418
  {
275
419
  self.buffer.push('\n');
276
420
  }
@@ -289,6 +433,11 @@ impl Emitter {
289
433
  self.emit_indent(indent_level)?;
290
434
  write!(self.buffer, "def ")?;
291
435
 
436
+ // Handle class methods (def self.method_name)
437
+ if let Some(receiver) = node.metadata.get("receiver") {
438
+ write!(self.buffer, "{}.", receiver)?;
439
+ }
440
+
292
441
  if let Some(name) = node.metadata.get("name") {
293
442
  write!(self.buffer, "{}", name)?;
294
443
  }
@@ -321,6 +470,8 @@ impl Emitter {
321
470
  }
322
471
  }
323
472
 
473
+ // Emit trailing comment on same line as def
474
+ self.emit_trailing_comments(node.location.start_line)?;
324
475
  self.buffer.push('\n');
325
476
 
326
477
  // Emit body (children), but skip structural nodes like parameter nodes
@@ -398,28 +549,78 @@ impl Emitter {
398
549
  write!(self.buffer, "rescue")?;
399
550
 
400
551
  // Extract exception classes and variable from source
552
+ // Handle multi-line rescue clauses (e.g., multiple exception classes spanning lines)
401
553
  if !self.source.is_empty() && node.location.end_offset <= self.source.len() {
402
554
  if let Some(source_text) = self
403
555
  .source
404
556
  .get(node.location.start_offset..node.location.end_offset)
405
557
  {
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)?;
558
+ // Find the rescue declaration part (first line only, unless trailing comma/backslash)
559
+ let mut rescue_decl = String::new();
560
+ let mut expect_continuation = false;
561
+
562
+ for line in source_text.lines() {
563
+ let trimmed = line.trim();
564
+
565
+ if rescue_decl.is_empty() {
566
+ // First line - remove "rescue" prefix
567
+ let after_rescue = trimmed.trim_start_matches("rescue").trim();
568
+ if !after_rescue.is_empty() {
569
+ // Check if line ends with continuation marker
570
+ expect_continuation =
571
+ after_rescue.ends_with(',') || after_rescue.ends_with('\\');
572
+ rescue_decl.push_str(after_rescue.trim_end_matches('\\').trim());
573
+ }
574
+ if !expect_continuation {
575
+ break;
576
+ }
577
+ } else if expect_continuation {
578
+ // Continuation line after trailing comma or backslash
579
+ // Add space after comma or if no trailing space
580
+ if !rescue_decl.ends_with(' ') {
581
+ rescue_decl.push(' ');
582
+ }
583
+ let content = trimmed.trim_end_matches('\\').trim();
584
+ rescue_decl.push_str(content);
585
+ expect_continuation = trimmed.ends_with(',') || trimmed.ends_with('\\');
586
+ if !expect_continuation {
587
+ break;
588
+ }
589
+ } else {
590
+ break;
412
591
  }
413
592
  }
593
+
594
+ if !rescue_decl.is_empty() {
595
+ write!(self.buffer, " {}", rescue_decl)?;
596
+ }
414
597
  }
415
598
  }
416
599
 
417
600
  self.buffer.push('\n');
418
601
 
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)?;
602
+ // Emit rescue body and handle subsequent rescue nodes
603
+ // Children structure:
604
+ // - ConstantReadNode/ConstantPathNode (exception classes)
605
+ // - LocalVariableTargetNode (optional, exception variable)
606
+ // - StatementsNode (rescue body)
607
+ // - RescueNode (optional, subsequent rescue clause)
608
+ for child in &node.children {
609
+ match &child.node_type {
610
+ NodeType::StatementsNode => {
611
+ self.emit_node(child, indent_level)?;
612
+ }
613
+ NodeType::RescueNode => {
614
+ // Emit subsequent rescue clause
615
+ // Ensure newline before subsequent rescue
616
+ if !self.buffer.ends_with('\n') {
617
+ self.buffer.push('\n');
618
+ }
619
+ self.emit_rescue(child, indent_level)?;
620
+ }
621
+ _ => {
622
+ // Skip exception classes and variable (already handled above)
623
+ }
423
624
  }
424
625
  }
425
626
 
@@ -612,6 +813,59 @@ impl Emitter {
612
813
  return Ok(());
613
814
  }
614
815
 
816
+ // Check for ternary operator
817
+ let is_ternary = node
818
+ .metadata
819
+ .get("is_ternary")
820
+ .map(|v| v == "true")
821
+ .unwrap_or(false);
822
+
823
+ if is_ternary && !is_elsif {
824
+ self.emit_comments_before(node.location.start_line, indent_level)?;
825
+ self.emit_indent(indent_level)?;
826
+
827
+ // Emit condition
828
+ if let Some(predicate) = node.children.first() {
829
+ if !self.source.is_empty() {
830
+ let start = predicate.location.start_offset;
831
+ let end = predicate.location.end_offset;
832
+ if let Some(text) = self.source.get(start..end) {
833
+ write!(self.buffer, "{}", text)?;
834
+ }
835
+ }
836
+ }
837
+
838
+ write!(self.buffer, " ? ")?;
839
+
840
+ // Emit then expression
841
+ if let Some(statements) = node.children.get(1) {
842
+ if !self.source.is_empty() {
843
+ let start = statements.location.start_offset;
844
+ let end = statements.location.end_offset;
845
+ if let Some(text) = self.source.get(start..end) {
846
+ write!(self.buffer, "{}", text.trim())?;
847
+ }
848
+ }
849
+ }
850
+
851
+ write!(self.buffer, " : ")?;
852
+
853
+ // Emit else expression
854
+ if let Some(else_node) = node.children.get(2) {
855
+ if let Some(else_statements) = else_node.children.first() {
856
+ if !self.source.is_empty() {
857
+ let start = else_statements.location.start_offset;
858
+ let end = else_statements.location.end_offset;
859
+ if let Some(text) = self.source.get(start..end) {
860
+ write!(self.buffer, "{}", text.trim())?;
861
+ }
862
+ }
863
+ }
864
+ }
865
+
866
+ return Ok(());
867
+ }
868
+
615
869
  // Normal if/unless/elsif
616
870
  if !is_elsif {
617
871
  self.emit_comments_before(node.location.start_line, indent_level)?;
@@ -637,6 +891,8 @@ impl Emitter {
637
891
  }
638
892
  }
639
893
 
894
+ // Emit trailing comment on same line as if/unless/elsif
895
+ self.emit_trailing_comments(node.location.start_line)?;
640
896
  self.buffer.push('\n');
641
897
 
642
898
  // Emit then clause (second child is StatementsNode)
@@ -760,6 +1016,8 @@ impl Emitter {
760
1016
  // Emit block parameters if present (|x, y|)
761
1017
  self.emit_block_parameters(block_node)?;
762
1018
 
1019
+ // Emit trailing comment on same line as do |...|
1020
+ self.emit_trailing_comments(block_node.location.start_line)?;
763
1021
  self.buffer.push('\n');
764
1022
 
765
1023
  // Find and emit the body (StatementsNode among children)
@@ -851,6 +1109,20 @@ impl Emitter {
851
1109
  if let Some(text) = text_owned {
852
1110
  self.emit_indent(indent_level)?;
853
1111
  write!(self.buffer, "{}", text)?;
1112
+
1113
+ // Mark comments that are strictly inside this node's line range as emitted
1114
+ // (they are included in the source extraction)
1115
+ // Don't mark trailing comments on the last line (they come after the node ends)
1116
+ for (idx, comment) in self.all_comments.iter().enumerate() {
1117
+ if !self.emitted_comment_indices.contains(&idx)
1118
+ && comment.location.start_line >= node.location.start_line
1119
+ && comment.location.end_line < node.location.end_line
1120
+ {
1121
+ self.emitted_comment_indices.push(idx);
1122
+ }
1123
+ }
1124
+
1125
+ // Emit trailing comments on the same line (after the node ends)
854
1126
  self.emit_trailing_comments(node.location.end_line)?;
855
1127
  }
856
1128
  }
@@ -871,12 +1143,13 @@ impl Emitter {
871
1143
  self.emit_indent(indent_level)?;
872
1144
  write!(self.buffer, "{}", text)?;
873
1145
 
874
- // Mark comments within this node's range as emitted
1146
+ // Mark comments that are strictly inside this node's line range as emitted
875
1147
  // (they are included in the source extraction)
1148
+ // Don't mark trailing comments on the last line (they come after the node ends)
876
1149
  for (idx, comment) in self.all_comments.iter().enumerate() {
877
1150
  if !self.emitted_comment_indices.contains(&idx)
878
1151
  && comment.location.start_line >= node.location.start_line
879
- && comment.location.end_line <= node.location.end_line
1152
+ && comment.location.end_line < node.location.end_line
880
1153
  {
881
1154
  self.emitted_comment_indices.push(idx);
882
1155
  }
@@ -933,6 +1206,8 @@ impl Emitter {
933
1206
  }
934
1207
  }
935
1208
 
1209
+ // Emit trailing comment on same line as while/until
1210
+ self.emit_trailing_comments(node.location.start_line)?;
936
1211
  self.buffer.push('\n');
937
1212
 
938
1213
  // Emit body - second child (StatementsNode)
@@ -1001,6 +1276,7 @@ impl Emitter {
1001
1276
  }
1002
1277
 
1003
1278
  /// Check if node is structural (part of definition syntax, not body)
1279
+ /// These nodes are part of class/module/method definitions and should not be emitted as body
1004
1280
  fn is_structural_node(&self, node_type: &NodeType) -> bool {
1005
1281
  matches!(
1006
1282
  node_type,
@@ -312,14 +312,14 @@ module Rfmt
312
312
 
313
313
  case node
314
314
  when Prism::ClassNode
315
- if (name = extract_node_name(node))
315
+ if (name = extract_class_or_module_name(node))
316
316
  metadata['name'] = name
317
317
  end
318
318
  if (superclass = extract_superclass_name(node))
319
319
  metadata['superclass'] = superclass
320
320
  end
321
321
  when Prism::ModuleNode
322
- if (name = extract_node_name(node))
322
+ if (name = extract_class_or_module_name(node))
323
323
  metadata['name'] = name
324
324
  end
325
325
  when Prism::DefNode
@@ -327,6 +327,15 @@ module Rfmt
327
327
  metadata['name'] = name
328
328
  end
329
329
  metadata['parameters_count'] = extract_parameter_count(node).to_s
330
+ # Check if this is a class method (def self.method_name)
331
+ if node.respond_to?(:receiver) && node.receiver
332
+ receiver = node.receiver
333
+ if receiver.is_a?(Prism::SelfNode)
334
+ metadata['receiver'] = 'self'
335
+ elsif receiver.respond_to?(:slice)
336
+ metadata['receiver'] = receiver.slice
337
+ end
338
+ end
330
339
  when Prism::CallNode
331
340
  if (name = extract_node_name(node))
332
341
  metadata['name'] = name
@@ -350,6 +359,9 @@ module Rfmt
350
359
  if (value = extract_literal_value(node))
351
360
  metadata['value'] = value
352
361
  end
362
+ when Prism::IfNode, Prism::UnlessNode
363
+ # Detect ternary operator: if_keyword_loc is nil for ternary
364
+ metadata['is_ternary'] = node.if_keyword_loc.nil?.to_s if node.respond_to?(:if_keyword_loc)
353
365
  end
354
366
 
355
367
  metadata
@@ -14,6 +14,31 @@ module Rfmt
14
14
  node.name.to_s
15
15
  end
16
16
 
17
+ # Extract full name from class or module node (handles namespaced names like Foo::Bar::Baz)
18
+ # @param node [Prism::ClassNode, Prism::ModuleNode] The class or module node
19
+ # @return [String, nil] The full name or nil if not available
20
+ def extract_class_or_module_name(node)
21
+ return nil unless node.respond_to?(:constant_path)
22
+
23
+ cp = node.constant_path
24
+ return node.name.to_s if cp.nil?
25
+
26
+ case cp
27
+ when Prism::ConstantReadNode
28
+ cp.name.to_s
29
+ when Prism::ConstantPathNode
30
+ if cp.respond_to?(:full_name)
31
+ cp.full_name.to_s
32
+ elsif cp.respond_to?(:slice)
33
+ cp.slice
34
+ else
35
+ cp.location.slice
36
+ end
37
+ else
38
+ node.name.to_s
39
+ end
40
+ end
41
+
17
42
  # Extract superclass name from a class node
18
43
  # @param class_node [Prism::ClassNode] The class node
19
44
  # @return [String, nil] The superclass name or nil if not available
data/lib/rfmt/rfmt.so CHANGED
Binary file
data/lib/rfmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rfmt
4
- VERSION = '1.2.2'
4
+ VERSION = '1.2.4'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora