rfmt 1.4.1 → 1.5.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: a17203d9ab18d011d42790fc039889490f78bd9c1eb86701a15e15784bfafd4c
4
- data.tar.gz: ae34f8b79a4d7e64fec13ea9b1a24ae8d1b46f03aeb4c64f302e52a3895c62e6
3
+ metadata.gz: 2df39db6eb525d4ca6bc42d55e01057e5a12772f0b61a7a8e3101e3a8fff76ec
4
+ data.tar.gz: b34bbf251457c8eb0d9174497c124f71d6a7b098ad506712acaec54812bcc674
5
5
  SHA512:
6
- metadata.gz: 390bd25696b21224e5c182b6516a4a1733281a93bae01eb3ae58a787f3d53b430e308a760d1b47adc1389f2bd389b544aa6fe41e6990d0de654c5ead89d8e31f
7
- data.tar.gz: f638716804d25efc8d74cd18c6a2c1e014d459a63a91dea4968ff2e1f21fee4c51890781051e6c92e2c5cac6e924687b531d9ac412d1e9eae6bfef8155b1b166
6
+ metadata.gz: 1d341d108c8875f683a2ef3654e6e540b1fe9840a4f908abc75725da544c0b8be2b27cf5534cdf634eb2e9eaaa36bed0df9500f978a6fffc95ec7379e2485cf3
7
+ data.tar.gz: 9cbce53637c758998c79c77af60d10130dd293dac67c4a7918d882e816d848ef525400bd6fcebc030523a2407e08140dc1bc9227cbd9c1cfc3f579f0e5b477be
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.5.0] - 2026-01-25
4
+
5
+ ### Added
6
+ - Docker Compose test environment setup (#84)
7
+ - Support for `then` expression emission (#80)
8
+ - CI support for Ruby 3.4 and Ruby 4 (#82)
9
+
10
+ ### Changed
11
+ - Upgrade Magnus (Rust-Ruby FFI library) (#83)
12
+ - Optimize Docker build with multi-stage and caching (#84)
13
+ - Upgrade unicode-emoji dependency
14
+
15
+ ### Fixed
16
+ - Preserve heredoc content and closing identifier (#81)
17
+ - Fix `rescue`/`ensure` clauses being deleted inside `do...end` blocks (#78)
18
+ - Fix inline comment node handling (#77)
19
+ - Fix BTreeMap range error (Issue #71)
20
+
3
21
  ## [1.4.1] - 2026-01-17
4
22
 
5
23
  ### Fixed
data/Cargo.lock CHANGED
@@ -838,9 +838,9 @@ dependencies = [
838
838
 
839
839
  [[package]]
840
840
  name = "magnus"
841
- version = "0.6.4"
841
+ version = "0.8.2"
842
842
  source = "registry+https://github.com/rust-lang/crates.io-index"
843
- checksum = "b1597ef40aa8c36be098249e82c9a20cf7199278ac1c1a1a995eeead6a184479"
843
+ checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
844
844
  dependencies = [
845
845
  "magnus-macros",
846
846
  "rb-sys",
@@ -850,9 +850,9 @@ dependencies = [
850
850
 
851
851
  [[package]]
852
852
  name = "magnus-macros"
853
- version = "0.6.0"
853
+ version = "0.8.0"
854
854
  source = "registry+https://github.com/rust-lang/crates.io-index"
855
- checksum = "5968c820e2960565f647819f5928a42d6e874551cab9d88d75e3e0660d7f71e3"
855
+ checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
856
856
  dependencies = [
857
857
  "proc-macro2",
858
858
  "quote",
@@ -1168,9 +1168,9 @@ dependencies = [
1168
1168
 
1169
1169
  [[package]]
1170
1170
  name = "rb-sys-env"
1171
- version = "0.1.2"
1171
+ version = "0.2.3"
1172
1172
  source = "registry+https://github.com/rust-lang/crates.io-index"
1173
- checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
1173
+ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
1174
1174
 
1175
1175
  [[package]]
1176
1176
  name = "redox_users"
@@ -1214,7 +1214,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
1214
1214
 
1215
1215
  [[package]]
1216
1216
  name = "rfmt"
1217
- version = "1.4.1"
1217
+ version = "1.5.0"
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.4.1"
3
+ version = "1.5.0"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -12,8 +12,8 @@ crate-type = ["cdylib"]
12
12
 
13
13
  [dependencies]
14
14
  # Ruby FFI
15
- magnus = { version = "0.6.2" }
16
- rb-sys = "0.9.117"
15
+ magnus = { version = "0.8.2" }
16
+ rb-sys = "0.9.124"
17
17
 
18
18
  # Serialization
19
19
  serde = { version = "1.0", features = ["derive"] }
@@ -144,6 +144,10 @@ impl Emitter {
144
144
  /// Get comment indices in the given line range [start_line, end_line)
145
145
  /// Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
146
146
  fn get_comment_indices_in_range(&self, start_line: usize, end_line: usize) -> Vec<usize> {
147
+ // Guard against invalid range (e.g., endless methods where start_line >= end_line)
148
+ if start_line >= end_line {
149
+ return Vec::new();
150
+ }
147
151
  self.comments_by_line
148
152
  .range(start_line..end_line)
149
153
  .flat_map(|(_, indices)| indices.iter().copied())
@@ -228,6 +232,10 @@ impl Emitter {
228
232
  /// Check if there are any unemitted comments in the given line range
229
233
  /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
230
234
  fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
235
+ // Guard against invalid range (e.g., endless methods where start_line >= end_line)
236
+ if start_line >= end_line {
237
+ return false;
238
+ }
231
239
  self.comments_by_line
232
240
  .range(start_line..end_line)
233
241
  .flat_map(|(_, indices)| indices.iter())
@@ -344,6 +352,31 @@ impl Emitter {
344
352
  false
345
353
  }
346
354
 
355
+ /// Check if the node spans only a single line
356
+ fn is_single_line(&self, node: &Node) -> bool {
357
+ node.location.start_line == node.location.end_line
358
+ }
359
+
360
+ /// Extract and write source text for a node
361
+ fn write_source_text(&mut self, node: &Node) -> Result<()> {
362
+ let start = node.location.start_offset;
363
+ let end = node.location.end_offset;
364
+ if let Some(text) = self.source.get(start..end) {
365
+ write!(self.buffer, "{}", text)?;
366
+ }
367
+ Ok(())
368
+ }
369
+
370
+ /// Extract and write trimmed source text for a node
371
+ fn write_source_text_trimmed(&mut self, node: &Node) -> Result<()> {
372
+ let start = node.location.start_offset;
373
+ let end = node.location.end_offset;
374
+ if let Some(text) = self.source.get(start..end) {
375
+ write!(self.buffer, "{}", text.trim())?;
376
+ }
377
+ Ok(())
378
+ }
379
+
347
380
  /// Emit comments that are within a given line range, preserving blank lines from prev_line
348
381
  /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
349
382
  fn emit_comments_in_range_with_prev_line(
@@ -899,25 +932,30 @@ impl Emitter {
899
932
 
900
933
  // Emit conditions with comma separator
901
934
  for (i, cond) in conditions.iter().enumerate() {
902
- let start = cond.location.start_offset;
903
- let end = cond.location.end_offset;
904
- if let Some(text) = self.source.get(start..end) {
905
- write!(self.buffer, "{}", text)?;
906
- }
935
+ self.write_source_text(cond)?;
907
936
  if i < conditions.len() - 1 {
908
937
  write!(self.buffer, ", ")?;
909
938
  }
910
939
  }
911
940
 
912
- self.buffer.push('\n');
913
-
914
- // Emit statements body
915
- if let Some(statements) = node
941
+ let statements = node
916
942
  .children
917
943
  .iter()
918
- .find(|c| matches!(c.node_type, NodeType::StatementsNode))
919
- {
920
- self.emit_statements(statements, indent_level + 1)?;
944
+ .find(|c| matches!(c.node_type, NodeType::StatementsNode));
945
+
946
+ if self.is_single_line(node) {
947
+ // Inline style: when X then Y
948
+ if let Some(statements) = statements {
949
+ write!(self.buffer, " then ")?;
950
+ self.write_source_text(statements)?;
951
+ }
952
+ } else {
953
+ // Multi-line style: when X\n Y
954
+ self.buffer.push('\n');
955
+
956
+ if let Some(statements) = statements {
957
+ self.emit_statements(statements, indent_level + 1)?;
958
+ }
921
959
  }
922
960
 
923
961
  Ok(())
@@ -951,14 +989,7 @@ impl Emitter {
951
989
  // Emit statement
952
990
  if let Some(statements) = node.children.get(1) {
953
991
  if matches!(statements.node_type, NodeType::StatementsNode) {
954
- // Extract the statement text (without extra indentation)
955
- if !self.source.is_empty() {
956
- let start = statements.location.start_offset;
957
- let end = statements.location.end_offset;
958
- if let Some(text) = self.source.get(start..end) {
959
- write!(self.buffer, "{}", text.trim())?;
960
- }
961
- }
992
+ self.write_source_text_trimmed(statements)?;
962
993
  }
963
994
  }
964
995
 
@@ -966,13 +997,7 @@ impl Emitter {
966
997
 
967
998
  // Emit condition
968
999
  if let Some(predicate) = node.children.first() {
969
- if !self.source.is_empty() {
970
- let start = predicate.location.start_offset;
971
- let end = predicate.location.end_offset;
972
- if let Some(text) = self.source.get(start..end) {
973
- write!(self.buffer, "{}", text)?;
974
- }
975
- }
1000
+ self.write_source_text(predicate)?;
976
1001
  }
977
1002
 
978
1003
  return Ok(());
@@ -991,26 +1016,14 @@ impl Emitter {
991
1016
 
992
1017
  // Emit condition
993
1018
  if let Some(predicate) = node.children.first() {
994
- if !self.source.is_empty() {
995
- let start = predicate.location.start_offset;
996
- let end = predicate.location.end_offset;
997
- if let Some(text) = self.source.get(start..end) {
998
- write!(self.buffer, "{}", text)?;
999
- }
1000
- }
1019
+ self.write_source_text(predicate)?;
1001
1020
  }
1002
1021
 
1003
1022
  write!(self.buffer, " ? ")?;
1004
1023
 
1005
1024
  // Emit then expression
1006
1025
  if let Some(statements) = node.children.get(1) {
1007
- if !self.source.is_empty() {
1008
- let start = statements.location.start_offset;
1009
- let end = statements.location.end_offset;
1010
- if let Some(text) = self.source.get(start..end) {
1011
- write!(self.buffer, "{}", text.trim())?;
1012
- }
1013
- }
1026
+ self.write_source_text_trimmed(statements)?;
1014
1027
  }
1015
1028
 
1016
1029
  write!(self.buffer, " : ")?;
@@ -1018,19 +1031,40 @@ impl Emitter {
1018
1031
  // Emit else expression
1019
1032
  if let Some(else_node) = node.children.get(2) {
1020
1033
  if let Some(else_statements) = else_node.children.first() {
1021
- if !self.source.is_empty() {
1022
- let start = else_statements.location.start_offset;
1023
- let end = else_statements.location.end_offset;
1024
- if let Some(text) = self.source.get(start..end) {
1025
- write!(self.buffer, "{}", text.trim())?;
1026
- }
1027
- }
1034
+ self.write_source_text_trimmed(else_statements)?;
1028
1035
  }
1029
1036
  }
1030
1037
 
1031
1038
  return Ok(());
1032
1039
  }
1033
1040
 
1041
+ // Check for inline then style: "if true then 1 end"
1042
+ // Single line, not postfix, not ternary, no else clause
1043
+ let is_inline_then =
1044
+ !is_elsif && self.is_single_line(node) && node.children.get(2).is_none();
1045
+
1046
+ if is_inline_then {
1047
+ self.emit_comments_before(node.location.start_line, indent_level)?;
1048
+ self.emit_indent(indent_level)?;
1049
+ write!(self.buffer, "{} ", keyword)?;
1050
+
1051
+ // Emit condition
1052
+ if let Some(predicate) = node.children.first() {
1053
+ self.write_source_text(predicate)?;
1054
+ }
1055
+
1056
+ write!(self.buffer, " then ")?;
1057
+
1058
+ // Emit statement
1059
+ if let Some(statements) = node.children.get(1) {
1060
+ self.write_source_text_trimmed(statements)?;
1061
+ }
1062
+
1063
+ write!(self.buffer, " end")?;
1064
+ self.emit_trailing_comments(node.location.end_line)?;
1065
+ return Ok(());
1066
+ }
1067
+
1034
1068
  // Normal if/unless/elsif
1035
1069
  if !is_elsif {
1036
1070
  self.emit_comments_before(node.location.start_line, indent_level)?;
@@ -1046,14 +1080,7 @@ impl Emitter {
1046
1080
 
1047
1081
  // Emit predicate (condition) - first child
1048
1082
  if let Some(predicate) = node.children.first() {
1049
- // Extract predicate from source
1050
- if !self.source.is_empty() {
1051
- let start = predicate.location.start_offset;
1052
- let end = predicate.location.end_offset;
1053
- if let Some(text) = self.source.get(start..end) {
1054
- write!(self.buffer, "{}", text)?;
1055
- }
1056
- }
1083
+ self.write_source_text(predicate)?;
1057
1084
  }
1058
1085
 
1059
1086
  // Emit trailing comment on same line as if/unless/elsif
@@ -1193,14 +1220,27 @@ impl Emitter {
1193
1220
  let mut last_stmt_end_line = block_start_line;
1194
1221
 
1195
1222
  for child in &block_node.children {
1196
- if matches!(child.node_type, NodeType::StatementsNode) {
1197
- self.emit_statements(child, indent_level + 1)?;
1198
- // Track the last statement's end line for blank line preservation
1199
- if let Some(last_child) = child.children.last() {
1200
- last_stmt_end_line = last_child.location.end_line;
1223
+ match &child.node_type {
1224
+ NodeType::StatementsNode => {
1225
+ self.emit_statements(child, indent_level + 1)?;
1226
+ // Track the last statement's end line for blank line preservation
1227
+ if let Some(last_child) = child.children.last() {
1228
+ last_stmt_end_line = last_child.location.end_line;
1229
+ }
1230
+ self.buffer.push('\n');
1231
+ break;
1232
+ }
1233
+ NodeType::BeginNode => {
1234
+ // Block with rescue/ensure/else - delegate to emit_begin
1235
+ // which handles implicit begin (no "begin" keyword)
1236
+ self.emit_begin(child, indent_level + 1)?;
1237
+ self.buffer.push('\n');
1238
+ last_stmt_end_line = child.location.end_line;
1239
+ break;
1240
+ }
1241
+ _ => {
1242
+ // Skip parameter nodes
1201
1243
  }
1202
- self.buffer.push('\n');
1203
- break;
1204
1244
  }
1205
1245
  }
1206
1246
 
@@ -1236,6 +1276,7 @@ impl Emitter {
1236
1276
  fn emit_brace_block(&mut self, block_node: &Node, indent_level: usize) -> Result<()> {
1237
1277
  // Determine if block should be inline or multiline
1238
1278
  let is_multiline = block_node.location.start_line != block_node.location.end_line;
1279
+ let block_end_line = block_node.location.end_line;
1239
1280
 
1240
1281
  if is_multiline {
1241
1282
  // Multiline brace block
@@ -1254,6 +1295,7 @@ impl Emitter {
1254
1295
 
1255
1296
  self.emit_indent(indent_level)?;
1256
1297
  write!(self.buffer, "}}")?;
1298
+ self.emit_trailing_comments(block_end_line)?;
1257
1299
  } else {
1258
1300
  // Inline brace block - extract from source to preserve spacing
1259
1301
  write!(self.buffer, " ")?;
@@ -1263,6 +1305,7 @@ impl Emitter {
1263
1305
  {
1264
1306
  write!(self.buffer, "{}", text)?;
1265
1307
  }
1308
+ self.emit_trailing_comments(block_end_line)?;
1266
1309
  }
1267
1310
 
1268
1311
  Ok(())
@@ -1571,7 +1614,6 @@ impl Emitter {
1571
1614
  match &child.node_type {
1572
1615
  NodeType::InNode => {
1573
1616
  self.emit_in(child, indent_level)?;
1574
- self.buffer.push('\n');
1575
1617
  }
1576
1618
  NodeType::ElseNode => {
1577
1619
  self.emit_indent(indent_level)?;
@@ -1607,19 +1649,25 @@ impl Emitter {
1607
1649
 
1608
1650
  // First child is the pattern
1609
1651
  if let Some(pattern) = node.children.first() {
1610
- let start = pattern.location.start_offset;
1611
- let end = pattern.location.end_offset;
1612
- if let Some(text) = self.source.get(start..end) {
1613
- write!(self.buffer, "{}", text)?;
1614
- }
1652
+ self.write_source_text(pattern)?;
1615
1653
  }
1616
1654
 
1617
- self.buffer.push('\n');
1655
+ if self.is_single_line(node) {
1656
+ // Inline style: in X then Y
1657
+ if let Some(statements) = node.children.get(1) {
1658
+ write!(self.buffer, " then ")?;
1659
+ self.write_source_text(statements)?;
1660
+ }
1661
+ self.buffer.push('\n');
1662
+ } else {
1663
+ // Multi-line style: in X\n Y
1664
+ self.buffer.push('\n');
1618
1665
 
1619
- // Second child is the statements body
1620
- if let Some(statements) = node.children.get(1) {
1621
- if matches!(statements.node_type, NodeType::StatementsNode) {
1622
- self.emit_statements(statements, indent_level + 1)?;
1666
+ // Second child is the statements body
1667
+ if let Some(statements) = node.children.get(1) {
1668
+ if matches!(statements.node_type, NodeType::StatementsNode) {
1669
+ self.emit_statements(statements, indent_level + 1)?;
1670
+ }
1623
1671
  }
1624
1672
  }
1625
1673
 
data/ext/rfmt/src/lib.rs CHANGED
@@ -10,7 +10,7 @@ use policy::SecurityPolicy;
10
10
 
11
11
  use config::Config;
12
12
  use emitter::Emitter;
13
- use magnus::{define_module, function, prelude::*, Error, Ruby};
13
+ use magnus::{function, prelude::*, Error, Ruby};
14
14
  use parser::{PrismAdapter, RubyParser};
15
15
 
16
16
  fn format_ruby_code(ruby: &Ruby, source: String, json: String) -> Result<String, Error> {
@@ -45,10 +45,10 @@ fn rust_version() -> String {
45
45
  }
46
46
 
47
47
  #[magnus::init]
48
- fn init(_ruby: &Ruby) -> Result<(), Error> {
48
+ fn init(ruby: &Ruby) -> Result<(), Error> {
49
49
  logging::RfmtLogger::init();
50
50
 
51
- let module = define_module("Rfmt")?;
51
+ let module = ruby.define_module("Rfmt")?;
52
52
 
53
53
  module.define_singleton_method("format_code", function!(format_ruby_code, 2))?;
54
54
  module.define_singleton_method("parse_to_json", function!(parse_to_json, 1))?;
@@ -108,13 +108,42 @@ module Rfmt
108
108
  # Extract location information from node
109
109
  def self.extract_location(node)
110
110
  loc = node.location
111
+
112
+ # For heredoc nodes, the location only covers the opening tag (<<~CSV)
113
+ # We need to find the maximum end_offset including closing_loc
114
+ end_offset = loc.end_offset
115
+ end_line = loc.end_line
116
+ end_column = loc.end_column
117
+
118
+ # Check this node's closing_loc
119
+ if node.respond_to?(:closing_loc) && node.closing_loc
120
+ closing = node.closing_loc
121
+ if closing.end_offset > end_offset
122
+ end_offset = closing.end_offset
123
+ end_line = closing.end_line
124
+ end_column = closing.end_column
125
+ end
126
+ end
127
+
128
+ # Check child nodes for heredoc (e.g., LocalVariableWriteNode containing StringNode)
129
+ node.child_nodes.compact.each do |child|
130
+ next unless child.respond_to?(:closing_loc) && child.closing_loc
131
+
132
+ closing = child.closing_loc
133
+ next unless closing.end_offset > end_offset
134
+
135
+ end_offset = closing.end_offset
136
+ end_line = closing.end_line
137
+ end_column = closing.end_column
138
+ end
139
+
111
140
  {
112
141
  start_line: loc.start_line,
113
142
  start_column: loc.start_column,
114
- end_line: loc.end_line,
115
- end_column: loc.end_column,
143
+ end_line: end_line,
144
+ end_column: end_column,
116
145
  start_offset: loc.start_offset,
117
- end_offset: loc.end_offset
146
+ end_offset: end_offset
118
147
  }
119
148
  end
120
149
 
@@ -176,6 +205,7 @@ module Rfmt
176
205
  [
177
206
  node.statements,
178
207
  node.rescue_clause,
208
+ node.else_clause,
179
209
  node.ensure_clause
180
210
  ].compact
181
211
  when Prism::EnsureNode
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.4.1'
4
+ VERSION = '1.5.0'
5
5
  end
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.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-01-17 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: diff-lcs
@@ -113,7 +112,6 @@ metadata:
113
112
  source_code_uri: https://github.com/fs0414/rfmt
114
113
  changelog_uri: https://github.com/fs0414/rfmt/releases
115
114
  ruby_lsp_addon: 'true'
116
- post_install_message:
117
115
  rdoc_options: []
118
116
  require_paths:
119
117
  - lib
@@ -128,8 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
126
  - !ruby/object:Gem::Version
129
127
  version: 3.0.0
130
128
  requirements: []
131
- rubygems_version: 3.5.22
132
- signing_key:
129
+ rubygems_version: 3.6.9
133
130
  specification_version: 4
134
131
  summary: Ruby Formatter impl Rust lang.
135
132
  test_files: []