rfmt 1.3.1 → 1.3.2

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: 3711e11ba344ee0b9e1e76d0885fd99bd7ccbd7901ab5ba819619ae40647365f
4
- data.tar.gz: 95f1b17c4c9a542cc24d9b89df6dee44398f59d27da0446961ac320b338a6bb4
3
+ metadata.gz: 79f5e6c24f82f6552874aefda2a6b97d8f4612419810d76ebd9381e862edded4
4
+ data.tar.gz: f193dab59e9a02f62d3e599c8f0349ebbf6e7fda070ba55990e9a8d399990f72
5
5
  SHA512:
6
- metadata.gz: 7bb8aff9825e2a8d44f3161786b7952a2b2e3b679d0e607eb748102971d4c8a417ca8dcaac80e171a97ef223814c229b14b63af505321309393edc9432550fe6
7
- data.tar.gz: 110a42ff056ac3dd6939d0f67b50a4784bbdafd513c4c0a1ab6e9eb8d0a040a0aa65bc39d0b37975ffb1972cf81d8396868d33e627b71ed017894cf1674d3cc4
6
+ metadata.gz: 6ebfd2c04793445912e1edc28db74e5cfa218fcd3fc28f6e3b42a88b1794fe0bfc6643890fa34477291c7ad8624696f3bb7207436ecfeb6f756439be5d9065ae
7
+ data.tar.gz: e209cf0a8671786af5e1560979f70f5fe8bb4e2de3912613a59e6d6269effe54f123407ca8989b50578f330d16e8a0d2fa7a1b47c8c07d5731e893664892fe1a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.2] - 2026-01-09
4
+
5
+ ### Added
6
+ - Implement `std::str::FromStr` trait for `NodeType`
7
+ - Unit tests for validation module
8
+
9
+ ### Changed
10
+ - Use serde enum for comment type deserialization (type-safe JSON parsing)
11
+ - Convert recursive `find_last_code_line` to iterative approach (prevent stack overflow)
12
+ - Use BTreeMap index for comment lookup in `emit_statements` (O(n) → O(log n))
13
+
14
+ ### Fixed
15
+ - Remove panic-prone `unwrap()` on Mutex lock in logger (prevent Ruby VM crash)
16
+
3
17
  ## [1.3.1] - 2026-01-08
4
18
 
5
19
  ### Added
data/Cargo.lock CHANGED
@@ -1214,7 +1214,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
1214
1214
 
1215
1215
  [[package]]
1216
1216
  name = "rfmt"
1217
- version = "1.3.1"
1217
+ version = "1.3.2"
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.1"
3
+ version = "1.3.2"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -235,9 +235,11 @@ pub enum NodeType {
235
235
  Unknown(String),
236
236
  }
237
237
 
