rfmt 1.2.1 → 1.2.3

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: 79d925cc8ef6ccbc3509abb49f62ef9e1129f40dcb81bf3d2cf27e4148bbb32c
4
- data.tar.gz: 7519420a2f5fdddc3da5bfe0f89cb7e39ea5b244aefe46049362f143dd58d547
3
+ metadata.gz: b5c30419e6083cc4be5e665f6cd4f5d40a725a1790df70ef78cf68638681459c
4
+ data.tar.gz: 066f373ebe30238a91b34a3d01acccdfbbd270256f8c0e7ba5c909e280e009b0
5
5
  SHA512:
6
- metadata.gz: 39209b9927d02d53345484f43454b3e633e2d6c3839c1d472b31052dd97bf137835778034c1b734527a3296a530d6cffebb8fe6f398075d846beeabac82d2d1d
7
- data.tar.gz: 45de5f91b06fe9c5294742adcc8c91b401f085c1b2bf4b8179efd396ff99936baff8bfd3f82d804f743a100b3cde75597b15dc7aec4d3decda7acaff6d51744c
6
+ metadata.gz: c17af15dfa819de322a14bde2c318f85d7db27f8d2446f1793550dac256e3309bf4cc3b30269c86cf5bd8aef4269f4666040d7e163b078902c4f75197e72424e
7
+ data.tar.gz: c3d5a19ed768dcbd8616321dcdfb4374645e933d1eb89e0530683ff28a4f6aec848870f263d94b69a123315c5f505587d183e8c8fca13c6cbc2e406278c8e3c7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.3] - 2026-01-04
4
+
5
+ ### Fixed
6
+ - Fix migration file formatting (`emit_rescue` handling for rescue blocks)
7
+
8
+ ## [1.2.2] - 2026-01-04
9
+
10
+ ### Fixed
11
+ - Ruby 3.4.1 compatibility: Use `OpenSSL::Digest::SHA256` instead of `Digest::SHA2` to avoid `metadata is not initialized properly` error in Ruby 3.4.1
12
+
3
13
  ## [1.2.1] - 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.1"
