rfmt 1.3.0 → 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 +4 -4
- data/CHANGELOG.md +30 -0
- data/Cargo.lock +1 -1
- data/ext/rfmt/Cargo.toml +1 -1
- data/ext/rfmt/src/ast/mod.rs +105 -4
- data/ext/rfmt/src/emitter/mod.rs +316 -115
- 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/prism_bridge.rb +56 -4
- data/lib/rfmt/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79f5e6c24f82f6552874aefda2a6b97d8f4612419810d76ebd9381e862edded4
|
|
4
|
+
data.tar.gz: f193dab59e9a02f62d3e599c8f0349ebbf6e7fda070ba55990e9a8d399990f72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ebfd2c04793445912e1edc28db74e5cfa218fcd3fc28f6e3b42a88b1794fe0bfc6643890fa34477291c7ad8624696f3bb7207436ecfeb6f756439be5d9065ae
|
|
7
|
+
data.tar.gz: e209cf0a8671786af5e1560979f70f5fe8bb4e2de3912613a59e6d6269effe54f123407ca8989b50578f330d16e8a0d2fa7a1b47c8c07d5731e893664892fe1a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
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
|
+
|
|
17
|
+
## [1.3.1] - 2026-01-08
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Dedicated emitters for SingletonClassNode and pattern matching (CaseMatchNode, InNode)
|
|
21
|
+
- Literal and pattern match node support
|
|
22
|
+
- NoKeywordsParameterNode support
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Performance: Comment lookup optimized with BTreeMap index (O(n) → O(log n))
|
|
26
|
+
- Performance: HashSet for comment tracking (O(n) → O(1) contains check)
|
|
27
|
+
- Performance: Cached indent strings to avoid repeated allocations
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- DefNode parameter handling and missing node types
|
|
31
|
+
- Deep nest JSON parse error (max_nesting: false)
|
|
32
|
+
|
|
3
33
|
## [1.3.0] - 2026-01-07
|
|
4
34
|
|
|
5
35
|
### Added
|
data/Cargo.lock
CHANGED
data/ext/rfmt/Cargo.toml
CHANGED
data/ext/rfmt/src/ast/mod.rs
CHANGED
|
@@ -180,12 +180,66 @@ pub enum NodeType {
|
|
|
180
180
|
KeywordRestParameterNode,
|
|
181
181
|
BlockParameterNode,
|
|
182
182
|
|
|
183
|
+
// Source metadata nodes
|
|
184
|
+
SourceFileNode,
|
|
185
|
+
SourceLineNode,
|
|
186
|
+
SourceEncodingNode,
|
|
187
|
+
|
|
188
|
+
// Pre/Post execution
|
|
189
|
+
PreExecutionNode,
|
|
190
|
+
PostExecutionNode,
|
|
191
|
+
|
|
192
|
+
// Numeric literals
|
|
193
|
+
RationalNode,
|
|
194
|
+
ImaginaryNode,
|
|
195
|
+
|
|
196
|
+
// String interpolation
|
|
197
|
+
EmbeddedVariableNode,
|
|
198
|
+
|
|
199
|
+
// Pattern matching patterns
|
|
200
|
+
ArrayPatternNode,
|
|
201
|
+
HashPatternNode,
|
|
202
|
+
FindPatternNode,
|
|
203
|
+
CapturePatternNode,
|
|
204
|
+
AlternationPatternNode,
|
|
205
|
+
PinnedExpressionNode,
|
|
206
|
+
PinnedVariableNode,
|
|
207
|
+
|
|
208
|
+
// Forwarding
|
|
209
|
+
ForwardingArgumentsNode,
|
|
210
|
+
ForwardingParameterNode,
|
|
211
|
+
NoKeywordsParameterNode,
|
|
212
|
+
|
|
213
|
+
// References
|
|
214
|
+
BackReferenceReadNode,
|
|
215
|
+
NumberedReferenceReadNode,
|
|
216
|
+
|
|
217
|
+
// Call/Index compound assignment
|
|
218
|
+
CallAndWriteNode,
|
|
219
|
+
CallOrWriteNode,
|
|
220
|
+
CallOperatorWriteNode,
|
|
221
|
+
IndexAndWriteNode,
|
|
222
|
+
IndexOrWriteNode,
|
|
223
|
+
IndexOperatorWriteNode,
|
|
224
|
+
|
|
225
|
+
// Match
|
|
226
|
+
MatchWriteNode,
|
|
227
|
+
MatchLastLineNode,
|
|
228
|
+
InterpolatedMatchLastLineNode,
|
|
229
|
+
|
|
230
|
+
// Other
|
|
231
|
+
FlipFlopNode,
|
|
232
|
+
ImplicitNode,
|
|
233
|
+
ImplicitRestNode,
|
|
234
|
+
|
|
183
235
|
Unknown(String),
|
|
184
236
|
}
|
|
185
237
|
|
|
186
|
-
impl NodeType {
|
|
187
|
-
|
|
188
|
-
|
|
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 {
|
|
189
243
|
"program_node" => Self::ProgramNode,
|
|
190
244
|
"statements_node" => Self::StatementsNode,
|
|
191
245
|
"class_node" => Self::ClassNode,
|
|
@@ -290,8 +344,55 @@ impl NodeType {
|
|
|
290
344
|
"block_argument_node" => Self::BlockArgumentNode,
|
|
291
345
|
"multi_write_node" => Self::MultiWriteNode,
|
|
292
346
|
"multi_target_node" => Self::MultiTargetNode,
|
|
347
|
+
"source_file_node" => Self::SourceFileNode,
|
|
348
|
+
"source_line_node" => Self::SourceLineNode,
|
|
349
|
+
"source_encoding_node" => Self::SourceEncodingNode,
|
|
350
|
+
"pre_execution_node" => Self::PreExecutionNode,
|
|
351
|
+
"post_execution_node" => Self::PostExecutionNode,
|
|
352
|
+
// Numeric literals
|
|
353
|
+
"rational_node" => Self::RationalNode,
|
|
354
|
+
"imaginary_node" => Self::ImaginaryNode,
|
|
355
|
+
// String interpolation
|
|
356
|
+
"embedded_variable_node" => Self::EmbeddedVariableNode,
|
|
357
|
+
// Pattern matching patterns
|
|
358
|
+
"array_pattern_node" => Self::ArrayPatternNode,
|
|
359
|
+
"hash_pattern_node" => Self::HashPatternNode,
|
|
360
|
+
"find_pattern_node" => Self::FindPatternNode,
|
|
361
|
+
"capture_pattern_node" => Self::CapturePatternNode,
|
|
362
|
+
"alternation_pattern_node" => Self::AlternationPatternNode,
|
|
363
|
+
"pinned_expression_node" => Self::PinnedExpressionNode,
|
|
364
|
+
"pinned_variable_node" => Self::PinnedVariableNode,
|
|
365
|
+
// Forwarding
|
|
366
|
+
"forwarding_arguments_node" => Self::ForwardingArgumentsNode,
|
|
367
|
+
"forwarding_parameter_node" => Self::ForwardingParameterNode,
|
|
368
|
+
"no_keywords_parameter_node" => Self::NoKeywordsParameterNode,
|
|
369
|
+
// References
|
|
370
|
+
"back_reference_read_node" => Self::BackReferenceReadNode,
|
|
371
|
+
"numbered_reference_read_node" => Self::NumberedReferenceReadNode,
|
|
372
|
+
// Call/Index compound assignment
|
|
373
|
+
"call_and_write_node" => Self::CallAndWriteNode,
|
|
374
|
+
"call_or_write_node" => Self::CallOrWriteNode,
|
|
375
|
+
"call_operator_write_node" => Self::CallOperatorWriteNode,
|
|
376
|
+
"index_and_write_node" => Self::IndexAndWriteNode,
|
|
377
|
+
"index_or_write_node" => Self::IndexOrWriteNode,
|
|
378
|
+
"index_operator_write_node" => Self::IndexOperatorWriteNode,
|
|
379
|
+
// Match
|
|
380
|
+
"match_write_node" => Self::MatchWriteNode,
|
|
381
|
+
"match_last_line_node" => Self::MatchLastLineNode,
|
|
382
|
+
"interpolated_match_last_line_node" => Self::InterpolatedMatchLastLineNode,
|
|
383
|
+
// Other
|
|
384
|
+
"flip_flop_node" => Self::FlipFlopNode,
|
|
385
|
+
"implicit_node" => Self::ImplicitNode,
|
|
386
|
+
"implicit_rest_node" => Self::ImplicitRestNode,
|
|
293
387
|
_ => Self::Unknown(s.to_string()),
|
|
294
|
-
}
|
|
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()
|
|
295
396
|
}
|
|
296
397
|
|
|
297
398
|
/// Check if this node type is a definition (class, module, or method)
|
data/ext/rfmt/src/emitter/mod.rs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use crate::ast::{Comment, Node, NodeType};
|
|
2
2
|
use crate::config::{Config, IndentStyle};
|
|
3
3
|
use crate::error::Result;
|
|
4
|
+
use std::collections::{BTreeMap, HashSet};
|
|
4
5
|
use std::fmt::Write;
|
|
5
6
|
|
|
6
7
|
/// Block style for Ruby blocks
|
|
@@ -16,7 +17,12 @@ pub struct Emitter {
|
|
|
16
17
|
source: String,
|
|
17
18
|
buffer: String,
|
|
18
19
|
all_comments: Vec<Comment>,
|
|
19
|
-
emitted_comment_indices:
|
|
20
|
+
emitted_comment_indices: HashSet<usize>,
|
|
21
|
+
/// Cached indent strings by level (index = level, value = indent string)
|
|
22
|
+
indent_cache: Vec<String>,
|
|
23
|
+
/// Index of comment indices by start line for O(log n) lookup
|
|
24
|
+
/// Key: start_line, Value: Vec of comment indices that start on that line
|
|
25
|
+
comments_by_line: BTreeMap<usize, Vec<usize>>,
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
impl Emitter {
|
|
@@ -26,7 +32,9 @@ impl Emitter {
|
|
|
26
32
|
source: String::new(),
|
|
27
33
|
buffer: String::new(),
|
|
28
34
|
all_comments: Vec::new(),
|
|
29
|
-
emitted_comment_indices:
|
|
35
|
+
emitted_comment_indices: HashSet::new(),
|
|
36
|
+
indent_cache: Vec::new(),
|
|
37
|
+
comments_by_line: BTreeMap::new(),
|
|
30
38
|
}
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -37,7 +45,9 @@ impl Emitter {
|
|
|
37
45
|
source,
|
|
38
46
|
buffer: String::new(),
|
|
39
47
|
all_comments: Vec::new(),
|
|
40
|
-
emitted_comment_indices:
|
|
48
|
+
emitted_comment_indices: HashSet::new(),
|
|
49
|
+
indent_cache: Vec::new(),
|
|
50
|
+
comments_by_line: BTreeMap::new(),
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
|
|
@@ -45,8 +55,10 @@ impl Emitter {
|
|
|
45
55
|
pub fn emit(&mut self, ast: &Node) -> Result<String> {
|
|
46
56
|
self.buffer.clear();
|
|
47
57
|
self.emitted_comment_indices.clear();
|
|
58
|
+
self.comments_by_line.clear();
|
|
48
59
|
|
|
49
60
|
self.collect_comments(ast);
|
|
61
|
+
self.build_comment_index();
|
|
50
62
|
|
|
51
63
|
self.emit_node(ast, 0)?;
|
|
52
64
|
|
|
@@ -61,18 +73,19 @@ impl Emitter {
|
|
|
61
73
|
self.buffer.push('\n');
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
Ok(self.buffer
|
|
76
|
+
Ok(std::mem::take(&mut self.buffer))
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
/// Find the last line of code in the AST (excluding comments)
|
|
68
80
|
fn find_last_code_line(ast: &Node) -> usize {
|
|
69
81
|
let mut max_line = ast.location.end_line;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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());
|
|
75
87
|
}
|
|
88
|
+
|
|
76
89
|
max_line
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -107,7 +120,7 @@ impl Emitter {
|
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
writeln!(self.buffer, "{}", comment.text)?;
|
|
110
|
-
self.emitted_comment_indices.
|
|
123
|
+
self.emitted_comment_indices.insert(idx);
|
|
111
124
|
last_end_line = Some(comment.location.end_line);
|
|
112
125
|
is_first_comment = false;
|
|
113
126
|
}
|
|
@@ -122,28 +135,75 @@ impl Emitter {
|
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
|
|
138
|
+
/// Build the comment index by start line for O(log n) range lookups
|
|
139
|
+
fn build_comment_index(&mut self) {
|
|
140
|
+
for (idx, comment) in self.all_comments.iter().enumerate() {
|
|
141
|
+
self.comments_by_line
|
|
142
|
+
.entry(comment.location.start_line)
|
|
143
|
+
.or_default()
|
|
144
|
+
.push(idx);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Get comment indices in the given line range [start_line, end_line)
|
|
149
|
+
/// Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
|
|
150
|
+
fn get_comment_indices_in_range(&self, start_line: usize, end_line: usize) -> Vec<usize> {
|
|
151
|
+
self.comments_by_line
|
|
152
|
+
.range(start_line..end_line)
|
|
153
|
+
.flat_map(|(_, indices)| indices.iter().copied())
|
|
154
|
+
.filter(|&idx| !self.emitted_comment_indices.contains(&idx))
|
|
155
|
+
.collect()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Get comment indices before a given line (exclusive)
|
|
159
|
+
/// Uses BTreeMap range for O(log n) lookup
|
|
160
|
+
fn get_comment_indices_before(&self, line: usize) -> Vec<usize> {
|
|
161
|
+
self.comments_by_line
|
|
162
|
+
.range(..line)
|
|
163
|
+
.flat_map(|(_, indices)| indices.iter().copied())
|
|
164
|
+
.filter(|&idx| {
|
|
165
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
166
|
+
&& self.all_comments[idx].location.end_line < line
|
|
167
|
+
})
|
|
168
|
+
.collect()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Get comment indices on a specific line (for trailing comments)
|
|
172
|
+
/// Uses BTreeMap get for O(log n) lookup
|
|
173
|
+
fn get_comment_indices_on_line(&self, line: usize) -> Vec<usize> {
|
|
174
|
+
self.comments_by_line
|
|
175
|
+
.get(&line)
|
|
176
|
+
.map(|indices| {
|
|
177
|
+
indices
|
|
178
|
+
.iter()
|
|
179
|
+
.copied()
|
|
180
|
+
.filter(|&idx| !self.emitted_comment_indices.contains(&idx))
|
|
181
|
+
.collect()
|
|
182
|
+
})
|
|
183
|
+
.unwrap_or_default()
|
|
184
|
+
}
|
|
185
|
+
|
|
125
186
|
/// Emit comments that appear before a given line
|
|
187
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
126
188
|
fn emit_comments_before(&mut self, line: usize, indent_level: usize) -> Result<()> {
|
|
127
|
-
let indent_str =
|
|
128
|
-
IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * indent_level),
|
|
129
|
-
IndentStyle::Tabs => "\t".repeat(indent_level),
|
|
130
|
-
};
|
|
189
|
+
let indent_str = self.get_indent(indent_level).to_string();
|
|
131
190
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if self.emitted_comment_indices.contains(&idx) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
191
|
+
// Use indexed lookup instead of iterating all comments
|
|
192
|
+
let indices = self.get_comment_indices_before(line);
|
|
137
193
|
|
|
138
|
-
|
|
139
|
-
|
|
194
|
+
// Build list of comments to emit with their data
|
|
195
|
+
let mut comments_to_emit: Vec<_> = indices
|
|
196
|
+
.into_iter()
|
|
197
|
+
.map(|idx| {
|
|
198
|
+
let comment = &self.all_comments[idx];
|
|
199
|
+
(
|
|
140
200
|
idx,
|
|
141
201
|
comment.text.clone(),
|
|
142
202
|
comment.location.start_line,
|
|
143
203
|
comment.location.end_line,
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
.collect();
|
|
147
207
|
|
|
148
208
|
// Sort by start_line to emit in order
|
|
149
209
|
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
@@ -163,7 +223,7 @@ impl Emitter {
|
|
|
163
223
|
}
|
|
164
224
|
|
|
165
225
|
writeln!(self.buffer, "{}{}", indent_str, text)?;
|
|
166
|
-
self.emitted_comment_indices.
|
|
226
|
+
self.emitted_comment_indices.insert(idx);
|
|
167
227
|
last_comment_end_line = Some(comment_end_line);
|
|
168
228
|
|
|
169
229
|
// Add blank line after the LAST comment if there was a gap to the code
|
|
@@ -176,41 +236,44 @@ impl Emitter {
|
|
|
176
236
|
}
|
|
177
237
|
|
|
178
238
|
/// Check if there are any unemitted comments in the given line range
|
|
239
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
179
240
|
fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
|
|
180
|
-
self.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
self.comments_by_line
|
|
242
|
+
.range(start_line..end_line)
|
|
243
|
+
.flat_map(|(_, indices)| indices.iter())
|
|
244
|
+
.any(|&idx| {
|
|
245
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
246
|
+
&& self.all_comments[idx].location.end_line < end_line
|
|
247
|
+
})
|
|
185
248
|
}
|
|
186
249
|
|
|
187
250
|
/// Emit comments that are within a given line range (exclusive of end_line)
|
|
251
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
188
252
|
fn emit_comments_in_range(
|
|
189
253
|
&mut self,
|
|
190
254
|
start_line: usize,
|
|
191
255
|
end_line: usize,
|
|
192
256
|
indent_level: usize,
|
|
193
257
|
) -> Result<()> {
|
|
194
|
-
let indent_str =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
comments_to_emit.push((
|
|
258
|
+
let indent_str = self.get_indent(indent_level).to_string();
|
|
259
|
+
|
|
260
|
+
// Use indexed lookup instead of iterating all comments
|
|
261
|
+
let indices = self.get_comment_indices_in_range(start_line, end_line);
|
|
262
|
+
|
|
263
|
+
// Build list of comments to emit, filtering by end_line
|
|
264
|
+
let mut comments_to_emit: Vec<_> = indices
|
|
265
|
+
.into_iter()
|
|
266
|
+
.filter(|&idx| self.all_comments[idx].location.end_line < end_line)
|
|
267
|
+
.map(|idx| {
|
|
268
|
+
let comment = &self.all_comments[idx];
|
|
269
|
+
(
|
|
207
270
|
idx,
|
|
208
271
|
comment.text.clone(),
|
|
209
272
|
comment.location.start_line,
|
|
210
273
|
comment.location.end_line,
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
|
|
274
|
+
)
|
|
275
|
+
})
|
|
276
|
+
.collect();
|
|
214
277
|
|
|
215
278
|
// Sort by start_line to emit in order
|
|
216
279
|
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
@@ -227,7 +290,7 @@ impl Emitter {
|
|
|
227
290
|
}
|
|
228
291
|
|
|
229
292
|
writeln!(self.buffer, "{}{}", indent_str, text)?;
|
|
230
|
-
self.emitted_comment_indices.
|
|
293
|
+
self.emitted_comment_indices.insert(idx);
|
|
231
294
|
last_comment_end_line = Some(comment_end_line);
|
|
232
295
|
}
|
|
233
296
|
|
|
@@ -235,6 +298,7 @@ impl Emitter {
|
|
|
235
298
|
}
|
|
236
299
|
|
|
237
300
|
/// Emit comments that are within a given line range, preserving blank lines from prev_line
|
|
301
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
238
302
|
fn emit_comments_in_range_with_prev_line(
|
|
239
303
|
&mut self,
|
|
240
304
|
start_line: usize,
|
|
@@ -242,26 +306,25 @@ impl Emitter {
|
|
|
242
306
|
indent_level: usize,
|
|
243
307
|
prev_line: usize,
|
|
244
308
|
) -> Result<()> {
|
|
245
|
-
let indent_str =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
comments_to_emit.push((
|
|
309
|
+
let indent_str = self.get_indent(indent_level).to_string();
|
|
310
|
+
|
|
311
|
+
// Use indexed lookup instead of iterating all comments
|
|
312
|
+
let indices = self.get_comment_indices_in_range(start_line, end_line);
|
|
313
|
+
|
|
314
|
+
// Build list of comments to emit, filtering by end_line
|
|
315
|
+
let mut comments_to_emit: Vec<_> = indices
|
|
316
|
+
.into_iter()
|
|
317
|
+
.filter(|&idx| self.all_comments[idx].location.end_line < end_line)
|
|
318
|
+
.map(|idx| {
|
|
319
|
+
let comment = &self.all_comments[idx];
|
|
320
|
+
(
|
|
258
321
|
idx,
|
|
259
322
|
comment.text.clone(),
|
|
260
323
|
comment.location.start_line,
|
|
261
324
|
comment.location.end_line,
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
.collect();
|
|
265
328
|
|
|
266
329
|
// Sort by start_line to emit in order
|
|
267
330
|
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
@@ -276,7 +339,7 @@ impl Emitter {
|
|
|
276
339
|
}
|
|
277
340
|
|
|
278
341
|
writeln!(self.buffer, "{}{}", indent_str, text)?;
|
|
279
|
-
self.emitted_comment_indices.
|
|
342
|
+
self.emitted_comment_indices.insert(idx);
|
|
280
343
|
last_end_line = comment_end_line;
|
|
281
344
|
}
|
|
282
345
|
|
|
@@ -284,23 +347,21 @@ impl Emitter {
|
|
|
284
347
|
}
|
|
285
348
|
|
|
286
349
|
/// Emit comments that appear on the same line (trailing comments)
|
|
350
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
287
351
|
fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if self.emitted_comment_indices.contains(&idx) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
352
|
+
// Use indexed lookup for O(log n) access
|
|
353
|
+
let indices = self.get_comment_indices_on_line(line);
|
|
293
354
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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();
|
|
299
360
|
|
|
300
361
|
// Now emit the collected comments
|
|
301
362
|
for (idx, text) in indices_to_emit {
|
|
302
363
|
write!(self.buffer, " {}", text)?;
|
|
303
|
-
self.emitted_comment_indices.
|
|
364
|
+
self.emitted_comment_indices.insert(idx);
|
|
304
365
|
}
|
|
305
366
|
|
|
306
367
|
Ok(())
|
|
@@ -326,6 +387,9 @@ impl Emitter {
|
|
|
326
387
|
NodeType::WhileNode => self.emit_while_until(node, indent_level, "while")?,
|
|
327
388
|
NodeType::UntilNode => self.emit_while_until(node, indent_level, "until")?,
|
|
328
389
|
NodeType::ForNode => self.emit_for(node, indent_level)?,
|
|
390
|
+
NodeType::SingletonClassNode => self.emit_singleton_class(node, indent_level)?,
|
|
391
|
+
NodeType::CaseMatchNode => self.emit_case_match(node, indent_level)?,
|
|
392
|
+
NodeType::InNode => self.emit_in(node, indent_level)?,
|
|
329
393
|
_ => self.emit_generic(node, indent_level)?,
|
|
330
394
|
}
|
|
331
395
|
Ok(())
|
|
@@ -363,15 +427,12 @@ impl Emitter {
|
|
|
363
427
|
let next_start_line = next_child.location.start_line;
|
|
364
428
|
|
|
365
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
|
|
366
431
|
let first_comment_line = self
|
|
367
|
-
.
|
|
368
|
-
.
|
|
369
|
-
.
|
|
370
|
-
|
|
371
|
-
&& c.location.end_line < next_start_line
|
|
372
|
-
})
|
|
373
|
-
.map(|c| c.location.start_line)
|
|
374
|
-
.min();
|
|
432
|
+
.comments_by_line
|
|
433
|
+
.range((current_end_line + 1)..next_start_line)
|
|
434
|
+
.next()
|
|
435
|
+
.map(|(line, _)| *line);
|
|
375
436
|
|
|
376
437
|
// Calculate line diff based on whether there's a comment
|
|
377
438
|
let effective_next_line = first_comment_line.unwrap_or(next_start_line);
|
|
@@ -508,31 +569,17 @@ impl Emitter {
|
|
|
508
569
|
write!(self.buffer, "{}", name)?;
|
|
509
570
|
}
|
|
510
571
|
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if let Some(source_text) = self
|
|
523
|
-
.source
|
|
524
|
-
.get(node.location.start_offset..node.location.end_offset)
|
|
525
|
-
{
|
|
526
|
-
// Find parameters in source (between def name and \n or ;)
|
|
527
|
-
if let Some(def_line) = source_text.lines().next() {
|
|
528
|
-
if let Some(params_start) = def_line.find('(') {
|
|
529
|
-
if let Some(params_end) = def_line.find(')') {
|
|
530
|
-
let params = &def_line[params_start..=params_end];
|
|
531
|
-
write!(self.buffer, "{}", params)?;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
572
|
+
// Emit parameters using metadata from prism_bridge
|
|
573
|
+
if let Some(params_text) = node.metadata.get("parameters_text") {
|
|
574
|
+
let has_parens = node
|
|
575
|
+
.metadata
|
|
576
|
+
.get("has_parens")
|
|
577
|
+
.map(|v| v == "true")
|
|
578
|
+
.unwrap_or(false);
|
|
579
|
+
if has_parens {
|
|
580
|
+
write!(self.buffer, "({})", params_text)?;
|
|
581
|
+
} else {
|
|
582
|
+
write!(self.buffer, " {}", params_text)?;
|
|
536
583
|
}
|
|
537
584
|
}
|
|
538
585
|
|
|
@@ -1221,7 +1268,7 @@ impl Emitter {
|
|
|
1221
1268
|
&& comment.location.start_line >= node.location.start_line
|
|
1222
1269
|
&& comment.location.end_line < node.location.end_line
|
|
1223
1270
|
{
|
|
1224
|
-
self.emitted_comment_indices.
|
|
1271
|
+
self.emitted_comment_indices.insert(idx);
|
|
1225
1272
|
}
|
|
1226
1273
|
}
|
|
1227
1274
|
|
|
@@ -1254,7 +1301,7 @@ impl Emitter {
|
|
|
1254
1301
|
&& comment.location.start_line >= node.location.start_line
|
|
1255
1302
|
&& comment.location.end_line < node.location.end_line
|
|
1256
1303
|
{
|
|
1257
|
-
self.emitted_comment_indices.
|
|
1304
|
+
self.emitted_comment_indices.insert(idx);
|
|
1258
1305
|
}
|
|
1259
1306
|
}
|
|
1260
1307
|
|
|
@@ -1264,13 +1311,23 @@ impl Emitter {
|
|
|
1264
1311
|
Ok(())
|
|
1265
1312
|
}
|
|
1266
1313
|
|
|
1314
|
+
/// Get cached indent string for a given level
|
|
1315
|
+
fn get_indent(&mut self, level: usize) -> &str {
|
|
1316
|
+
// Extend cache if needed
|
|
1317
|
+
while self.indent_cache.len() <= level {
|
|
1318
|
+
let len = self.indent_cache.len();
|
|
1319
|
+
let indent = match self.config.formatting.indent_style {
|
|
1320
|
+
IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * len),
|
|
1321
|
+
IndentStyle::Tabs => "\t".repeat(len),
|
|
1322
|
+
};
|
|
1323
|
+
self.indent_cache.push(indent);
|
|
1324
|
+
}
|
|
1325
|
+
&self.indent_cache[level]
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1267
1328
|
/// Emit indentation
|
|
1268
1329
|
fn emit_indent(&mut self, level: usize) -> Result<()> {
|
|
1269
|
-
let indent_str =
|
|
1270
|
-
IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * level),
|
|
1271
|
-
IndentStyle::Tabs => "\t".repeat(level),
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1330
|
+
let indent_str = self.get_indent(level).to_string();
|
|
1274
1331
|
write!(self.buffer, "{}", indent_str)?;
|
|
1275
1332
|
Ok(())
|
|
1276
1333
|
}
|
|
@@ -1382,6 +1439,148 @@ impl Emitter {
|
|
|
1382
1439
|
Ok(())
|
|
1383
1440
|
}
|
|
1384
1441
|
|
|
1442
|
+
/// Emit singleton class definition (class << self / class << object)
|
|
1443
|
+
fn emit_singleton_class(&mut self, node: &Node, indent_level: usize) -> Result<()> {
|
|
1444
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
1445
|
+
self.emit_indent(indent_level)?;
|
|
1446
|
+
|
|
1447
|
+
write!(self.buffer, "class << ")?;
|
|
1448
|
+
|
|
1449
|
+
// First child is the expression (self or an object)
|
|
1450
|
+
if let Some(expression) = node.children.first() {
|
|
1451
|
+
if !self.source.is_empty() {
|
|
1452
|
+
let start = expression.location.start_offset;
|
|
1453
|
+
let end = expression.location.end_offset;
|
|
1454
|
+
if let Some(text) = self.source.get(start..end) {
|
|
1455
|
+
write!(self.buffer, "{}", text)?;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Emit trailing comments on the class << line
|
|
1461
|
+
self.emit_trailing_comments(node.location.start_line)?;
|
|
1462
|
+
self.buffer.push('\n');
|
|
1463
|
+
|
|
1464
|
+
let class_start_line = node.location.start_line;
|
|
1465
|
+
let class_end_line = node.location.end_line;
|
|
1466
|
+
let mut has_body_content = false;
|
|
1467
|
+
|
|
1468
|
+
// Emit body (skip the first child which is the expression)
|
|
1469
|
+
for (i, child) in node.children.iter().enumerate() {
|
|
1470
|
+
if i == 0 {
|
|
1471
|
+
// Skip the expression (self or object)
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if matches!(child.node_type, NodeType::StatementsNode) {
|
|
1475
|
+
has_body_content = true;
|
|
1476
|
+
self.emit_statements(child, indent_level + 1)?;
|
|
1477
|
+
} else if !self.is_structural_node(&child.node_type) {
|
|
1478
|
+
has_body_content = true;
|
|
1479
|
+
self.emit_node(child, indent_level + 1)?;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Emit comments inside the singleton class body
|
|
1484
|
+
self.emit_comments_in_range(class_start_line + 1, class_end_line, indent_level + 1)?;
|
|
1485
|
+
|
|
1486
|
+
// Add newline before end if there was body content
|
|
1487
|
+
if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
|
|
1488
|
+
&& !self.buffer.ends_with('\n')
|
|
1489
|
+
{
|
|
1490
|
+
self.buffer.push('\n');
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
self.emit_indent(indent_level)?;
|
|
1494
|
+
write!(self.buffer, "end")?;
|
|
1495
|
+
self.emit_trailing_comments(node.location.end_line)?;
|
|
1496
|
+
|
|
1497
|
+
Ok(())
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/// Emit case match (Ruby 3.0+ pattern matching with case...in)
|
|
1501
|
+
fn emit_case_match(&mut self, node: &Node, indent_level: usize) -> Result<()> {
|
|
1502
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
1503
|
+
self.emit_indent(indent_level)?;
|
|
1504
|
+
|
|
1505
|
+
// Write "case" keyword
|
|
1506
|
+
write!(self.buffer, "case")?;
|
|
1507
|
+
|
|
1508
|
+
// Find predicate (first child that isn't InNode or ElseNode)
|
|
1509
|
+
let mut in_start_idx = 0;
|
|
1510
|
+
if let Some(first_child) = node.children.first() {
|
|
1511
|
+
if !matches!(first_child.node_type, NodeType::InNode | NodeType::ElseNode) {
|
|
1512
|
+
// This is the predicate - extract from source
|
|
1513
|
+
let start = first_child.location.start_offset;
|
|
1514
|
+
let end = first_child.location.end_offset;
|
|
1515
|
+
if let Some(text) = self.source.get(start..end) {
|
|
1516
|
+
write!(self.buffer, " {}", text)?;
|
|
1517
|
+
}
|
|
1518
|
+
in_start_idx = 1;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
self.buffer.push('\n');
|
|
1523
|
+
|
|
1524
|
+
// Emit in clauses and else
|
|
1525
|
+
for child in node.children.iter().skip(in_start_idx) {
|
|
1526
|
+
match &child.node_type {
|
|
1527
|
+
NodeType::InNode => {
|
|
1528
|
+
self.emit_in(child, indent_level)?;
|
|
1529
|
+
self.buffer.push('\n');
|
|
1530
|
+
}
|
|
1531
|
+
NodeType::ElseNode => {
|
|
1532
|
+
self.emit_indent(indent_level)?;
|
|
1533
|
+
writeln!(self.buffer, "else")?;
|
|
1534
|
+
// Emit else body
|
|
1535
|
+
for else_child in &child.children {
|
|
1536
|
+
if matches!(else_child.node_type, NodeType::StatementsNode) {
|
|
1537
|
+
self.emit_statements(else_child, indent_level + 1)?;
|
|
1538
|
+
} else {
|
|
1539
|
+
self.emit_node(else_child, indent_level + 1)?;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
self.buffer.push('\n');
|
|
1543
|
+
}
|
|
1544
|
+
_ => {}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Emit "end" keyword
|
|
1549
|
+
self.emit_indent(indent_level)?;
|
|
1550
|
+
write!(self.buffer, "end")?;
|
|
1551
|
+
self.emit_trailing_comments(node.location.end_line)?;
|
|
1552
|
+
|
|
1553
|
+
Ok(())
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/// Emit in node (pattern matching clause)
|
|
1557
|
+
fn emit_in(&mut self, node: &Node, indent_level: usize) -> Result<()> {
|
|
1558
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
1559
|
+
self.emit_indent(indent_level)?;
|
|
1560
|
+
|
|
1561
|
+
write!(self.buffer, "in ")?;
|
|
1562
|
+
|
|
1563
|
+
// First child is the pattern
|
|
1564
|
+
if let Some(pattern) = node.children.first() {
|
|
1565
|
+
let start = pattern.location.start_offset;
|
|
1566
|
+
let end = pattern.location.end_offset;
|
|
1567
|
+
if let Some(text) = self.source.get(start..end) {
|
|
1568
|
+
write!(self.buffer, "{}", text)?;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
self.buffer.push('\n');
|
|
1573
|
+
|
|
1574
|
+
// Second child is the statements body
|
|
1575
|
+
if let Some(statements) = node.children.get(1) {
|
|
1576
|
+
if matches!(statements.node_type, NodeType::StatementsNode) {
|
|
1577
|
+
self.emit_statements(statements, indent_level + 1)?;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
Ok(())
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1385
1584
|
/// Check if node is structural (part of definition syntax, not body)
|
|
1386
1585
|
/// These nodes are part of class/module/method definitions and should not be emitted as body
|
|
1387
1586
|
fn is_structural_node(&self, node_type: &NodeType) -> bool {
|
|
@@ -1398,6 +1597,8 @@ impl Emitter {
|
|
|
1398
1597
|
| NodeType::OptionalKeywordParameterNode
|
|
1399
1598
|
| NodeType::KeywordRestParameterNode
|
|
1400
1599
|
| NodeType::BlockParameterNode
|
|
1600
|
+
| NodeType::ForwardingParameterNode
|
|
1601
|
+
| NodeType::NoKeywordsParameterNode
|
|
1401
1602
|
)
|
|
1402
1603
|
}
|
|
1403
1604
|
}
|
|
@@ -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
|
+
}
|
data/lib/rfmt/prism_bridge.rb
CHANGED
|
@@ -56,7 +56,7 @@ module Rfmt
|
|
|
56
56
|
|
|
57
57
|
# Serialize the Prism AST to JSON
|
|
58
58
|
def self.serialize_ast(node)
|
|
59
|
-
JSON.generate(convert_node(node))
|
|
59
|
+
JSON.generate(convert_node(node), max_nesting: false)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Serialize the Prism AST with comments to JSON
|
|
@@ -80,7 +80,7 @@ module Rfmt
|
|
|
80
80
|
JSON.generate({
|
|
81
81
|
ast: convert_node(result.value),
|
|
82
82
|
comments: comments
|
|
83
|
-
})
|
|
83
|
+
}, max_nesting: false)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Convert a Prism node to our internal representation
|
|
@@ -241,8 +241,6 @@ module Rfmt
|
|
|
241
241
|
[node.expression].compact
|
|
242
242
|
when Prism::AndNode
|
|
243
243
|
[node.left, node.right].compact
|
|
244
|
-
when Prism::NotNode
|
|
245
|
-
[node.expression].compact
|
|
246
244
|
when Prism::InterpolatedRegularExpressionNode, Prism::InterpolatedSymbolNode,
|
|
247
245
|
Prism::InterpolatedXStringNode
|
|
248
246
|
node.parts || []
|
|
@@ -293,6 +291,55 @@ module Rfmt
|
|
|
293
291
|
[*node.lefts, node.rest, *node.rights, node.value].compact
|
|
294
292
|
when Prism::MultiTargetNode
|
|
295
293
|
[*node.lefts, node.rest, *node.rights].compact
|
|
294
|
+
when Prism::SourceFileNode, Prism::SourceLineNode, Prism::SourceEncodingNode
|
|
295
|
+
[]
|
|
296
|
+
when Prism::PreExecutionNode, Prism::PostExecutionNode
|
|
297
|
+
[node.statements].compact
|
|
298
|
+
# Numeric literals
|
|
299
|
+
when Prism::RationalNode, Prism::ImaginaryNode
|
|
300
|
+
[node.numeric].compact
|
|
301
|
+
# String interpolation
|
|
302
|
+
when Prism::EmbeddedVariableNode
|
|
303
|
+
[node.variable].compact
|
|
304
|
+
# Pattern matching patterns
|
|
305
|
+
when Prism::ArrayPatternNode
|
|
306
|
+
[*node.requireds, node.rest, *node.posts].compact
|
|
307
|
+
when Prism::HashPatternNode
|
|
308
|
+
[*node.elements, node.rest].compact
|
|
309
|
+
when Prism::FindPatternNode
|
|
310
|
+
[node.left, *node.requireds, node.right].compact
|
|
311
|
+
when Prism::CapturePatternNode
|
|
312
|
+
[node.value, node.target].compact
|
|
313
|
+
when Prism::AlternationPatternNode
|
|
314
|
+
[node.left, node.right].compact
|
|
315
|
+
when Prism::PinnedExpressionNode
|
|
316
|
+
[node.expression].compact
|
|
317
|
+
when Prism::PinnedVariableNode
|
|
318
|
+
[node.variable].compact
|
|
319
|
+
# Forwarding and special parameters
|
|
320
|
+
when Prism::ForwardingArgumentsNode, Prism::ForwardingParameterNode,
|
|
321
|
+
Prism::NoKeywordsParameterNode
|
|
322
|
+
[]
|
|
323
|
+
# References
|
|
324
|
+
when Prism::BackReferenceReadNode, Prism::NumberedReferenceReadNode
|
|
325
|
+
[]
|
|
326
|
+
# Call/Index compound assignment
|
|
327
|
+
when Prism::CallAndWriteNode, Prism::CallOrWriteNode, Prism::CallOperatorWriteNode
|
|
328
|
+
[node.receiver, node.value].compact
|
|
329
|
+
when Prism::IndexAndWriteNode, Prism::IndexOrWriteNode, Prism::IndexOperatorWriteNode
|
|
330
|
+
[node.receiver, node.arguments, node.value].compact
|
|
331
|
+
# Match
|
|
332
|
+
when Prism::MatchWriteNode
|
|
333
|
+
[node.call, *node.targets].compact
|
|
334
|
+
when Prism::MatchLastLineNode, Prism::InterpolatedMatchLastLineNode
|
|
335
|
+
[]
|
|
336
|
+
# Other
|
|
337
|
+
when Prism::FlipFlopNode
|
|
338
|
+
[node.left, node.right].compact
|
|
339
|
+
when Prism::ImplicitNode
|
|
340
|
+
[node.value].compact
|
|
341
|
+
when Prism::ImplicitRestNode
|
|
342
|
+
[]
|
|
296
343
|
else
|
|
297
344
|
# For unknown types, try to get child nodes if they exist
|
|
298
345
|
[]
|
|
@@ -327,6 +374,11 @@ module Rfmt
|
|
|
327
374
|
metadata['name'] = name
|
|
328
375
|
end
|
|
329
376
|
metadata['parameters_count'] = extract_parameter_count(node).to_s
|
|
377
|
+
# Extract parameters text directly from source
|
|
378
|
+
if node.parameters
|
|
379
|
+
metadata['parameters_text'] = node.parameters.location.slice
|
|
380
|
+
metadata['has_parens'] = (!node.lparen_loc.nil?).to_s
|
|
381
|
+
end
|
|
330
382
|
# Check if this is a class method (def self.method_name)
|
|
331
383
|
if node.respond_to?(:receiver) && node.receiver
|
|
332
384
|
receiver = node.receiver
|
data/lib/rfmt/version.rb
CHANGED
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.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-
|
|
11
|
+
date: 2026-01-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|