rfmt 1.3.1 → 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 +4 -4
- data/CHANGELOG.md +26 -0
- data/Cargo.lock +1 -1
- data/ext/rfmt/Cargo.toml +1 -1
- data/ext/rfmt/src/ast/mod.rs +13 -4
- data/ext/rfmt/src/emitter/mod.rs +48 -73
- data/ext/rfmt/src/lib.rs +0 -11
- data/ext/rfmt/src/logging/logger.rs +11 -2
- data/ext/rfmt/src/parser/prism_adapter.rs +54 -32
- data/ext/rfmt/src/policy/validation.rs +35 -0
- data/lib/rfmt/native_extension_loader.rb +138 -0
- data/lib/rfmt/version.rb +1 -1
- data/lib/rfmt.rb +4 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d2092d9d53fa47024fcb8a13fb635a6da7eba5d4c51d3a8df854d551380037b
|
|
4
|
+
data.tar.gz: 59bb09f50cb7ee5a033328300a581e973879aaad56067d267b041c2070cac8bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7043e22d625b0b6a689f01950a6ed0fae7ab3fa1cfa2dcd51af544d72aadf8b0c3aa65a3465c9f9baabf2f1a000c37bddb2f2f57e2defd11344f6194ad3b68b
|
|
7
|
+
data.tar.gz: 407b70b3614da3ed19c74bd43d9491ee9bff1ce6cb1ef7f1e1d2d0b71068afb937bd5409668ae0c7454a6f2d12cb4764fa6df0273bef149980be30e0f9f1d7b6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
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
|
+
|
|
15
|
+
## [1.3.2] - 2026-01-09
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Implement `std::str::FromStr` trait for `NodeType`
|
|
19
|
+
- Unit tests for validation module
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Use serde enum for comment type deserialization (type-safe JSON parsing)
|
|
23
|
+
- Convert recursive `find_last_code_line` to iterative approach (prevent stack overflow)
|
|
24
|
+
- Use BTreeMap index for comment lookup in `emit_statements` (O(n) → O(log n))
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Remove panic-prone `unwrap()` on Mutex lock in logger (prevent Ruby VM crash)
|
|
28
|
+
|
|
3
29
|
## [1.3.1] - 2026-01-08
|
|
4
30
|
|
|
5
31
|
### Added
|
data/Cargo.lock
CHANGED
data/ext/rfmt/Cargo.toml
CHANGED
data/ext/rfmt/src/ast/mod.rs
CHANGED
|
@@ -235,9 +235,11 @@ pub enum NodeType {
|
|
|
235
235
|
Unknown(String),
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
impl NodeType {
|
|
239
|
-
|
|
240
|
-
|
|
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)
|
data/ext/rfmt/src/emitter/mod.rs
CHANGED
|
@@ -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
|
}
|
|
@@ -79,12 +75,13 @@ impl Emitter {
|
|
|
79
75
|
/// Find the last line of code in the AST (excluding comments)
|
|
80
76
|
fn find_last_code_line(ast: &Node) -> usize {
|
|
81
77
|
let mut max_line = ast.location.end_line;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
let mut stack = vec![ast];
|
|
79
|
+
|
|
80
|
+
while let Some(node) = stack.pop() {
|
|
81
|
+
max_line = max_line.max(node.location.end_line);
|
|
82
|
+
stack.extend(node.children.iter());
|
|
87
83
|
}
|
|
84
|
+
|
|
88
85
|
max_line
|
|
89
86
|
}
|
|
90
87
|
|
|
@@ -185,35 +182,26 @@ impl Emitter {
|
|
|
185
182
|
/// Emit comments that appear before a given line
|
|
186
183
|
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
187
184
|
fn emit_comments_before(&mut self, line: usize, indent_level: usize) -> Result<()> {
|
|
188
|
-
|
|
185
|
+
self.ensure_indent_cache(indent_level);
|
|
189
186
|
|
|
190
|
-
// Use indexed lookup instead of iterating all comments
|
|
191
187
|
let indices = self.get_comment_indices_before(line);
|
|
192
188
|
|
|
193
|
-
// Build list of comments to emit with their data
|
|
194
189
|
let mut comments_to_emit: Vec<_> = indices
|
|
195
190
|
.into_iter()
|
|
196
191
|
.map(|idx| {
|
|
197
192
|
let comment = &self.all_comments[idx];
|
|
198
|
-
(
|
|
199
|
-
idx,
|
|
200
|
-
comment.text.clone(),
|
|
201
|
-
comment.location.start_line,
|
|
202
|
-
comment.location.end_line,
|
|
203
|
-
)
|
|
193
|
+
(idx, comment.location.start_line, comment.location.end_line)
|
|
204
194
|
})
|
|
205
195
|
.collect();
|
|
206
196
|
|
|
207
|
-
|
|
208
|
-
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
197
|
+
comments_to_emit.sort_by_key(|(_, start, _)| *start);
|
|
209
198
|
|
|
210
199
|
let comments_count = comments_to_emit.len();
|
|
211
200
|
let mut last_comment_end_line: Option<usize> = None;
|
|
212
201
|
|
|
213
|
-
for (i, (idx,
|
|
202
|
+
for (i, (idx, comment_start_line, comment_end_line)) in
|
|
214
203
|
comments_to_emit.into_iter().enumerate()
|
|
215
204
|
{
|
|
216
|
-
// Preserve blank lines between comments
|
|
217
205
|
if let Some(prev_end) = last_comment_end_line {
|
|
218
206
|
let gap = comment_start_line.saturating_sub(prev_end);
|
|
219
207
|
for _ in 1..gap {
|
|
@@ -221,11 +209,14 @@ impl Emitter {
|
|
|
221
209
|
}
|
|
222
210
|
}
|
|
223
211
|
|
|
224
|
-
writeln!(
|
|
212
|
+
writeln!(
|
|
213
|
+
self.buffer,
|
|
214
|
+
"{}{}",
|
|
215
|
+
&self.indent_cache[indent_level], &self.all_comments[idx].text
|
|
216
|
+
)?;
|
|
225
217
|
self.emitted_comment_indices.insert(idx);
|
|
226
218
|
last_comment_end_line = Some(comment_end_line);
|
|
227
219
|
|
|
228
|
-
// Add blank line after the LAST comment if there was a gap to the code
|
|
229
220
|
if i == comments_count - 1 && line > comment_end_line + 1 {
|
|
230
221
|
self.buffer.push('\n');
|
|
231
222
|
}
|
|
@@ -254,33 +245,24 @@ impl Emitter {
|
|
|
254
245
|
end_line: usize,
|
|
255
246
|
indent_level: usize,
|
|
256
247
|
) -> Result<()> {
|
|
257
|
-
|
|
248
|
+
self.ensure_indent_cache(indent_level);
|
|
258
249
|
|
|
259
|
-
// Use indexed lookup instead of iterating all comments
|
|
260
250
|
let indices = self.get_comment_indices_in_range(start_line, end_line);
|
|
261
251
|
|
|
262
|
-
// Build list of comments to emit, filtering by end_line
|
|
263
252
|
let mut comments_to_emit: Vec<_> = indices
|
|
264
253
|
.into_iter()
|
|
265
254
|
.filter(|&idx| self.all_comments[idx].location.end_line < end_line)
|
|
266
255
|
.map(|idx| {
|
|
267
256
|
let comment = &self.all_comments[idx];
|
|
268
|
-
(
|
|
269
|
-
idx,
|
|
270
|
-
comment.text.clone(),
|
|
271
|
-
comment.location.start_line,
|
|
272
|
-
comment.location.end_line,
|
|
273
|
-
)
|
|
257
|
+
(idx, comment.location.start_line, comment.location.end_line)
|
|
274
258
|
})
|
|
275
259
|
.collect();
|
|
276
260
|
|
|
277
|
-
|
|
278
|
-
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
261
|
+
comments_to_emit.sort_by_key(|(_, start, _)| *start);
|
|
279
262
|
|
|
280
263
|
let mut last_comment_end_line: Option<usize> = None;
|
|
281
264
|
|
|
282
|
-
for (idx,
|
|
283
|
-
// Preserve blank lines between comments
|
|
265
|
+
for (idx, comment_start_line, comment_end_line) in comments_to_emit {
|
|
284
266
|
if let Some(prev_end) = last_comment_end_line {
|
|
285
267
|
let gap = comment_start_line.saturating_sub(prev_end);
|
|
286
268
|
for _ in 1..gap {
|
|
@@ -288,7 +270,11 @@ impl Emitter {
|
|
|
288
270
|
}
|
|
289
271
|
}
|
|
290
272
|
|
|
291
|
-
writeln!(
|
|
273
|
+
writeln!(
|
|
274
|
+
self.buffer,
|
|
275
|
+
"{}{}",
|
|
276
|
+
&self.indent_cache[indent_level], &self.all_comments[idx].text
|
|
277
|
+
)?;
|
|
292
278
|
self.emitted_comment_indices.insert(idx);
|
|
293
279
|
last_comment_end_line = Some(comment_end_line);
|
|
294
280
|
}
|
|
@@ -305,39 +291,35 @@ impl Emitter {
|
|
|
305
291
|
indent_level: usize,
|
|
306
292
|
prev_line: usize,
|
|
307
293
|
) -> Result<()> {
|
|
308
|
-
|
|
294
|
+
self.ensure_indent_cache(indent_level);
|
|
309
295
|
|
|
310
|
-
// Use indexed lookup instead of iterating all comments
|
|
311
296
|
let indices = self.get_comment_indices_in_range(start_line, end_line);
|
|
312
297
|
|
|
313
|
-
// Build list of comments to emit, filtering by end_line
|
|
314
298
|
let mut comments_to_emit: Vec<_> = indices
|
|
315
299
|
.into_iter()
|
|
316
300
|
.filter(|&idx| self.all_comments[idx].location.end_line < end_line)
|
|
317
301
|
.map(|idx| {
|
|
318
302
|
let comment = &self.all_comments[idx];
|
|
319
|
-
(
|
|
320
|
-
idx,
|
|
321
|
-
comment.text.clone(),
|
|
322
|
-
comment.location.start_line,
|
|
323
|
-
comment.location.end_line,
|
|
324
|
-
)
|
|
303
|
+
(idx, comment.location.start_line, comment.location.end_line)
|
|
325
304
|
})
|
|
326
305
|
.collect();
|
|
327
306
|
|
|
328
|
-
|
|
329
|
-
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
307
|
+
comments_to_emit.sort_by_key(|(_, start, _)| *start);
|
|
330
308
|
|
|
331
309
|
let mut last_end_line: usize = prev_line;
|
|
332
310
|
|
|
333
|
-
for (idx,
|
|
311
|
+
for (idx, comment_start_line, comment_end_line) in comments_to_emit {
|
|
334
312
|
// Preserve blank lines between previous content and this comment
|
|
335
313
|
let gap = comment_start_line.saturating_sub(last_end_line);
|
|
336
314
|
for _ in 1..gap {
|
|
337
315
|
self.buffer.push('\n');
|
|
338
316
|
}
|
|
339
317
|
|
|
340
|
-
writeln!(
|
|
318
|
+
writeln!(
|
|
319
|
+
self.buffer,
|
|
320
|
+
"{}{}",
|
|
321
|
+
&self.indent_cache[indent_level], &self.all_comments[idx].text
|
|
322
|
+
)?;
|
|
341
323
|
self.emitted_comment_indices.insert(idx);
|
|
342
324
|
last_end_line = comment_end_line;
|
|
343
325
|
}
|
|
@@ -351,15 +333,12 @@ impl Emitter {
|
|
|
351
333
|
// Use indexed lookup for O(log n) access
|
|
352
334
|
let indices = self.get_comment_indices_on_line(line);
|
|
353
335
|
|
|
354
|
-
//
|
|
355
|
-
let indices_to_emit: Vec<
|
|
356
|
-
.into_iter()
|
|
357
|
-
.map(|idx| (idx, self.all_comments[idx].text.clone()))
|
|
358
|
-
.collect();
|
|
336
|
+
// Collect indices only (no text clone needed)
|
|
337
|
+
let indices_to_emit: Vec<usize> = indices;
|
|
359
338
|
|
|
360
|
-
// Now emit the collected comments
|
|
361
|
-
for
|
|
362
|
-
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)?;
|
|
363
342
|
self.emitted_comment_indices.insert(idx);
|
|
364
343
|
}
|
|
365
344
|
|
|
@@ -426,15 +405,12 @@ impl Emitter {
|
|
|
426
405
|
let next_start_line = next_child.location.start_line;
|
|
427
406
|
|
|
428
407
|
// Find the first comment between current and next node (if any)
|
|
408
|
+
// Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
|
|
429
409
|
let first_comment_line = self
|
|
430
|
-
.
|
|
431
|
-
.
|
|
432
|
-
.
|
|
433
|
-
|
|
434
|
-
&& c.location.end_line < next_start_line
|
|
435
|
-
})
|
|
436
|
-
.map(|c| c.location.start_line)
|
|
437
|
-
.min();
|
|
410
|
+
.comments_by_line
|
|
411
|
+
.range((current_end_line + 1)..next_start_line)
|
|
412
|
+
.next()
|
|
413
|
+
.map(|(line, _)| *line);
|
|
438
414
|
|
|
439
415
|
// Calculate line diff based on whether there's a comment
|
|
440
416
|
let effective_next_line = first_comment_line.unwrap_or(next_start_line);
|
|
@@ -1313,9 +1289,9 @@ impl Emitter {
|
|
|
1313
1289
|
Ok(())
|
|
1314
1290
|
}
|
|
1315
1291
|
|
|
1316
|
-
///
|
|
1317
|
-
|
|
1318
|
-
|
|
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) {
|
|
1319
1295
|
while self.indent_cache.len() <= level {
|
|
1320
1296
|
let len = self.indent_cache.len();
|
|
1321
1297
|
let indent = match self.config.formatting.indent_style {
|
|
@@ -1324,13 +1300,12 @@ impl Emitter {
|
|
|
1324
1300
|
};
|
|
1325
1301
|
self.indent_cache.push(indent);
|
|
1326
1302
|
}
|
|
1327
|
-
&self.indent_cache[level]
|
|
1328
1303
|
}
|
|
1329
1304
|
|
|
1330
1305
|
/// Emit indentation
|
|
1331
1306
|
fn emit_indent(&mut self, level: usize) -> Result<()> {
|
|
1332
|
-
|
|
1333
|
-
write!(self.buffer, "{}",
|
|
1307
|
+
self.ensure_indent_cache(level);
|
|
1308
|
+
write!(self.buffer, "{}", &self.indent_cache[level])?;
|
|
1334
1309
|
Ok(())
|
|
1335
1310
|
}
|
|
1336
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
|
}
|
|
@@ -54,7 +54,12 @@ impl Log for RfmtLogger {
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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>> =
|
|
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(
|
|
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(
|
|
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) =
|
|
113
|
-
let mut node =
|
|
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(
|
|
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
|
-
|
|
164
|
-
pub
|
|
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
|
+
}
|
|
@@ -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
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/
|
|
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.
|
|
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-
|
|
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
|