238
- impl NodeType {
239
- pub fn from_str(s: &str) -> Self {
240
- match s {
238
+ impl std::str::FromStr for NodeType {
239
+ type Err = std::convert::Infallible;
240
+
241
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
242
+ Ok(match s {
241
243
  "program_node" => Self::ProgramNode,
242
244
  "statements_node" => Self::StatementsNode,
243
245
  "class_node" => Self::ClassNode,
@@ -383,7 +385,14 @@ impl NodeType {
383
385
  "implicit_node" => Self::ImplicitNode,
384
386
  "implicit_rest_node" => Self::ImplicitRestNode,
385
387
  _ => Self::Unknown(s.to_string()),
386
- }
388
+ })
389
+ }
390
+ }
391
+
392
+ impl NodeType {
393
+ /// Parse a node type from a string (convenience wrapper for `FromStr`)
394
+ pub fn from_str(s: &str) -> Self {
395
+ s.parse().unwrap()
387
396
  }
388
397
 
389
398
  /// Check if this node type is a definition (class, module, or method)
@@ -79,12 +79,13 @@ impl Emitter {
79
79
  /// Find the last line of code in the AST (excluding comments)
80
80
  fn find_last_code_line(ast: &Node) -> usize {
81
81
  let mut max_line = ast.location.end_line;
82
- for child in &ast.children {
83
- let child_end = Self::find_last_code_line(child);
84
- if child_end > max_line {
85
- max_line = child_end;
86
- }
82
+ let mut stack = vec![ast];
83
+
84
+ while let Some(node) = stack.pop() {
85
+ max_line = max_line.max(node.location.end_line);
86
+ stack.extend(node.children.iter());
87
87
  }
88
+
88
89
  max_line
89
90
  }
90
91
 
@@ -426,15 +427,12 @@ impl Emitter {
426
427
  let next_start_line = next_child.location.start_line;
427
428
 
428
429
  // Find the first comment between current and next node (if any)
430
+ // Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
429
431
  let first_comment_line = self
430
- .all_comments
431
- .iter()
432
- .filter(|c| {
433
- c.location.start_line > current_end_line
434
- && c.location.end_line < next_start_line
435
- })
436
- .map(|c| c.location.start_line)
437
- .min();
432
+ .comments_by_line
433
+ .range((current_end_line + 1)..next_start_line)
434
+ .next()
435
+ .map(|(line, _)| *line);
438
436
 
439
437
  // Calculate line diff based on whether there's a comment
440
438
  let effective_next_line = first_comment_line.unwrap_or(next_start_line);
@@ -54,7 +54,12 @@ impl Log for RfmtLogger {
54
54
  return;
55
55
  }
56
56
 
57
- let mut output = self.output.lock().unwrap();
57
+ // Use unwrap_or_else to recover from poisoned mutex.
58
+ // Logging should never cause a panic, even if another thread panicked while holding the lock.
59
+ let mut output = self
60
+ .output
61
+ .lock()
62
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
58
63
 
59
64
  writeln!(
60
65
  output,
@@ -67,7 +72,11 @@ impl Log for RfmtLogger {
67
72
  }
68
73
 
69
74
  fn flush(&self) {
70
- let mut output = self.output.lock().unwrap();
75
+ // Recover from poisoned mutex - flushing should not panic
76
+ let mut output = self
77
+ .output
78
+ .lock()
79
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
71
80
  output.flush().ok();
72
81
  }
73
82
  }
@@ -13,8 +13,8 @@ impl PrismAdapter {
13
13
  Self
14
14
  }
15
15
 
16
- /// Parse JSON from Ruby's PrismBridge
17
- fn parse_json(&self, json: &str) -> Result<(PrismNode, Vec<PrismComment>)> {
16
+ /// Parse JSON from Ruby's `PrismBridge`
17
+ fn parse_json(json: &str) -> Result<(PrismNode, Vec<PrismComment>)> {
18
18
  // Try to parse as new format with comments first
19
19
  if let Ok(wrapper) = serde_json::from_str::<PrismWrapper>(json) {
20
20
  return Ok((wrapper.ast, wrapper.comments));
@@ -26,8 +26,8 @@ impl PrismAdapter {
26
26
  Ok((node, Vec::new()))
27
27
  }
28
28
 
29
- /// Convert PrismNode to internal Node representation
30
- fn convert_node(&self, prism_node: &PrismNode) -> Result<Node> {
29
+ /// Convert `PrismNode` to internal `Node` representation
30
+ fn convert_node(prism_node: &PrismNode) -> Result<Node> {
31
31
  // Convert node type (always succeeds, returns Unknown for unsupported types)
32
32
  let node_type = NodeType::from_str(&prism_node.node_type);
33
33
 
@@ -42,18 +42,15 @@ impl PrismAdapter {
42
42
  );
43
43
 
44
44
  // Convert children recursively
45
- let children: Result<Vec<Node>> = prism_node
46
- .children
47
- .iter()
48
- .map(|child| self.convert_node(child))
49
- .collect();
45
+ let children: Result<Vec<Node>> =
46
+ prism_node.children.iter().map(Self::convert_node).collect();
50
47
  let children = children?;
51
48
 
52
49
  // Convert comments
53
50
  let comments: Vec<Comment> = prism_node
54
51
  .comments
55
52
  .iter()
56
- .map(|c| self.convert_comment(c))
53
+ .map(Self::convert_comment)
57
54
  .collect();
58
55
 
59
56
  // Convert formatting info
@@ -76,21 +73,8 @@ impl PrismAdapter {
76
73
  })
77
74
  }
78
75
 
79
- /// Convert PrismComment to internal Comment
80
- fn convert_comment(&self, comment: &PrismComment) -> Comment {
81
- let comment_type = match comment.comment_type.as_str() {
82
- "line" => CommentType::Line,
83
- "block" => CommentType::Block,
84
- _ => CommentType::Line, // default to line comment
85
- };
86
-
87
- let position = match comment.position.as_str() {
88
- "leading" => CommentPosition::Leading,
89
- "trailing" => CommentPosition::Trailing,
90
- "inner" => CommentPosition::Inner,
91
- _ => CommentPosition::Leading, // default to leading
92
- };
93
-
76
+ /// Convert `PrismComment` to internal `Comment`
77
+ fn convert_comment(comment: &PrismComment) -> Comment {
94
78
  Comment {
95
79
  text: comment.text.clone(),
96
80
  location: Location::new(
@@ -101,21 +85,21 @@ impl PrismAdapter {
101
85
  comment.location.start_offset,
102
86
  comment.location.end_offset,
103
87
  ),
104
- comment_type,
105
- position,
88
+ comment_type: comment.comment_type.into(),
89
+ position: comment.position.into(),
106
90
  }
107
91
  }
108
92
  }
109
93
 
110
94
  impl RubyParser for PrismAdapter {
111
95
  fn parse(&self, json: &str) -> Result<Node> {
112
- let (prism_ast, top_level_comments) = self.parse_json(json)?;
113
- let mut node = self.convert_node(&prism_ast)?;
96
+ let (prism_ast, top_level_comments) = Self::parse_json(json)?;
97
+ let mut node = Self::convert_node(&prism_ast)?;
114
98
 
115
99
  // Attach top-level comments to the root node
116
100
  if !top_level_comments.is_empty() {
117
101
  node.comments
118
- .extend(top_level_comments.iter().map(|c| self.convert_comment(c)));
102
+ .extend(top_level_comments.iter().map(Self::convert_comment));
119
103
  }
120
104
 
121
105
  Ok(node)
@@ -160,8 +144,46 @@ pub struct PrismLocation {
160
144
  pub struct PrismComment {
161
145
  pub text: String,
162
146
  pub location: PrismLocation,
163
- pub comment_type: String,
164
- pub position: String,
147
+ #[serde(rename = "type", default)]
148
+ pub comment_type: PrismCommentType,
149
+ #[serde(default)]
150
+ pub position: PrismCommentPosition,
151
+ }
152
+
153
+ #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
154
+ #[serde(rename_all = "lowercase")]
155
+ pub enum PrismCommentType {
156
+ #[default]
157
+ Line,
158
+ Block,
159
+ }
160
+
161
+ impl From<PrismCommentType> for CommentType {
162
+ fn from(t: PrismCommentType) -> Self {
163
+ match t {
164
+ PrismCommentType::Line => CommentType::Line,
165
+ PrismCommentType::Block => CommentType::Block,
166
+ }
167
+ }
168
+ }
169
+
170
+ #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
171
+ #[serde(rename_all = "lowercase")]
172
+ pub enum PrismCommentPosition {
173
+ #[default]
174
+ Leading,
175
+ Trailing,
176
+ Inner,
177
+ }
178
+
179
+ impl From<PrismCommentPosition> for CommentPosition {
180
+ fn from(p: PrismCommentPosition) -> Self {
181
+ match p {
182
+ PrismCommentPosition::Leading => CommentPosition::Leading,
183
+ PrismCommentPosition::Trailing => CommentPosition::Trailing,
184
+ PrismCommentPosition::Inner => CommentPosition::Inner,
185
+ }
186
+ }
165
187
  }
166
188
 
167
189
  #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -16,3 +16,38 @@ pub fn validate_source_size(source: &str, max_size: u64) -> Result<()> {
16
16
 
17
17
  Ok(())
18
18
  }
19
+
20
+ #[cfg(test)]
21
+ mod tests {
22
+ use super::*;
23
+
24
+ #[test]
25
+ fn test_validate_source_size_ok() {
26
+ assert!(validate_source_size("small", 1000).is_ok());
27
+ }
28
+
29
+ #[test]
30
+ fn test_validate_source_size_at_limit() {
31
+ let source = "a".repeat(1000);
32
+ assert!(validate_source_size(&source, 1000).is_ok());
33
+ }
34
+
35
+ #[test]
36
+ fn test_validate_source_size_exceeds_limit() {
37
+ let source = "a".repeat(1001);
38
+ assert!(validate_source_size(&source, 1000).is_err());
39
+ }
40
+
41
+ #[test]
42
+ fn test_validate_source_size_empty() {
43
+ assert!(validate_source_size("", 1000).is_ok());
44
+ }
45
+
46
+ #[test]
47
+ fn test_validate_source_size_unicode() {
48
+ // "日本語" = 9 bytes in UTF-8
49
+ let source = "日本語";
50
+ assert!(validate_source_size(source, 9).is_ok());
51
+ assert!(validate_source_size(source, 8).is_err());
52
+ }
53
+ }
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.1'
4
+ VERSION = '1.3.2'
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.1
4
+ version: 1.3.2
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-07 00:00:00.000000000 Z
11
+ date: 2026-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys