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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Cargo.lock +1 -1
- data/ext/rfmt/Cargo.toml +1 -1
- data/ext/rfmt/src/emitter/mod.rs +164 -17
- data/lib/rfmt/cache.rb +2 -2
- data/lib/rfmt/prism_bridge.rb +14 -2
- data/lib/rfmt/prism_node_extractor.rb +25 -0
- data/lib/rfmt/rfmt.so +0 -0
- data/lib/rfmt/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5c30419e6083cc4be5e665f6cd4f5d40a725a1790df70ef78cf68638681459c
|
|
4
|
+
data.tar.gz: 066f373ebe30238a91b34a3d01acccdfbbd270256f8c0e7ba5c909e280e009b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/ext/rfmt/Cargo.toml
CHANGED
data/ext/rfmt/src/emitter/mod.rs
CHANGED
|
@@ -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
|
|
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
|
-
.
|
|
235
|
-
|
|
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
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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 '
|
|
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::
|
|
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
|
data/lib/rfmt/prism_bridge.rb
CHANGED
|
@@ -312,14 +312,14 @@ module Rfmt
|
|
|
312
312
|
|
|
313
313
|
case node
|
|
314
314
|
when Prism::ClassNode
|
|
315
|
-
if (name =
|
|
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 =
|
|
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