rfmt 1.3.2 → 1.3.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: 79f5e6c24f82f6552874aefda2a6b97d8f4612419810d76ebd9381e862edded4
4
- data.tar.gz: f193dab59e9a02f62d3e599c8f0349ebbf6e7fda070ba55990e9a8d399990f72
3
+ metadata.gz: 7d2092d9d53fa47024fcb8a13fb635a6da7eba5d4c51d3a8df854d551380037b
4
+ data.tar.gz: 59bb09f50cb7ee5a033328300a581e973879aaad56067d267b041c2070cac8bf
5
5
  SHA512:
6
- metadata.gz: 6ebfd2c04793445912e1edc28db74e5cfa218fcd3fc28f6e3b42a88b1794fe0bfc6643890fa34477291c7ad8624696f3bb7207436ecfeb6f756439be5d9065ae
7
- data.tar.gz: e209cf0a8671786af5e1560979f70f5fe8bb4e2de3912613a59e6d6269effe54f123407ca8989b50578f330d16e8a0d2fa7a1b47c8c07d5731e893664892fe1a
6
+ metadata.gz: f7043e22d625b0b6a689f01950a6ed0fae7ab3fa1cfa2dcd51af544d72aadf8b0c3aa65a3465c9f9baabf2f1a000c37bddb2f2f57e2defd11344f6194ad3b68b
7
+ data.tar.gz: 407b70b3614da3ed19c74bd43d9491ee9bff1ce6cb1ef7f1e1d2d0b71068afb937bd5409668ae0c7454a6f2d12cb4764fa6df0273bef149980be30e0f9f1d7b6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.3] - 2026-01-17
4
+
5
+ ### Fixed
6
+ - Add native extension loader for Ruby 3.3+ compatibility (#65)
7
+ - Resolves LoadError on Ruby 3.3+ arm64-darwin systems
8
+ - Implements dynamic path resolution for version-specific directories
9
+
10
+ ### Changed
11
+ - Remove unnecessary String clones in comment emission (performance optimization)
12
+ - Remove debug logs and obvious comments from codebase
13
+ - Update .gitignore with development artifacts
14
+
3
15
  ## [1.3.2] - 2026-01-09
4
16
 
5
17
  ### Added
data/Cargo.lock CHANGED
@@ -1214,7 +1214,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
1214
1214
 
1215
1215
  [[package]]
1216
1216
  name = "rfmt"
1217
- version = "1.3.2"
1217
+ version = "1.3.3"
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.2"
3
+ version = "1.3.3"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -62,13 +62,9 @@ impl Emitter {
62
62
 
63
63
  self.emit_node(ast, 0)?;
64
64
 
65
- // Find the last emitted code line for proper blank line handling
66
65
  let last_code_line = Self::find_last_code_line(ast);
67
-
68
- // Emit any remaining comments that weren't emitted
69
66
  self.emit_remaining_comments(last_code_line)?;
70
67
 
71
- // Ensure file ends with a newline
72
68
  if !self.buffer.ends_with('\n') {
73
69
  self.buffer.push('\n');
74
70
  }
@@ -186,35 +182,26 @@ impl Emitter {
186
182
  /// Emit comments that appear before a given line
187
183
  /// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
188
184
  fn emit_comments_before(&mut self, line: usize, indent_level: usize) -> Result<()> {
189
- let indent_str = self.get_indent(indent_level).to_string();
185
+ self.ensure_indent_cache(indent_level);
190
186
 
191
- // Use indexed lookup instead of iterating all comments
192
187
  let indices = self.get_comment_indices_before(line);
193
188
 
194
- // Build list of comments to emit with their data
195
189
  let mut comments_to_emit: Vec<_> = indices
196
190
  .into_iter()
197
191
  .map(|idx| {
198
192
  let comment = &self.all_comments[idx];
199
- (
200
- idx,
201
- comment.text.clone(),
202
- comment.location.start_line,
203
- comment.location.end_line,
204
- )
193
+ (idx, comment.location.start_line, comment.location.end_line)
205
194
  })
206
195
  .collect();
207
196
 
208
- // Sort by start_line to emit in order
209
- comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
197
+ comments_to_emit.sort_by_key(|(_, start, _)| *start);
210
198
 
211
199
  let comments_count = comments_to_emit.len();
212
200
  let mut last_comment_end_line: Option<usize> = None;
213
201
 
214
- for (i, (idx, text, comment_start_line, comment_end_line)) in
202
+ for (i, (idx, comment_start_line, comment_end_line)) in
215
203
  comments_to_emit.into_iter().enumerate()
216
204
  {
217
- // Preserve blank lines between comments
218
205
  if let Some(prev_end) = last_comment_end_line {
219
206
  let gap = comment_start_line.saturating_sub(prev_end);
220
207
  for _ in 1..gap {
@@ -222,11 +209,14 @@ impl Emitter {
222
209
  }
223
210
  }
224
211
 
225
- writeln!(self.buffer, "{}{}", indent_str, text)?;
212
+ writeln!(
213
+ self.buffer,
214
+ "{}{}",
215
+ &self.indent_cache[indent_level], &self.all_comments[idx].text
216
+ )?;
226
217
  self.emitted_comment_indices.insert(idx);
227
218
  last_comment_end_line = Some(comment_end_line);
228
219
 
229
- // Add blank line after the LAST comment if there was a gap to the code
230
220
  if i == comments_count - 1 && line > comment_end_line + 1 {
231
221
  self.buffer.push('\n');
232
222
  }
@@ -255,33 +245,24 @@ impl Emitter {
255
245
  end_line: usize,
256
246
  indent_level: usize,
257
247
  ) -> Result<()> {
258
- let indent_str = self.get_indent(indent_level).to_string();
248
+ self.ensure_indent_cache(indent_level);
259
249
 
260
- // Use indexed lookup instead of iterating all comments
261
250
  let indices = self.get_comment_indices_in_range(start_line, end_line);
262
251
 
263
- // Build list of comments to emit, filtering by end_line
264
252
  let mut comments_to_emit: Vec<_> = indices
265
253
  .into_iter()
266
254
  .filter(|&idx| self.all_comments[idx].location.end_line < end_line)
267
255
  .map(|idx| {
268
256
  let comment = &self.all_comments[idx];
269
- (
270
- idx,
271
- comment.text.clone(),
272
- comment.location.start_line,
273
- comment.location.end_line,
274
- )
257
+ (idx, comment.location.start_line, comment.location.end_line)
275
258
  })
276
259
  .collect();
277
260
 
278
- // Sort by start_line to emit in order
279
- comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
261
+ comments_to_emit.sort_by_key(|(_, start, _)| *start);
280
262
 
281
263
  let mut last_comment_end_line: Option<usize> = None;
282
264
 
283
- for (idx, text, comment_start_line, comment_end_line) in comments_to_emit {
284
- // Preserve blank lines between comments
265
+ for (idx, comment_start_line, comment_end_line) in comments_to_emit {
285
266
  if let Some(prev_end) = last_comment_end_line {
286
267
  let gap = comment_start_line.saturating_sub(prev_end);
287
268
  for _ in 1..gap {
@@ -289,7 +270,11 @@ impl Emitter {
289
270
  }
290
271
  }
291
272
 
292
- writeln!(self.buffer, "{}{}", indent_str, text)?;
273
+ writeln!(
274
+ self.buffer,
275
+ "{}{}",
276
+ &self.indent_cache[indent_level], &self.all_comments[idx].text
277
+ )?;
293
278
  self.emitted_comment_indices.insert(idx);
294
279
  last_comment_end_line = Some(comment_end_line);
295
280
  }
@@ -306,39 +291,35 @@ impl Emitter {
306
291
  indent_level: usize,
307
292
  prev_line: usize,
308
293
  ) -> Result<()> {
309
- let indent_str = self.get_indent(indent_level).to_string();
294
+ self.ensure_indent_cache(indent_level);
310
295
 
311
- // Use indexed lookup instead of iterating all comments
312
296
  let indices = self.get_comment_indices_in_range(start_line, end_line);
313
297
 
314
- // Build list of comments to emit, filtering by end_line
315
298
  let mut comments_to_emit: Vec<_> = indices
316
299
  .into_iter()
317
300
  .filter(|&idx| self.all_comments[idx].location.end_line < end_line)
318
301
  .map(|idx| {
319
302
  let comment = &self.all_comments[idx];
320
- (
321
- idx,
322
- comment.text.clone(),
323
- comment.location.start_line,
324
- comment.location.end_line,
325
- )
303
+ (idx, comment.location.start_line, comment.location.end_line)
326
304
  })
327
305
  .collect();
328
306
 
329
- // Sort by start_line to emit in order
330
- comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
307
+ comments_to_emit.sort_by_key(|(_, start, _)| *start);
331
308
 
332
309
  let mut last_end_line: usize = prev_line;
333
310
 
334
- for (idx, text, comment_start_line, comment_end_line) in comments_to_emit {
311
+ for (idx, comment_start_line, comment_end_line) in comments_to_emit {
335
312
  // Preserve blank lines between previous content and this comment
336
313
  let gap = comment_start_line.saturating_sub(last_end_line);
337
314
  for _ in 1..gap {
338
315
  self.buffer.push('\n');
339
316
  }
340
317
 
341
- writeln!(self.buffer, "{}{}", indent_str, text)?;
318
+ writeln!(
319
+ self.buffer,
320
+ "{}{}",
321
+ &self.indent_cache[indent_level], &self.all_comments[idx].text
322
+ )?;
342
323
  self.emitted_comment_indices.insert(idx);
343
324
  last_end_line = comment_end_line;
344
325
  }
@@ -352,15 +333,12 @@ impl Emitter {
352
333
  // Use indexed lookup for O(log n) access
353
334
  let indices = self.get_comment_indices_on_line(line);
354
335
 
355
- // Build list of comments to emit
356
- let indices_to_emit: Vec<_> = indices
357
- .into_iter()
358
- .map(|idx| (idx, self.all_comments[idx].text.clone()))
359
- .collect();
336
+ // Collect indices only (no text clone needed)
337
+ let indices_to_emit: Vec<usize> = indices;
360
338
 
361
- // Now emit the collected comments
362
- for (idx, text) in indices_to_emit {
363
- write!(self.buffer, " {}", text)?;
339
+ // Now emit the collected comments by accessing text at write time
340
+ for idx in indices_to_emit {
341
+ write!(self.buffer, " {}", &self.all_comments[idx].text)?;
364
342
  self.emitted_comment_indices.insert(idx);
365
343
  }
366
344
 
@@ -1311,9 +1289,9 @@ impl Emitter {
1311
1289
  Ok(())
1312
1290
  }
1313
1291
 
1314
- /// Get cached indent string for a given level
1315
- fn get_indent(&mut self, level: usize) -> &str {
1316
- // Extend cache if needed
1292
+ /// Ensure indent cache has entries up to and including the given level
1293
+ /// This allows pre-building the cache before borrowing self.indent_cache
1294
+ fn ensure_indent_cache(&mut self, level: usize) {
1317
1295
  while self.indent_cache.len() <= level {
1318
1296
  let len = self.indent_cache.len();
1319
1297
  let indent = match self.config.formatting.indent_style {
@@ -1322,13 +1300,12 @@ impl Emitter {
1322
1300
  };
1323
1301
  self.indent_cache.push(indent);
1324
1302
  }
1325
- &self.indent_cache[level]
1326
1303
  }
1327
1304
 
1328
1305
  /// Emit indentation
1329
1306
  fn emit_indent(&mut self, level: usize) -> Result<()> {
1330
- let indent_str = self.get_indent(level).to_string();
1331
- write!(self.buffer, "{}", indent_str)?;
1307
+ self.ensure_indent_cache(level);
1308
+ write!(self.buffer, "{}", &self.indent_cache[level])?;
1332
1309
  Ok(())
1333
1310
  }
1334
1311
 
data/ext/rfmt/src/lib.rs CHANGED
@@ -14,25 +14,16 @@ use magnus::{define_module, 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> {
17
- log::info!("format_ruby_code called");
18
17
  let policy = SecurityPolicy::default();
19
18
 
20
19
  policy
21
20
  .validate_source_size(&source)
22
21
  .map_err(|e| e.to_magnus_error(ruby))?;
23
22
 
24
- log::debug!("Source code validated, size: {} bytes", source.len());
25
-
26
23
  let parser = PrismAdapter::new();
27
24
  let ast = parser.parse(&json).map_err(|e| e.to_magnus_error(ruby))?;
28
25
 
29
- // Load configuration from file or use defaults
30
- log::info!("Attempting to discover config file...");
31
26
  let config = Config::discover().map_err(|e| e.to_magnus_error(ruby))?;
32
- log::info!(
33
- "Config loaded successfully, line_length: {}",
34
- config.formatting.line_length
35
- );
36
27
  let mut emitter = Emitter::with_source(config, source);
37
28
 
38
29
  let formatted = emitter.emit(&ast).map_err(|e| e.to_magnus_error(ruby))?;
@@ -56,7 +47,6 @@ fn rust_version() -> String {
56
47
  #[magnus::init]
57
48
  fn init(_ruby: &Ruby) -> Result<(), Error> {
58
49
  logging::RfmtLogger::init();
59
- log::info!("Initializing rfmt Rust extension");
60
50
 
61
51
  let module = define_module("Rfmt")?;
62
52
 
@@ -64,6 +54,5 @@ fn init(_ruby: &Ruby) -> Result<(), Error> {
64
54
  module.define_singleton_method("parse_to_json", function!(parse_to_json, 1))?;
65
55
  module.define_singleton_method("rust_version", function!(rust_version, 0))?;
66
56
 
67
- log::info!("rfmt Rust extension initialized successfully");
68
57
  Ok(())
69
58
  }
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rfmt
4
+ # Handles loading of native extension across different Ruby versions
5
+ # Ruby 3.3+ places native extensions in version-specific subdirectories
6
+ module NativeExtensionLoader
7
+ class << self
8
+ # Load the native extension, trying multiple possible paths
9
+ # @return [Boolean] true if successfully loaded
10
+ # @raise [LoadError] if the extension cannot be found
11
+ def load_extension
12
+ debug_log "Loading native extension for Ruby #{RUBY_VERSION}"
13
+
14
+ possible_paths = build_possible_paths
15
+ debug_log "Trying paths: #{possible_paths.inspect}"
16
+
17
+ load_from_paths(possible_paths) || raise(LoadError, build_error_message(possible_paths))
18
+ end
19
+
20
+ private
21
+
22
+ # Try loading from multiple paths
23
+ # @param paths [Array<String>] paths to try
24
+ # @return [Boolean, nil] true if loaded, nil otherwise
25
+ def load_from_paths(paths)
26
+ paths.each do |path|
27
+ if try_load_extension(path)
28
+ debug_log "Successfully loaded from: #{path}"
29
+ return true
30
+ end
31
+ end
32
+ nil
33
+ end
34
+
35
+ # Build list of possible paths for the native extension
36
+ # @return [Array<String>] paths to try, in order of preference
37
+ def build_possible_paths
38
+ paths = []
39
+
40
+ # Ruby 3.3+ style: version-specific subdirectory
41
+ paths << version_specific_path if ruby_version >= '3.3'
42
+
43
+ # Ruby 3.0-3.2 style: might use version directory
44
+ paths << version_specific_path if ruby_version >= '3.0' && ruby_version < '3.3'
45
+
46
+ # Legacy/fallback: direct placement
47
+ paths << File.join(__dir__, 'rfmt')
48
+
49
+ # Additional fallback: check for .bundle extension explicitly
50
+ paths << File.join(__dir__, 'rfmt.bundle')
51
+
52
+ paths.uniq
53
+ end
54
+
55
+ # Get version-specific path
56
+ # @return [String] path with version directory
57
+ def version_specific_path
58
+ File.join(__dir__, ruby_version_dir, 'rfmt')
59
+ end
60
+
61
+ # Try to load extension from a specific path
62
+ # @param path [String] path to try
63
+ # @return [Boolean] true if successful, false otherwise
64
+ def try_load_extension(path)
65
+ require path
66
+ true
67
+ rescue LoadError => e
68
+ debug_log "Failed to load from #{path}: #{e.message}"
69
+ false
70
+ end
71
+
72
+ # Get Ruby version for comparison
73
+ # @return [String] Ruby version string
74
+ def ruby_version
75
+ RUBY_VERSION
76
+ end
77
+
78
+ # Get Ruby version directory name (e.g., "3.3" for Ruby 3.3.0)
79
+ # @return [String] version directory name
80
+ def ruby_version_dir
81
+ RUBY_VERSION.split('.')[0..1].join('.')
82
+ end
83
+
84
+ # Build detailed error message when extension cannot be loaded
85
+ # @param tried_paths [Array<String>] paths that were tried
86
+ # @return [String] error message
87
+ def build_error_message(tried_paths)
88
+ [
89
+ error_header,
90
+ format_tried_paths(tried_paths),
91
+ error_explanation,
92
+ workaround_instructions
93
+ ].join("\n")
94
+ end
95
+
96
+ # Error message header
97
+ # @return [String] header text
98
+ def error_header
99
+ "Unable to load rfmt native extension for Ruby #{RUBY_VERSION}.\n"
100
+ end
101
+
102
+ # Format list of tried paths
103
+ # @param paths [Array<String>] paths that were tried
104
+ # @return [String] formatted path list
105
+ def format_tried_paths(paths)
106
+ "Tried the following paths:\n#{paths.map { |p| " - #{p}" }.join("\n")}\n"
107
+ end
108
+
109
+ # Error explanation text
110
+ # @return [String] explanation
111
+ def error_explanation
112
+ "This might be a packaging issue with the gem for your Ruby version.\n"
113
+ end
114
+
115
+ # Workaround instructions
116
+ # @return [String] instructions
117
+ def workaround_instructions
118
+ <<~MSG.chomp
119
+ Workaround:
120
+ 1. Check if rfmt.bundle exists in: #{__dir__}/
121
+ 2. If it's in a subdirectory, create a symlink:
122
+ cd #{__dir__}
123
+ ln -sf <subdirectory>/rfmt.bundle rfmt.bundle
124
+
125
+ Please report this issue at: https://github.com/fs0414/rfmt/issues
126
+ MSG
127
+ end
128
+
129
+ # Log debug information if RFMT_DEBUG is set
130
+ # @param message [String] message to log
131
+ def debug_log(message)
132
+ return unless ENV['RFMT_DEBUG']
133
+
134
+ warn "[RFMT::NativeExtensionLoader] #{message}"
135
+ end
136
+ end
137
+ end
138
+ end
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.2'
4
+ VERSION = '1.3.3'
5
5
  end
data/lib/rfmt.rb CHANGED
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'rfmt/version'
4
- require_relative 'rfmt/rfmt'
4
+ require_relative 'rfmt/native_extension_loader'
5
5
  require_relative 'rfmt/prism_bridge'
6
6
 
7
+ # Load native extension with version-aware loader
8
+ Rfmt::NativeExtensionLoader.load_extension
9
+
7
10
  module Rfmt
8
11
  class Error < StandardError; end
9
12
  # Errors from Rust side
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.2
4
+ version: 1.3.3
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-09 00:00:00.000000000 Z
11
+ date: 2026-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -56,6 +56,7 @@ files:
56
56
  - lib/rfmt/cache.rb
57
57
  - lib/rfmt/cli.rb
58
58
  - lib/rfmt/configuration.rb
59
+ - lib/rfmt/native_extension_loader.rb
59
60
  - lib/rfmt/prism_bridge.rb
60
61
  - lib/rfmt/prism_node_extractor.rb
61
62
  - lib/rfmt/version.rb