1222
+ version = "1.2.3"
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.1"
3
+ version = "1.2.3"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -50,6 +50,9 @@ impl Emitter {
50
50
 
51
51
  self.emit_node(ast, 0)?;
52
52
 
53
+ // Emit any remaining comments that weren't emitted
54
+ self.emit_remaining_comments()?;
55
+
53
56
  // Ensure file ends with a newline
54
57
  if !self.buffer.ends_with('\n') {
55
58
  self.buffer.push('\n');
@@ -58,6 +61,27 @@ impl Emitter {
58
61
  Ok(self.buffer.clone())
59
62
  }
60
63
 
64
+ /// Emit all comments that haven't been emitted yet
65
+ fn emit_remaining_comments(&mut self) -> Result<()> {
66
+ let mut last_end_line: Option<usize> = None;
67
+ for (idx, comment) in self.all_comments.iter().enumerate() {
68
+ if self.emitted_comment_indices.contains(&idx) {
69
+ continue;
70
+ }
71
+ // Preserve blank lines between comments
72
+ if let Some(prev_line) = last_end_line {
73
+ let gap = comment.location.start_line.saturating_sub(prev_line);
74
+ for _ in 1..gap {
75
+ self.buffer.push('\n');
76
+ }
77
+ }
78
+ writeln!(self.buffer, "{}", comment.text)?;
79
+ self.emitted_comment_indices.push(idx);
80
+ last_end_line = Some(comment.location.end_line);
81
+ }
82
+ Ok(())
83
+ }
84
+
61
85
  /// Recursively collect all comments from the AST
62
86
  fn collect_comments(&mut self, node: &Node) {
63
87
  self.all_comments.extend(node.comments.clone());
@@ -218,23 +242,28 @@ impl Emitter {
218
242
  write!(self.buffer, " < {}", superclass)?;
219
243
  }
220
244
 
245
+ // Emit trailing comments on the class definition line (e.g., # rubocop:disable)
246
+ self.emit_trailing_comments(node.location.start_line)?;
221
247
  self.buffer.push('\n');
222
248
 
223
- // Emit body (children), but skip structural nodes like constant_read_node
249
+ // Emit body (children), but skip structural nodes (class name, superclass)
250
+ // Use start_line check to properly handle CallNode superclasses like ActiveRecord::Migration[8.0]
251
+ let class_start_line = node.location.start_line;
224
252
  for child in &node.children {
253
+ // Skip nodes on the same line as class definition (name, superclass)
254
+ if child.location.start_line == class_start_line {
255
+ continue;
256
+ }
225
257
  if self.is_structural_node(&child.node_type) {
226
258
  continue;
227
259
  }
228
260
  self.emit_node(child, indent_level + 1)?;
229
- // Note: don't add newline here, statements node will handle it
230
261
  }
231
262
 
232
263
  // 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))
237
- {
264
+ if node.children.iter().any(|c| {
265
+ c.location.start_line != class_start_line && !self.is_structural_node(&c.node_type)
266
+ }) {
238
267
  self.buffer.push('\n');
239
268
  }
240
269
 
@@ -256,6 +285,8 @@ impl Emitter {
256
285
  write!(self.buffer, "{}", name)?;
257
286
  }
258
287
 
288
+ // Emit trailing comments on the module definition line
289
+ self.emit_trailing_comments(node.location.start_line)?;
259
290
  self.buffer.push('\n');
260
291
 
261
292
  // Emit body (children), but skip structural nodes
@@ -289,6 +320,11 @@ impl Emitter {
289
320
  self.emit_indent(indent_level)?;
290
321
  write!(self.buffer, "def ")?;
291
322
 
323
+ // Handle class methods (def self.method_name)
324
+ if let Some(receiver) = node.metadata.get("receiver") {
325
+ write!(self.buffer, "{}.", receiver)?;
326
+ }
327
+
292
328
  if let Some(name) = node.metadata.get("name") {
293
329
  write!(self.buffer, "{}", name)?;
294
330
  }
@@ -321,6 +357,8 @@ impl Emitter {
321
357
  }
322
358
  }
323
359
 
360
+ // Emit trailing comment on same line as def
361
+ self.emit_trailing_comments(node.location.start_line)?;
324
362
  self.buffer.push('\n');
325
363
 
326
364
  // Emit body (children), but skip structural nodes like parameter nodes
@@ -398,28 +436,77 @@ impl Emitter {
398
436
  write!(self.buffer, "rescue")?;
399
437
 
400
438
  // Extract exception classes and variable from source
439
+ // Handle multi-line rescue clauses (e.g., multiple exception classes spanning lines)
401
440
  if !self.source.is_empty() && node.location.end_offset <= self.source.len() {
402
441
  if let Some(source_text) = self
403
442
  .source
404
443
  .get(node.location.start_offset..node.location.end_offset)
405
444
  {
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)?;
445
+ // Find the rescue declaration part (first line only, unless trailing comma/backslash)
446
+ let mut rescue_decl = String::new();
447
+ let mut expect_continuation = false;
448
+
449
+ for line in source_text.lines() {
450
+ let trimmed = line.trim();
451
+
452
+ if rescue_decl.is_empty() {
453
+ // First line - remove "rescue" prefix
454
+ let after_rescue = trimmed.trim_start_matches("rescue").trim();
455
+ if !after_rescue.is_empty() {
456
+ // Check if line ends with continuation marker
457
+ expect_continuation =
458
+ after_rescue.ends_with(',') || after_rescue.ends_with('\\');
459
+ rescue_decl.push_str(after_rescue.trim_end_matches('\\').trim());
460
+ }
461
+ if !expect_continuation {
462
+ break;
463
+ }
464
+ } else if expect_continuation {
465
+ // Continuation line after trailing comma or backslash
466
+ if !rescue_decl.ends_with(' ') && !rescue_decl.ends_with(',') {
467
+ rescue_decl.push(' ');
468
+ }
469
+ let content = trimmed.trim_end_matches('\\').trim();
470
+ rescue_decl.push_str(content);
471
+ expect_continuation = trimmed.ends_with(',') || trimmed.ends_with('\\');
472
+ if !expect_continuation {
473
+ break;
474
+ }
475
+ } else {
476
+ break;
412
477
  }
413
478
  }
479
+
480
+ if !rescue_decl.is_empty() {
481
+ write!(self.buffer, " {}", rescue_decl)?;
482
+ }
414
483
  }
415
484
  }
416
485
 
417
486
  self.buffer.push('\n');
418
487
 
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)?;
488
+ // Emit rescue body and handle subsequent rescue nodes
489
+ // Children structure:
490
+ // - ConstantReadNode/ConstantPathNode (exception classes)
491
+ // - LocalVariableTargetNode (optional, exception variable)
492
+ // - StatementsNode (rescue body)
493
+ // - RescueNode (optional, subsequent rescue clause)
494
+ for child in &node.children {
495
+ match &child.node_type {
496
+ NodeType::StatementsNode => {
497
+ self.emit_node(child, indent_level)?;
498
+ }
499
+ NodeType::RescueNode => {
500
+ // Emit subsequent rescue clause
501
+ // Ensure newline before subsequent rescue
502
+ if !self.buffer.ends_with('\n') {
503
+ self.buffer.push('\n');
504
+ }
505
+ self.emit_rescue(child, indent_level)?;
506
+ }
507
+ _ => {
508
+ // Skip exception classes and variable (already handled above)
509
+ }
423
510
  }
424
511
  }
425
512
 
@@ -612,6 +699,59 @@ impl Emitter {
612
699
  return Ok(());
613
700
  }
614
701
 
702
+ // Check for ternary operator
703
+ let is_ternary = node
704
+ .metadata
705
+ .get("is_ternary")
706
+ .map(|v| v == "true")
707
+ .unwrap_or(false);
708
+
709
+ if is_ternary && !is_elsif {
710
+ self.emit_comments_before(node.location.start_line, indent_level)?;
711
+ self.emit_indent(indent_level)?;
712
+
713
+ // Emit condition
714
+ if let Some(predicate) = node.children.first() {
715
+ if !self.source.is_empty() {
716
+ let start = predicate.location.start_offset;
717
+ let end = predicate.location.end_offset;
718
+ if let Some(text) = self.source.get(start..end) {
719
+ write!(self.buffer, "{}", text)?;
720
+ }
721
+ }
722
+ }
723
+
724
+ write!(self.buffer, " ? ")?;
725
+
726
+ // Emit then expression
727
+ if let Some(statements) = node.children.get(1) {
728
+ if !self.source.is_empty() {
729
+ let start = statements.location.start_offset;
730
+ let end = statements.location.end_offset;
731
+ if let Some(text) = self.source.get(start..end) {
732
+ write!(self.buffer, "{}", text.trim())?;
733
+ }
734
+ }
735
+ }
736
+
737
+ write!(self.buffer, " : ")?;
738
+
739
+ // Emit else expression
740
+ if let Some(else_node) = node.children.get(2) {
741
+ if let Some(else_statements) = else_node.children.first() {
742
+ if !self.source.is_empty() {
743
+ let start = else_statements.location.start_offset;
744
+ let end = else_statements.location.end_offset;
745
+ if let Some(text) = self.source.get(start..end) {
746
+ write!(self.buffer, "{}", text.trim())?;
747
+ }
748
+ }
749
+ }
750
+ }
751
+
752
+ return Ok(());
753
+ }
754
+
615
755
  // Normal if/unless/elsif
616
756
  if !is_elsif {
617
757
  self.emit_comments_before(node.location.start_line, indent_level)?;
@@ -637,6 +777,8 @@ impl Emitter {
637
777
  }
638
778
  }
639
779
 
780
+ // Emit trailing comment on same line as if/unless/elsif
781
+ self.emit_trailing_comments(node.location.start_line)?;
640
782
  self.buffer.push('\n');
641
783
 
642
784
  // Emit then clause (second child is StatementsNode)
@@ -760,6 +902,8 @@ impl Emitter {
760
902
  // Emit block parameters if present (|x, y|)
761
903
  self.emit_block_parameters(block_node)?;
762
904
 
905
+ // Emit trailing comment on same line as do |...|
906
+ self.emit_trailing_comments(block_node.location.start_line)?;
763
907
  self.buffer.push('\n');
764
908
 
765
909
  // Find and emit the body (StatementsNode among children)
@@ -933,6 +1077,8 @@ impl Emitter {
933
1077
  }
934
1078
  }
935
1079
 
1080
+ // Emit trailing comment on same line as while/until
1081
+ self.emit_trailing_comments(node.location.start_line)?;
936
1082
  self.buffer.push('\n');
937
1083
 
938
1084
  // Emit body - second child (StatementsNode)
@@ -1001,6 +1147,7 @@ impl Emitter {
1001
1147
  }
1002
1148
 
1003
1149
  /// Check if node is structural (part of definition syntax, not body)
1150
+ /// These nodes are part of class/module/method definitions and should not be emitted as body
1004
1151
  fn is_structural_node(&self, node_type: &NodeType) -> bool {
1005
1152
  matches!(
1006
1153
  node_type,
data/lib/rfmt/cache.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest/sha2'
3
+ require 'openssl'
4
4
  require 'json'
5
5
  require 'fileutils'
6
6
 
@@ -105,7 +105,7 @@ module Rfmt
105
105
 
106
106
  def file_hash(file_path)
107
107
  content = File.read(file_path)
108
- Digest::SHA2.new(256).hexdigest(content)
108
+ OpenSSL::Digest::SHA256.hexdigest(content)
109
109
  rescue StandardError => e
110
110
  raise CacheError, "Failed to read file #{file_path}: #{e.message}"
111
111
  end
@@ -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.1'
4
+ VERSION = '1.2.3'
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.1
4
+ version: 1.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora