rfmt 1.3.0 → 1.3.1
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 +16 -0
- data/Cargo.lock +1 -1
- data/ext/rfmt/Cargo.toml +1 -1
- data/ext/rfmt/src/ast/mod.rs +92 -0
- data/ext/rfmt/src/emitter/mod.rs +305 -102
- 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: 3711e11ba344ee0b9e1e76d0885fd99bd7ccbd7901ab5ba819619ae40647365f
|
|
4
|
+
data.tar.gz: 95f1b17c4c9a542cc24d9b89df6dee44398f59d27da0446961ac320b338a6bb4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bb8aff9825e2a8d44f3161786b7952a2b2e3b679d0e607eb748102971d4c8a417ca8dcaac80e171a97ef223814c229b14b63af505321309393edc9432550fe6
|
|
7
|
+
data.tar.gz: 110a42ff056ac3dd6939d0f67b50a4784bbdafd513c4c0a1ab6e9eb8d0a040a0aa65bc39d0b37975ffb1972cf81d8396868d33e627b71ed017894cf1674d3cc4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.3.1] - 2026-01-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Dedicated emitters for SingletonClassNode and pattern matching (CaseMatchNode, InNode)
|
|
7
|
+
- Literal and pattern match node support
|
|
8
|
+
- NoKeywordsParameterNode support
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Performance: Comment lookup optimized with BTreeMap index (O(n) → O(log n))
|
|
12
|
+
- Performance: HashSet for comment tracking (O(n) → O(1) contains check)
|
|
13
|
+
- Performance: Cached indent strings to avoid repeated allocations
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- DefNode parameter handling and missing node types
|
|
17
|
+
- Deep nest JSON parse error (max_nesting: false)
|
|
18
|
+
|
|
3
19
|
## [1.3.0] - 2026-01-07
|
|
4
20
|
|
|
5
21
|
### Added
|
data/Cargo.lock
CHANGED
data/ext/rfmt/Cargo.toml
CHANGED
data/ext/rfmt/src/ast/mod.rs
CHANGED
|
@@ -180,6 +180,58 @@ 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
|
|
|
@@ -290,6 +342,46 @@ impl NodeType {
|
|
|
290
342
|
"block_argument_node" => Self::BlockArgumentNode,
|
|
291
343
|
"multi_write_node" => Self::MultiWriteNode,
|
|
292
344
|
"multi_target_node" => Self::MultiTargetNode,
|
|
345
|
+
"source_file_node" => Self::SourceFileNode,
|
|
346
|
+
"source_line_node" => Self::SourceLineNode,
|
|
347
|
+
"source_encoding_node" => Self::SourceEncodingNode,
|
|
348
|
+
"pre_execution_node" => Self::PreExecutionNode,
|
|
349
|
+
"post_execution_node" => Self::PostExecutionNode,
|
|
350
|
+
// Numeric literals
|
|
351
|
+
"rational_node" => Self::RationalNode,
|
|
352
|
+
"imaginary_node" => Self::ImaginaryNode,
|
|
353
|
+
// String interpolation
|
|
354
|
+
"embedded_variable_node" => Self::EmbeddedVariableNode,
|
|
355
|
+
// Pattern matching patterns
|
|
356
|
+
"array_pattern_node" => Self::ArrayPatternNode,
|
|
357
|
+
"hash_pattern_node" => Self::HashPatternNode,
|
|
358
|
+
"find_pattern_node" => Self::FindPatternNode,
|
|
359
|
+
"capture_pattern_node" => Self::CapturePatternNode,
|
|
360
|
+
"alternation_pattern_node" => Self::AlternationPatternNode,
|
|
361
|
+
"pinned_expression_node" => Self::PinnedExpressionNode,
|
|
362
|
+
"pinned_variable_node" => Self::PinnedVariableNode,
|
|
363
|
+
// Forwarding
|
|
364
|
+
"forwarding_arguments_node" => Self::ForwardingArgumentsNode,
|
|
365
|
+
"forwarding_parameter_node" => Self::ForwardingParameterNode,
|
|
366
|
+
"no_keywords_parameter_node" => Self::NoKeywordsParameterNode,
|
|
367
|
+
// References
|
|
368
|
+
"back_reference_read_node" => Self::BackReferenceReadNode,
|
|
369
|
+
"numbered_reference_read_node" => Self::NumberedReferenceReadNode,
|
|
370
|
+
// Call/Index compound assignment
|
|
371
|
+
"call_and_write_node" => Self::CallAndWriteNode,
|
|
372
|
+
"call_or_write_node" => Self::CallOrWriteNode,
|
|
373
|
+
"call_operator_write_node" => Self::CallOperatorWriteNode,
|
|
374
|
+
"index_and_write_node" => Self::IndexAndWriteNode,
|
|
375
|
+
"index_or_write_node" => Self::IndexOrWriteNode,
|
|
376
|
+
"index_operator_write_node" => Self::IndexOperatorWriteNode,
|
|
377
|
+
// Match
|
|
378
|
+
"match_write_node" => Self::MatchWriteNode,
|
|
379
|
+
"match_last_line_node" => Self::MatchLastLineNode,
|
|
380
|
+
"interpolated_match_last_line_node" => Self::InterpolatedMatchLastLineNode,
|
|
381
|
+
// Other
|
|
382
|
+
"flip_flop_node" => Self::FlipFlopNode,
|
|
383
|
+
"implicit_node" => Self::ImplicitNode,
|
|
384
|
+
"implicit_rest_node" => Self::ImplicitRestNode,
|
|
293
385
|
_ => Self::Unknown(s.to_string()),
|
|
294
386
|
}
|
|
295
387
|
}
|
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,7 +73,7 @@ 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)
|
|
@@ -107,7 +119,7 @@ impl Emitter {
|
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
writeln!(self.buffer, "{}", comment.text)?;
|
|
110
|
-
self.emitted_comment_indices.
|
|
122
|
+
self.emitted_comment_indices.insert(idx);
|
|
111
123
|
last_end_line = Some(comment.location.end_line);
|
|
112
124
|
is_first_comment = false;
|
|
113
125
|
}
|
|
@@ -122,28 +134,75 @@ impl Emitter {
|
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
/// Build the comment index by start line for O(log n) range lookups
|
|
138
|
+
fn build_comment_index(&mut self) {
|
|
139
|
+
for (idx, comment) in self.all_comments.iter().enumerate() {
|
|
140
|
+
self.comments_by_line
|
|
141
|
+
.entry(comment.location.start_line)
|
|
142
|
+
.or_default()
|
|
143
|
+
.push(idx);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Get comment indices in the given line range [start_line, end_line)
|
|
148
|
+
/// Uses BTreeMap range for O(log n) lookup instead of O(n) iteration
|
|
149
|
+
fn get_comment_indices_in_range(&self, start_line: usize, end_line: usize) -> Vec<usize> {
|
|
150
|
+
self.comments_by_line
|
|
151
|
+
.range(start_line..end_line)
|
|
152
|
+
.flat_map(|(_, indices)| indices.iter().copied())
|
|
153
|
+
.filter(|&idx| !self.emitted_comment_indices.contains(&idx))
|
|
154
|
+
.collect()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Get comment indices before a given line (exclusive)
|
|
158
|
+
/// Uses BTreeMap range for O(log n) lookup
|
|
159
|
+
fn get_comment_indices_before(&self, line: usize) -> Vec<usize> {
|
|
160
|
+
self.comments_by_line
|
|
161
|
+
.range(..line)
|
|
162
|
+
.flat_map(|(_, indices)| indices.iter().copied())
|
|
163
|
+
.filter(|&idx| {
|
|
164
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
165
|
+
&& self.all_comments[idx].location.end_line < line
|
|
166
|
+
})
|
|
167
|
+
.collect()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Get comment indices on a specific line (for trailing comments)
|
|
171
|
+
/// Uses BTreeMap get for O(log n) lookup
|
|
172
|
+
fn get_comment_indices_on_line(&self, line: usize) -> Vec<usize> {
|
|
173
|
+
self.comments_by_line
|
|
174
|
+
.get(&line)
|
|
175
|
+
.map(|indices| {
|
|
176
|
+
indices
|
|
177
|
+
.iter()
|
|
178
|
+
.copied()
|
|
179
|
+
.filter(|&idx| !self.emitted_comment_indices.contains(&idx))
|
|
180
|
+
.collect()
|
|
181
|
+
})
|
|
182
|
+
.unwrap_or_default()
|
|
183
|
+
}
|
|
184
|
+
|
|
125
185
|
/// Emit comments that appear before a given line
|
|
186
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
126
187
|
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
|
-
};
|
|
188
|
+
let indent_str = self.get_indent(indent_level).to_string();
|
|
131
189
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if self.emitted_comment_indices.contains(&idx) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
190
|
+
// Use indexed lookup instead of iterating all comments
|
|
191
|
+
let indices = self.get_comment_indices_before(line);
|
|
137
192
|
|
|
138
|
-
|
|
139
|
-
|
|
193
|
+
// Build list of comments to emit with their data
|
|
194
|
+
let mut comments_to_emit: Vec<_> = indices
|
|
195
|
+
.into_iter()
|
|
196
|
+
.map(|idx| {
|
|
197
|
+
let comment = &self.all_comments[idx];
|
|
198
|
+
(
|
|
140
199
|
idx,
|
|
141
200
|
comment.text.clone(),
|
|
142
201
|
comment.location.start_line,
|
|
143
202
|
comment.location.end_line,
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
.collect();
|
|
147
206
|
|
|
148
207
|
// Sort by start_line to emit in order
|
|
149
208
|
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
@@ -163,7 +222,7 @@ impl Emitter {
|
|
|
163
222
|
}
|
|
164
223
|
|
|
165
224
|
writeln!(self.buffer, "{}{}", indent_str, text)?;
|
|
166
|
-
self.emitted_comment_indices.
|
|
225
|
+
self.emitted_comment_indices.insert(idx);
|
|
167
226
|
last_comment_end_line = Some(comment_end_line);
|
|
168
227
|
|
|
169
228
|
// Add blank line after the LAST comment if there was a gap to the code
|
|
@@ -176,41 +235,44 @@ impl Emitter {
|
|
|
176
235
|
}
|
|
177
236
|
|
|
178
237
|
/// Check if there are any unemitted comments in the given line range
|
|
238
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
179
239
|
fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
|
|
180
|
-
self.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
240
|
+
self.comments_by_line
|
|
241
|
+
.range(start_line..end_line)
|
|
242
|
+
.flat_map(|(_, indices)| indices.iter())
|
|
243
|
+
.any(|&idx| {
|
|
244
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
245
|
+
&& self.all_comments[idx].location.end_line < end_line
|
|
246
|
+
})
|
|
185
247
|
}
|
|
186
248
|
|
|
187
249
|
/// Emit comments that are within a given line range (exclusive of end_line)
|
|
250
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
188
251
|
fn emit_comments_in_range(
|
|
189
252
|
&mut self,
|
|
190
253
|
start_line: usize,
|
|
191
254
|
end_line: usize,
|
|
192
255
|
indent_level: usize,
|
|
193
256
|
) -> Result<()> {
|
|
194
|
-
let indent_str =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
comments_to_emit.push((
|
|
257
|
+
let indent_str = self.get_indent(indent_level).to_string();
|
|
258
|
+
|
|
259
|
+
// Use indexed lookup instead of iterating all comments
|
|
260
|
+
let indices = self.get_comment_indices_in_range(start_line, end_line);
|
|
261
|
+
|
|
262
|
+
// Build list of comments to emit, filtering by end_line
|
|
263
|
+
let mut comments_to_emit: Vec<_> = indices
|
|
264
|
+
.into_iter()
|
|
265
|
+
.filter(|&idx| self.all_comments[idx].location.end_line < end_line)
|
|
266
|
+
.map(|idx| {
|
|
267
|
+
let comment = &self.all_comments[idx];
|
|
268
|
+
(
|
|
207
269
|
idx,
|
|
208
270
|
comment.text.clone(),
|
|
209
271
|
comment.location.start_line,
|
|
210
272
|
comment.location.end_line,
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
|
|
273
|
+
)
|
|
274
|
+
})
|
|
275
|
+
.collect();
|
|
214
276
|
|
|
215
277
|
// Sort by start_line to emit in order
|
|
216
278
|
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
@@ -227,7 +289,7 @@ impl Emitter {
|
|
|
227
289
|
}
|
|
228
290
|
|
|
229
291
|
writeln!(self.buffer, "{}{}", indent_str, text)?;
|
|
230
|
-
self.emitted_comment_indices.
|
|
292
|
+
self.emitted_comment_indices.insert(idx);
|
|
231
293
|
last_comment_end_line = Some(comment_end_line);
|
|
232
294
|
}
|
|
233
295
|
|
|
@@ -235,6 +297,7 @@ impl Emitter {
|
|
|
235
297
|
}
|
|
236
298
|
|
|
237
299
|
/// Emit comments that are within a given line range, preserving blank lines from prev_line
|
|
300
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
238
301
|
fn emit_comments_in_range_with_prev_line(
|
|
239
302
|
&mut self,
|
|
240
303
|
start_line: usize,
|
|
@@ -242,26 +305,25 @@ impl Emitter {
|
|
|
242
305
|
indent_level: usize,
|
|
243
306
|
prev_line: usize,
|
|
244
307
|
) -> Result<()> {
|
|
245
|
-
let indent_str =
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
comments_to_emit.push((
|
|
308
|
+
let indent_str = self.get_indent(indent_level).to_string();
|
|
309
|
+
|
|
310
|
+
// Use indexed lookup instead of iterating all comments
|
|
311
|
+
let indices = self.get_comment_indices_in_range(start_line, end_line);
|
|
312
|
+
|
|
313
|
+
// Build list of comments to emit, filtering by end_line
|
|
314
|
+
let mut comments_to_emit: Vec<_> = indices
|
|
315
|
+
.into_iter()
|
|
316
|
+
.filter(|&idx| self.all_comments[idx].location.end_line < end_line)
|
|
317
|
+
.map(|idx| {
|
|
318
|
+
let comment = &self.all_comments[idx];
|
|
319
|
+
(
|
|
258
320
|
idx,
|
|
259
321
|
comment.text.clone(),
|
|
260
322
|
comment.location.start_line,
|
|
261
323
|
comment.location.end_line,
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
324
|
+
)
|
|
325
|
+
})
|
|
326
|
+
.collect();
|
|
265
327
|
|
|
266
328
|
// Sort by start_line to emit in order
|
|
267
329
|
comments_to_emit.sort_by_key(|(_, _, start, _)| *start);
|
|
@@ -276,7 +338,7 @@ impl Emitter {
|
|
|
276
338
|
}
|
|
277
339
|
|
|
278
340
|
writeln!(self.buffer, "{}{}", indent_str, text)?;
|
|
279
|
-
self.emitted_comment_indices.
|
|
341
|
+
self.emitted_comment_indices.insert(idx);
|
|
280
342
|
last_end_line = comment_end_line;
|
|
281
343
|
}
|
|
282
344
|
|
|
@@ -284,23 +346,21 @@ impl Emitter {
|
|
|
284
346
|
}
|
|
285
347
|
|
|
286
348
|
/// Emit comments that appear on the same line (trailing comments)
|
|
349
|
+
/// Uses BTreeMap index for O(log n) lookup instead of O(n) iteration
|
|
287
350
|
fn emit_trailing_comments(&mut self, line: usize) -> Result<()> {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if self.emitted_comment_indices.contains(&idx) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
351
|
+
// Use indexed lookup for O(log n) access
|
|
352
|
+
let indices = self.get_comment_indices_on_line(line);
|
|
293
353
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
354
|
+
// Build list of comments to emit
|
|
355
|
+
let indices_to_emit: Vec<_> = indices
|
|
356
|
+
.into_iter()
|
|
357
|
+
.map(|idx| (idx, self.all_comments[idx].text.clone()))
|
|
358
|
+
.collect();
|
|
299
359
|
|
|
300
360
|
// Now emit the collected comments
|
|
301
361
|
for (idx, text) in indices_to_emit {
|
|
302
362
|
write!(self.buffer, " {}", text)?;
|
|
303
|
-
self.emitted_comment_indices.
|
|
363
|
+
self.emitted_comment_indices.insert(idx);
|
|
304
364
|
}
|
|
305
365
|
|
|
306
366
|
Ok(())
|
|
@@ -326,6 +386,9 @@ impl Emitter {
|
|
|
326
386
|
NodeType::WhileNode => self.emit_while_until(node, indent_level, "while")?,
|
|
327
387
|
NodeType::UntilNode => self.emit_while_until(node, indent_level, "until")?,
|
|
328
388
|
NodeType::ForNode => self.emit_for(node, indent_level)?,
|
|
389
|
+
NodeType::SingletonClassNode => self.emit_singleton_class(node, indent_level)?,
|
|
390
|
+
NodeType::CaseMatchNode => self.emit_case_match(node, indent_level)?,
|
|
391
|
+
NodeType::InNode => self.emit_in(node, indent_level)?,
|
|
329
392
|
_ => self.emit_generic(node, indent_level)?,
|
|
330
393
|
}
|
|
331
394
|
Ok(())
|
|
@@ -508,31 +571,17 @@ impl Emitter {
|
|
|
508
571
|
write!(self.buffer, "{}", name)?;
|
|
509
572
|
}
|
|
510
573
|
|
|
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
|
-
}
|
|
574
|
+
// Emit parameters using metadata from prism_bridge
|
|
575
|
+
if let Some(params_text) = node.metadata.get("parameters_text") {
|
|
576
|
+
let has_parens = node
|
|
577
|
+
.metadata
|
|
578
|
+
.get("has_parens")
|
|
579
|
+
.map(|v| v == "true")
|
|
580
|
+
.unwrap_or(false);
|
|
581
|
+
if has_parens {
|
|
582
|
+
write!(self.buffer, "({})", params_text)?;
|
|
583
|
+
} else {
|
|
584
|
+
write!(self.buffer, " {}", params_text)?;
|
|
536
585
|
}
|
|
537
586
|
}
|
|
538
587
|
|
|
@@ -1221,7 +1270,7 @@ impl Emitter {
|
|
|
1221
1270
|
&& comment.location.start_line >= node.location.start_line
|
|
1222
1271
|
&& comment.location.end_line < node.location.end_line
|
|
1223
1272
|
{
|
|
1224
|
-
self.emitted_comment_indices.
|
|
1273
|
+
self.emitted_comment_indices.insert(idx);
|
|
1225
1274
|
}
|
|
1226
1275
|
}
|
|
1227
1276
|
|
|
@@ -1254,7 +1303,7 @@ impl Emitter {
|
|
|
1254
1303
|
&& comment.location.start_line >= node.location.start_line
|
|
1255
1304
|
&& comment.location.end_line < node.location.end_line
|
|
1256
1305
|
{
|
|
1257
|
-
self.emitted_comment_indices.
|
|
1306
|
+
self.emitted_comment_indices.insert(idx);
|
|
1258
1307
|
}
|
|
1259
1308
|
}
|
|
1260
1309
|
|
|
@@ -1264,13 +1313,23 @@ impl Emitter {
|
|
|
1264
1313
|
Ok(())
|
|
1265
1314
|
}
|
|
1266
1315
|
|
|
1316
|
+
/// Get cached indent string for a given level
|
|
1317
|
+
fn get_indent(&mut self, level: usize) -> &str {
|
|
1318
|
+
// Extend cache if needed
|
|
1319
|
+
while self.indent_cache.len() <= level {
|
|
1320
|
+
let len = self.indent_cache.len();
|
|
1321
|
+
let indent = match self.config.formatting.indent_style {
|
|
1322
|
+
IndentStyle::Spaces => " ".repeat(self.config.formatting.indent_width * len),
|
|
1323
|
+
IndentStyle::Tabs => "\t".repeat(len),
|
|
1324
|
+
};
|
|
1325
|
+
self.indent_cache.push(indent);
|
|
1326
|
+
}
|
|
1327
|
+
&self.indent_cache[level]
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1267
1330
|
/// Emit indentation
|
|
1268
1331
|
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
|
-
|
|
1332
|
+
let indent_str = self.get_indent(level).to_string();
|
|
1274
1333
|
write!(self.buffer, "{}", indent_str)?;
|
|
1275
1334
|
Ok(())
|
|
1276
1335
|
}
|
|
@@ -1382,6 +1441,148 @@ impl Emitter {
|
|
|
1382
1441
|
Ok(())
|
|
1383
1442
|
}
|
|
1384
1443
|
|
|
1444
|
+
/// Emit singleton class definition (class << self / class << object)
|
|
1445
|
+
fn emit_singleton_class(&mut self, node: &Node, indent_level: usize) -> Result<()> {
|
|
1446
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
1447
|
+
self.emit_indent(indent_level)?;
|
|
1448
|
+
|
|
1449
|
+
write!(self.buffer, "class << ")?;
|
|
1450
|
+
|
|
1451
|
+
// First child is the expression (self or an object)
|
|
1452
|
+
if let Some(expression) = node.children.first() {
|
|
1453
|
+
if !self.source.is_empty() {
|
|
1454
|
+
let start = expression.location.start_offset;
|
|
1455
|
+
let end = expression.location.end_offset;
|
|
1456
|
+
if let Some(text) = self.source.get(start..end) {
|
|
1457
|
+
write!(self.buffer, "{}", text)?;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Emit trailing comments on the class << line
|
|
1463
|
+
self.emit_trailing_comments(node.location.start_line)?;
|
|
1464
|
+
self.buffer.push('\n');
|
|
1465
|
+
|
|
1466
|
+
let class_start_line = node.location.start_line;
|
|
1467
|
+
let class_end_line = node.location.end_line;
|
|
1468
|
+
let mut has_body_content = false;
|
|
1469
|
+
|
|
1470
|
+
// Emit body (skip the first child which is the expression)
|
|
1471
|
+
for (i, child) in node.children.iter().enumerate() {
|
|
1472
|
+
if i == 0 {
|
|
1473
|
+
// Skip the expression (self or object)
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if matches!(child.node_type, NodeType::StatementsNode) {
|
|
1477
|
+
has_body_content = true;
|
|
1478
|
+
self.emit_statements(child, indent_level + 1)?;
|
|
1479
|
+
} else if !self.is_structural_node(&child.node_type) {
|
|
1480
|
+
has_body_content = true;
|
|
1481
|
+
self.emit_node(child, indent_level + 1)?;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Emit comments inside the singleton class body
|
|
1486
|
+
self.emit_comments_in_range(class_start_line + 1, class_end_line, indent_level + 1)?;
|
|
1487
|
+
|
|
1488
|
+
// Add newline before end if there was body content
|
|
1489
|
+
if (has_body_content || self.has_comments_in_range(class_start_line + 1, class_end_line))
|
|
1490
|
+
&& !self.buffer.ends_with('\n')
|
|
1491
|
+
{
|
|
1492
|
+
self.buffer.push('\n');
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
self.emit_indent(indent_level)?;
|
|
1496
|
+
write!(self.buffer, "end")?;
|
|
1497
|
+
self.emit_trailing_comments(node.location.end_line)?;
|
|
1498
|
+
|
|
1499
|
+
Ok(())
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/// Emit case match (Ruby 3.0+ pattern matching with case...in)
|
|
1503
|
+
fn emit_case_match(&mut self, node: &Node, indent_level: usize) -> Result<()> {
|
|
1504
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
1505
|
+
self.emit_indent(indent_level)?;
|
|
1506
|
+
|
|
1507
|
+
// Write "case" keyword
|
|
1508
|
+
write!(self.buffer, "case")?;
|
|
1509
|
+
|
|
1510
|
+
// Find predicate (first child that isn't InNode or ElseNode)
|
|
1511
|
+
let mut in_start_idx = 0;
|
|
1512
|
+
if let Some(first_child) = node.children.first() {
|
|
1513
|
+
if !matches!(first_child.node_type, NodeType::InNode | NodeType::ElseNode) {
|
|
1514
|
+
// This is the predicate - extract from source
|
|
1515
|
+
let start = first_child.location.start_offset;
|
|
1516
|
+
let end = first_child.location.end_offset;
|
|
1517
|
+
if let Some(text) = self.source.get(start..end) {
|
|
1518
|
+
write!(self.buffer, " {}", text)?;
|
|
1519
|
+
}
|
|
1520
|
+
in_start_idx = 1;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
self.buffer.push('\n');
|
|
1525
|
+
|
|
1526
|
+
// Emit in clauses and else
|
|
1527
|
+
for child in node.children.iter().skip(in_start_idx) {
|
|
1528
|
+
match &child.node_type {
|
|
1529
|
+
NodeType::InNode => {
|
|
1530
|
+
self.emit_in(child, indent_level)?;
|
|
1531
|
+
self.buffer.push('\n');
|
|
1532
|
+
}
|
|
1533
|
+
NodeType::ElseNode => {
|
|
1534
|
+
self.emit_indent(indent_level)?;
|
|
1535
|
+
writeln!(self.buffer, "else")?;
|
|
1536
|
+
// Emit else body
|
|
1537
|
+
for else_child in &child.children {
|
|
1538
|
+
if matches!(else_child.node_type, NodeType::StatementsNode) {
|
|
1539
|
+
self.emit_statements(else_child, indent_level + 1)?;
|
|
1540
|
+
} else {
|
|
1541
|
+
self.emit_node(else_child, indent_level + 1)?;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
self.buffer.push('\n');
|
|
1545
|
+
}
|
|
1546
|
+
_ => {}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// Emit "end" keyword
|
|
1551
|
+
self.emit_indent(indent_level)?;
|
|
1552
|
+
write!(self.buffer, "end")?;
|
|
1553
|
+
self.emit_trailing_comments(node.location.end_line)?;
|
|
1554
|
+
|
|
1555
|
+
Ok(())
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/// Emit in node (pattern matching clause)
|
|
1559
|
+
fn emit_in(&mut self, node: &Node, indent_level: usize) -> Result<()> {
|
|
1560
|
+
self.emit_comments_before(node.location.start_line, indent_level)?;
|
|
1561
|
+
self.emit_indent(indent_level)?;
|
|
1562
|
+
|
|
1563
|
+
write!(self.buffer, "in ")?;
|
|
1564
|
+
|
|
1565
|
+
// First child is the pattern
|
|
1566
|
+
if let Some(pattern) = node.children.first() {
|
|
1567
|
+
let start = pattern.location.start_offset;
|
|
1568
|
+
let end = pattern.location.end_offset;
|
|
1569
|
+
if let Some(text) = self.source.get(start..end) {
|
|
1570
|
+
write!(self.buffer, "{}", text)?;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
self.buffer.push('\n');
|
|
1575
|
+
|
|
1576
|
+
// Second child is the statements body
|
|
1577
|
+
if let Some(statements) = node.children.get(1) {
|
|
1578
|
+
if matches!(statements.node_type, NodeType::StatementsNode) {
|
|
1579
|
+
self.emit_statements(statements, indent_level + 1)?;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
Ok(())
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1385
1586
|
/// Check if node is structural (part of definition syntax, not body)
|
|
1386
1587
|
/// These nodes are part of class/module/method definitions and should not be emitted as body
|
|
1387
1588
|
fn is_structural_node(&self, node_type: &NodeType) -> bool {
|
|
@@ -1398,6 +1599,8 @@ impl Emitter {
|
|
|
1398
1599
|
| NodeType::OptionalKeywordParameterNode
|
|
1399
1600
|
| NodeType::KeywordRestParameterNode
|
|
1400
1601
|
| NodeType::BlockParameterNode
|
|
1602
|
+
| NodeType::ForwardingParameterNode
|
|
1603
|
+
| NodeType::NoKeywordsParameterNode
|
|
1401
1604
|
)
|
|
1402
1605
|
}
|
|
1403
1606
|
}
|
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.1
|
|
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-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|