rfmt 1.5.3 → 1.6.0
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 +31 -0
- data/Cargo.lock +1 -1
- data/README.md +22 -18
- data/ext/rfmt/Cargo.toml +4 -1
- data/ext/rfmt/src/doc/builders.rs +528 -0
- data/ext/rfmt/src/doc/mod.rs +220 -0
- data/ext/rfmt/src/doc/printer.rs +684 -0
- data/ext/rfmt/src/format/context.rs +448 -0
- data/ext/rfmt/src/format/formatter.rs +226 -0
- data/ext/rfmt/src/format/mod.rs +35 -0
- data/ext/rfmt/src/format/registry.rs +195 -0
- data/ext/rfmt/src/format/rule.rs +555 -0
- data/ext/rfmt/src/format/rules/begin.rs +295 -0
- data/ext/rfmt/src/format/rules/body_end.rs +109 -0
- data/ext/rfmt/src/format/rules/call.rs +409 -0
- data/ext/rfmt/src/format/rules/case.rs +359 -0
- data/ext/rfmt/src/format/rules/class.rs +160 -0
- data/ext/rfmt/src/format/rules/def.rs +216 -0
- data/ext/rfmt/src/format/rules/fallback.rs +116 -0
- data/ext/rfmt/src/format/rules/if_unless.rs +407 -0
- data/ext/rfmt/src/format/rules/loops.rs +325 -0
- data/ext/rfmt/src/format/rules/mod.rs +31 -0
- data/ext/rfmt/src/format/rules/module.rs +150 -0
- data/ext/rfmt/src/format/rules/singleton_class.rs +202 -0
- data/ext/rfmt/src/format/rules/statements.rs +122 -0
- data/ext/rfmt/src/format/rules/variable_write.rs +296 -0
- data/ext/rfmt/src/lib.rs +8 -5
- data/ext/rfmt/src/parser/prism_adapter.rs +157 -2
- data/lib/rfmt/version.rb +1 -1
- data/lib/ruby_lsp/rfmt/formatter_runner.rb +2 -0
- metadata +23 -2
- data/ext/rfmt/src/emitter/mod.rs +0 -1844
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
//! FormatContext - State management for formatting
|
|
2
|
+
//!
|
|
3
|
+
//! FormatContext encapsulates all state needed during formatting:
|
|
4
|
+
//! - Source code reference
|
|
5
|
+
//! - Configuration
|
|
6
|
+
//! - Comment tracking and emission
|
|
7
|
+
//! - Group ID generation for Doc IR
|
|
8
|
+
|
|
9
|
+
use crate::ast::{Comment, Node};
|
|
10
|
+
use crate::config::Config;
|
|
11
|
+
use std::collections::{BTreeMap, HashSet};
|
|
12
|
+
|
|
13
|
+
/// Formatting context that manages state during AST traversal.
|
|
14
|
+
///
|
|
15
|
+
/// This struct is passed to FormatRules and provides access to:
|
|
16
|
+
/// - Source code for extraction
|
|
17
|
+
/// - Configuration settings
|
|
18
|
+
/// - Comment management (collection, emission tracking)
|
|
19
|
+
/// - Group ID generation for Doc IR
|
|
20
|
+
pub struct FormatContext<'a> {
|
|
21
|
+
/// Reference to the configuration
|
|
22
|
+
config: &'a Config,
|
|
23
|
+
|
|
24
|
+
/// Reference to the source code
|
|
25
|
+
source: &'a str,
|
|
26
|
+
|
|
27
|
+
/// Source lines cached for efficient access
|
|
28
|
+
source_lines: Vec<&'a str>,
|
|
29
|
+
|
|
30
|
+
/// All comments collected from the AST
|
|
31
|
+
all_comments: Vec<Comment>,
|
|
32
|
+
|
|
33
|
+
/// Indices of comments that have been emitted
|
|
34
|
+
emitted_comment_indices: HashSet<usize>,
|
|
35
|
+
|
|
36
|
+
/// Index of comment indices by start line for O(log n) lookup
|
|
37
|
+
/// Key: start_line, Value: Vec of comment indices that start on that line
|
|
38
|
+
comments_by_line: BTreeMap<usize, Vec<usize>>,
|
|
39
|
+
|
|
40
|
+
/// Counter for generating unique group IDs
|
|
41
|
+
next_group_id: u32,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl<'a> FormatContext<'a> {
|
|
45
|
+
/// Creates a new FormatContext with the given configuration and source code.
|
|
46
|
+
pub fn new(config: &'a Config, source: &'a str) -> Self {
|
|
47
|
+
Self {
|
|
48
|
+
config,
|
|
49
|
+
source,
|
|
50
|
+
source_lines: source.lines().collect(),
|
|
51
|
+
all_comments: Vec::new(),
|
|
52
|
+
emitted_comment_indices: HashSet::new(),
|
|
53
|
+
comments_by_line: BTreeMap::new(),
|
|
54
|
+
next_group_id: 0,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Returns a reference to the configuration.
|
|
59
|
+
pub fn config(&self) -> &Config {
|
|
60
|
+
self.config
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Returns a reference to the source code.
|
|
64
|
+
pub fn source(&self) -> &str {
|
|
65
|
+
self.source
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Generates a new unique group ID for Doc IR.
|
|
69
|
+
pub fn next_group_id(&mut self) -> u32 {
|
|
70
|
+
let id = self.next_group_id;
|
|
71
|
+
self.next_group_id += 1;
|
|
72
|
+
id
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Collects all comments from the AST recursively.
|
|
76
|
+
pub fn collect_comments(&mut self, root: &Node) {
|
|
77
|
+
self.all_comments.clear();
|
|
78
|
+
self.emitted_comment_indices.clear();
|
|
79
|
+
self.comments_by_line.clear();
|
|
80
|
+
|
|
81
|
+
// Use iterative approach with stack to avoid deep recursion
|
|
82
|
+
let mut stack = vec![root];
|
|
83
|
+
while let Some(node) = stack.pop() {
|
|
84
|
+
// Reserve capacity hint based on typical comment count
|
|
85
|
+
if self.all_comments.is_empty() && !node.comments.is_empty() {
|
|
86
|
+
self.all_comments.reserve(node.comments.len() * 4);
|
|
87
|
+
}
|
|
88
|
+
self.all_comments.extend(node.comments.iter().cloned());
|
|
89
|
+
// Process children in reverse order to maintain order when popping
|
|
90
|
+
stack.extend(node.children.iter().rev());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
self.build_comment_index();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Builds the comment index by start line for O(log n) range lookups.
|
|
97
|
+
fn build_comment_index(&mut self) {
|
|
98
|
+
for (idx, comment) in self.all_comments.iter().enumerate() {
|
|
99
|
+
self.comments_by_line
|
|
100
|
+
.entry(comment.location.start_line)
|
|
101
|
+
.or_default()
|
|
102
|
+
.push(idx);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Gets comments that appear before a given line (not emitted yet).
|
|
107
|
+
///
|
|
108
|
+
/// Returns comments where the entire comment ends before the given line.
|
|
109
|
+
pub fn get_comments_before(&self, line: usize) -> Vec<&Comment> {
|
|
110
|
+
self.comments_by_line
|
|
111
|
+
.range(..line)
|
|
112
|
+
.flat_map(|(_, indices)| indices.iter())
|
|
113
|
+
.filter(|&&idx| {
|
|
114
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
115
|
+
&& self.all_comments[idx].location.end_line < line
|
|
116
|
+
})
|
|
117
|
+
.map(|&idx| &self.all_comments[idx])
|
|
118
|
+
.collect()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Gets trailing comments on a specific line (not emitted yet).
|
|
122
|
+
///
|
|
123
|
+
/// Trailing comments are comments on the same line as code.
|
|
124
|
+
pub fn get_trailing_comments(&self, line: usize) -> Vec<&Comment> {
|
|
125
|
+
self.comments_by_line
|
|
126
|
+
.get(&line)
|
|
127
|
+
.map(|indices| {
|
|
128
|
+
indices
|
|
129
|
+
.iter()
|
|
130
|
+
.filter(|&&idx| !self.emitted_comment_indices.contains(&idx))
|
|
131
|
+
.map(|&idx| &self.all_comments[idx])
|
|
132
|
+
.collect()
|
|
133
|
+
})
|
|
134
|
+
.unwrap_or_default()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Gets comments within a given line range [start_line, end_line).
|
|
138
|
+
///
|
|
139
|
+
/// Only returns comments that haven't been emitted yet.
|
|
140
|
+
pub fn get_comments_in_range(&self, start_line: usize, end_line: usize) -> Vec<&Comment> {
|
|
141
|
+
if start_line >= end_line {
|
|
142
|
+
return Vec::new();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
self.comments_by_line
|
|
146
|
+
.range(start_line..end_line)
|
|
147
|
+
.flat_map(|(_, indices)| indices.iter())
|
|
148
|
+
.filter(|&&idx| {
|
|
149
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
150
|
+
&& self.all_comments[idx].location.end_line < end_line
|
|
151
|
+
})
|
|
152
|
+
.map(|&idx| &self.all_comments[idx])
|
|
153
|
+
.collect()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Checks if there are any unemitted comments in the given line range.
|
|
157
|
+
pub fn has_comments_in_range(&self, start_line: usize, end_line: usize) -> bool {
|
|
158
|
+
if start_line >= end_line {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
self.comments_by_line
|
|
163
|
+
.range(start_line..end_line)
|
|
164
|
+
.flat_map(|(_, indices)| indices.iter())
|
|
165
|
+
.any(|&idx| {
|
|
166
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
167
|
+
&& self.all_comments[idx].location.end_line < end_line
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Marks a comment as emitted by finding it in the collection.
|
|
172
|
+
///
|
|
173
|
+
/// Uses the line index for O(log n) lookup instead of linear search.
|
|
174
|
+
pub fn mark_comment_emitted(&mut self, comment: &Comment) {
|
|
175
|
+
if let Some(indices) = self.comments_by_line.get(&comment.location.start_line) {
|
|
176
|
+
for &idx in indices {
|
|
177
|
+
let c = &self.all_comments[idx];
|
|
178
|
+
if c.location == comment.location && c.text == comment.text {
|
|
179
|
+
self.emitted_comment_indices.insert(idx);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Marks a comment at the given index as emitted.
|
|
187
|
+
#[inline]
|
|
188
|
+
pub fn mark_comment_emitted_by_index(&mut self, idx: usize) {
|
|
189
|
+
self.emitted_comment_indices.insert(idx);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Marks multiple comments as emitted by their indices.
|
|
193
|
+
///
|
|
194
|
+
/// More efficient than calling mark_comment_emitted_by_index repeatedly.
|
|
195
|
+
pub fn mark_comments_emitted(&mut self, indices: impl IntoIterator<Item = usize>) {
|
|
196
|
+
self.emitted_comment_indices.extend(indices);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Extracts source text for a node.
|
|
200
|
+
pub fn extract_source(&self, node: &Node) -> Option<&str> {
|
|
201
|
+
self.source
|
|
202
|
+
.get(node.location.start_offset..node.location.end_offset)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Extracts source text for a range of offsets.
|
|
206
|
+
pub fn extract_source_range(&self, start: usize, end: usize) -> Option<&str> {
|
|
207
|
+
self.source.get(start..end)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Checks if a comment is standalone (on its own line).
|
|
211
|
+
///
|
|
212
|
+
/// A standalone comment has only whitespace before it on the same line.
|
|
213
|
+
pub fn is_standalone_comment(&self, comment: &Comment) -> bool {
|
|
214
|
+
let comment_line = comment.location.start_line;
|
|
215
|
+
|
|
216
|
+
if comment_line == 0 || comment_line > self.source_lines.len() {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let line = self.source_lines[comment_line - 1]; // Convert to 0-indexed
|
|
221
|
+
|
|
222
|
+
if let Some(hash_pos) = line.find('#') {
|
|
223
|
+
let before_comment = &line[..hash_pos];
|
|
224
|
+
let is_only_whitespace = before_comment.bytes().all(|b| b == b' ' || b == b'\t');
|
|
225
|
+
|
|
226
|
+
let line_comment_text = &line[hash_pos..];
|
|
227
|
+
let is_same_comment = line_comment_text.trim_end() == comment.text.trim_end();
|
|
228
|
+
|
|
229
|
+
return is_only_whitespace && is_same_comment;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Gets all remaining unemitted comments.
|
|
236
|
+
///
|
|
237
|
+
/// Used for emitting comments at the end of the file.
|
|
238
|
+
pub fn get_remaining_comments(&self) -> Vec<&Comment> {
|
|
239
|
+
self.all_comments
|
|
240
|
+
.iter()
|
|
241
|
+
.enumerate()
|
|
242
|
+
.filter(|(idx, _)| !self.emitted_comment_indices.contains(idx))
|
|
243
|
+
.map(|(_, comment)| comment)
|
|
244
|
+
.collect()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Gets comment indices before a given line (not emitted yet).
|
|
248
|
+
///
|
|
249
|
+
/// Returns indices that can be used with `get_comment` and `mark_comment_emitted_by_index`.
|
|
250
|
+
/// This avoids allocating comment data when only indices are needed.
|
|
251
|
+
pub fn get_comment_indices_before(&self, line: usize) -> impl Iterator<Item = usize> + '_ {
|
|
252
|
+
self.comments_by_line
|
|
253
|
+
.range(..line)
|
|
254
|
+
.flat_map(|(_, indices)| indices.iter().copied())
|
|
255
|
+
.filter(move |&idx| {
|
|
256
|
+
!self.emitted_comment_indices.contains(&idx)
|
|
257
|
+
&& self.all_comments[idx].location.end_line < line
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Gets trailing comment indices on a specific line (not emitted yet).
|
|
262
|
+
pub fn get_trailing_comment_indices(&self, line: usize) -> impl Iterator<Item = usize> + '_ {
|
|
263
|
+
self.comments_by_line
|
|
264
|
+
.get(&line)
|
|
265
|
+
.into_iter()
|
|
266
|
+
.flat_map(|indices| indices.iter().copied())
|
|
267
|
+
.filter(move |&idx| !self.emitted_comment_indices.contains(&idx))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Gets comment indices within a given line range [start_line, end_line).
|
|
271
|
+
///
|
|
272
|
+
/// Returns an empty iterator if start_line >= end_line.
|
|
273
|
+
/// This is consistent with `get_comments_in_range` and `has_comments_in_range`.
|
|
274
|
+
pub fn get_comment_indices_in_range(
|
|
275
|
+
&self,
|
|
276
|
+
start_line: usize,
|
|
277
|
+
end_line: usize,
|
|
278
|
+
) -> impl Iterator<Item = usize> + '_ {
|
|
279
|
+
// Return empty iterator for invalid range (consistent with get_comments_in_range)
|
|
280
|
+
let valid_range = start_line < end_line;
|
|
281
|
+
|
|
282
|
+
self.comments_by_line
|
|
283
|
+
.range(start_line..end_line.max(start_line))
|
|
284
|
+
.flat_map(|(_, indices)| indices.iter().copied())
|
|
285
|
+
.filter(move |&idx| {
|
|
286
|
+
valid_range
|
|
287
|
+
&& !self.emitted_comment_indices.contains(&idx)
|
|
288
|
+
&& self.all_comments[idx].location.end_line < end_line
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// Gets remaining comment indices (not emitted yet).
|
|
293
|
+
pub fn get_remaining_comment_indices(&self) -> impl Iterator<Item = usize> + '_ {
|
|
294
|
+
(0..self.all_comments.len()).filter(|idx| !self.emitted_comment_indices.contains(idx))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Gets a comment by index.
|
|
298
|
+
#[inline]
|
|
299
|
+
pub fn get_comment(&self, idx: usize) -> Option<&Comment> {
|
|
300
|
+
self.all_comments.get(idx)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// Gets the last line of code in the AST (excluding comments).
|
|
304
|
+
pub fn find_last_code_line(ast: &Node) -> usize {
|
|
305
|
+
let mut max_line = ast.location.end_line;
|
|
306
|
+
let mut stack = vec![ast];
|
|
307
|
+
|
|
308
|
+
while let Some(node) = stack.pop() {
|
|
309
|
+
max_line = max_line.max(node.location.end_line);
|
|
310
|
+
stack.extend(node.children.iter());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
max_line
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[cfg(test)]
|
|
318
|
+
mod tests {
|
|
319
|
+
use super::*;
|
|
320
|
+
use crate::ast::{CommentPosition, CommentType, FormattingInfo, Location, NodeType};
|
|
321
|
+
use std::collections::HashMap;
|
|
322
|
+
|
|
323
|
+
fn make_comment(text: &str, start_line: usize) -> Comment {
|
|
324
|
+
Comment {
|
|
325
|
+
text: text.to_string(),
|
|
326
|
+
location: Location::new(start_line, 0, start_line, text.len(), 0, text.len()),
|
|
327
|
+
comment_type: CommentType::Line,
|
|
328
|
+
position: CommentPosition::Leading,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
fn make_node_with_comments(comments: Vec<Comment>) -> Node {
|
|
333
|
+
Node {
|
|
334
|
+
node_type: NodeType::ProgramNode,
|
|
335
|
+
location: Location::new(1, 0, 10, 0, 0, 100),
|
|
336
|
+
children: Vec::new(),
|
|
337
|
+
metadata: HashMap::new(),
|
|
338
|
+
comments,
|
|
339
|
+
formatting: FormattingInfo::default(),
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
fn test_collect_comments() {
|
|
345
|
+
let config = Config::default();
|
|
346
|
+
let source = "# comment\nclass Foo\nend";
|
|
347
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
348
|
+
|
|
349
|
+
let comment = make_comment("# comment", 1);
|
|
350
|
+
let node = make_node_with_comments(vec![comment]);
|
|
351
|
+
|
|
352
|
+
ctx.collect_comments(&node);
|
|
353
|
+
|
|
354
|
+
let comments = ctx.get_comments_before(10);
|
|
355
|
+
assert_eq!(comments.len(), 1);
|
|
356
|
+
assert_eq!(comments[0].text, "# comment");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#[test]
|
|
360
|
+
fn test_mark_comment_emitted() {
|
|
361
|
+
let config = Config::default();
|
|
362
|
+
let source = "# comment\ncode";
|
|
363
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
364
|
+
|
|
365
|
+
let comment = make_comment("# comment", 1);
|
|
366
|
+
let node = make_node_with_comments(vec![comment.clone()]);
|
|
367
|
+
|
|
368
|
+
ctx.collect_comments(&node);
|
|
369
|
+
|
|
370
|
+
// Before marking
|
|
371
|
+
assert_eq!(ctx.get_comments_before(10).len(), 1);
|
|
372
|
+
|
|
373
|
+
// Mark as emitted
|
|
374
|
+
ctx.mark_comment_emitted(&comment);
|
|
375
|
+
|
|
376
|
+
// After marking
|
|
377
|
+
assert_eq!(ctx.get_comments_before(10).len(), 0);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[test]
|
|
381
|
+
fn test_get_comments_in_range() {
|
|
382
|
+
let config = Config::default();
|
|
383
|
+
let source = "# comment\ncode";
|
|
384
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
385
|
+
|
|
386
|
+
let comment1 = make_comment("# comment 1", 2);
|
|
387
|
+
let comment2 = make_comment("# comment 2", 5);
|
|
388
|
+
let comment3 = make_comment("# comment 3", 8);
|
|
389
|
+
let node = make_node_with_comments(vec![comment1, comment2, comment3]);
|
|
390
|
+
|
|
391
|
+
ctx.collect_comments(&node);
|
|
392
|
+
|
|
393
|
+
let comments = ctx.get_comments_in_range(3, 7);
|
|
394
|
+
assert_eq!(comments.len(), 1);
|
|
395
|
+
assert_eq!(comments[0].text, "# comment 2");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#[test]
|
|
399
|
+
fn test_trailing_comments() {
|
|
400
|
+
let config = Config::default();
|
|
401
|
+
let source = "code # trailing";
|
|
402
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
403
|
+
|
|
404
|
+
let comment = Comment {
|
|
405
|
+
text: "# trailing".to_string(),
|
|
406
|
+
location: Location::new(1, 5, 1, 15, 5, 15),
|
|
407
|
+
comment_type: CommentType::Line,
|
|
408
|
+
position: CommentPosition::Trailing,
|
|
409
|
+
};
|
|
410
|
+
let node = make_node_with_comments(vec![comment]);
|
|
411
|
+
|
|
412
|
+
ctx.collect_comments(&node);
|
|
413
|
+
|
|
414
|
+
let trailing = ctx.get_trailing_comments(1);
|
|
415
|
+
assert_eq!(trailing.len(), 1);
|
|
416
|
+
assert_eq!(trailing[0].text, "# trailing");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn test_next_group_id() {
|
|
421
|
+
let config = Config::default();
|
|
422
|
+
let source = "";
|
|
423
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
424
|
+
|
|
425
|
+
assert_eq!(ctx.next_group_id(), 0);
|
|
426
|
+
assert_eq!(ctx.next_group_id(), 1);
|
|
427
|
+
assert_eq!(ctx.next_group_id(), 2);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#[test]
|
|
431
|
+
fn test_extract_source() {
|
|
432
|
+
let config = Config::default();
|
|
433
|
+
let source = "class Foo\nend";
|
|
434
|
+
let ctx = FormatContext::new(&config, source);
|
|
435
|
+
|
|
436
|
+
let node = Node {
|
|
437
|
+
node_type: NodeType::ClassNode,
|
|
438
|
+
location: Location::new(1, 0, 2, 3, 0, 13),
|
|
439
|
+
children: Vec::new(),
|
|
440
|
+
metadata: HashMap::new(),
|
|
441
|
+
comments: Vec::new(),
|
|
442
|
+
formatting: FormattingInfo::default(),
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
let extracted = ctx.extract_source(&node);
|
|
446
|
+
assert_eq!(extracted, Some("class Foo\nend"));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//! Formatter - Main entry point for the rule-based formatting system
|
|
2
|
+
//!
|
|
3
|
+
//! The Formatter coordinates the formatting process:
|
|
4
|
+
//! 1. Initialize FormatContext with source and config
|
|
5
|
+
//! 2. Collect comments from AST
|
|
6
|
+
//! 3. Apply rules to generate Doc IR
|
|
7
|
+
//! 4. Print Doc IR to string using Printer
|
|
8
|
+
|
|
9
|
+
use crate::ast::{Node, NodeType};
|
|
10
|
+
use crate::config::Config;
|
|
11
|
+
use crate::doc::{concat, hardline, Doc, Printer};
|
|
12
|
+
use crate::error::Result;
|
|
13
|
+
|
|
14
|
+
use super::context::FormatContext;
|
|
15
|
+
use super::registry::RuleRegistry;
|
|
16
|
+
use super::rule::format_remaining_comments;
|
|
17
|
+
|
|
18
|
+
/// Main formatter that coordinates the formatting process.
|
|
19
|
+
///
|
|
20
|
+
/// The formatter uses a rule-based architecture where each node type
|
|
21
|
+
/// can have a specific formatting rule. Unhandled node types fall back
|
|
22
|
+
/// to source extraction.
|
|
23
|
+
pub struct Formatter {
|
|
24
|
+
/// Configuration for formatting
|
|
25
|
+
config: Config,
|
|
26
|
+
/// Registry of formatting rules
|
|
27
|
+
registry: RuleRegistry,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Formatter {
|
|
31
|
+
/// Creates a new formatter with the given configuration.
|
|
32
|
+
pub fn new(config: Config) -> Self {
|
|
33
|
+
Self {
|
|
34
|
+
config,
|
|
35
|
+
registry: RuleRegistry::default_registry(),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Creates a new formatter with a custom registry.
|
|
40
|
+
pub fn with_registry(config: Config, registry: RuleRegistry) -> Self {
|
|
41
|
+
Self { config, registry }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Formats Ruby source code.
|
|
45
|
+
///
|
|
46
|
+
/// # Arguments
|
|
47
|
+
/// * `source` - The original Ruby source code
|
|
48
|
+
/// * `ast` - The parsed AST root node
|
|
49
|
+
///
|
|
50
|
+
/// # Returns
|
|
51
|
+
/// The formatted source code as a string
|
|
52
|
+
pub fn format(&self, source: &str, ast: &Node) -> Result<String> {
|
|
53
|
+
// 1. Initialize context
|
|
54
|
+
let mut ctx = FormatContext::new(&self.config, source);
|
|
55
|
+
|
|
56
|
+
// 2. Collect comments from AST
|
|
57
|
+
ctx.collect_comments(ast);
|
|
58
|
+
|
|
59
|
+
// 3. Generate Doc IR
|
|
60
|
+
let doc = self.format_node(ast, &mut ctx)?;
|
|
61
|
+
|
|
62
|
+
// 4. Handle remaining comments
|
|
63
|
+
let last_code_line = FormatContext::find_last_code_line(ast);
|
|
64
|
+
let remaining = format_remaining_comments(&mut ctx, last_code_line);
|
|
65
|
+
|
|
66
|
+
let final_doc = if remaining.is_empty() {
|
|
67
|
+
doc
|
|
68
|
+
} else {
|
|
69
|
+
concat(vec![doc, remaining])
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// 5. Print to string
|
|
73
|
+
let mut printer = Printer::new(&self.config);
|
|
74
|
+
let result = printer.print(&final_doc);
|
|
75
|
+
|
|
76
|
+
Ok(result)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Formats a single node.
|
|
80
|
+
pub fn format_node(&self, node: &Node, ctx: &mut FormatContext) -> Result<Doc> {
|
|
81
|
+
match &node.node_type {
|
|
82
|
+
NodeType::ProgramNode => self.format_program(node, ctx),
|
|
83
|
+
NodeType::StatementsNode => self.format_statements(node, ctx),
|
|
84
|
+
_ => {
|
|
85
|
+
// Use the rule registry for specific node types
|
|
86
|
+
let rule = self.registry.get_rule(&node.node_type);
|
|
87
|
+
rule.format(node, ctx, &self.registry)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Returns a reference to the registry for recursive formatting.
|
|
93
|
+
pub fn registry(&self) -> &RuleRegistry {
|
|
94
|
+
&self.registry
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Formats the program node (root).
|
|
98
|
+
fn format_program(&self, node: &Node, ctx: &mut FormatContext) -> Result<Doc> {
|
|
99
|
+
self.format_children_with_spacing(&node.children, ctx)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Formats a statements node (body of class/module/def).
|
|
103
|
+
fn format_statements(&self, node: &Node, ctx: &mut FormatContext) -> Result<Doc> {
|
|
104
|
+
self.format_children_with_spacing(&node.children, ctx)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Format a sequence of child nodes with appropriate line breaks.
|
|
108
|
+
fn format_children_with_spacing(
|
|
109
|
+
&self,
|
|
110
|
+
children: &[Node],
|
|
111
|
+
ctx: &mut FormatContext,
|
|
112
|
+
) -> Result<Doc> {
|
|
113
|
+
if children.is_empty() {
|
|
114
|
+
return Ok(Doc::Empty);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(children.len() * 2);
|
|
118
|
+
|
|
119
|
+
for (i, child) in children.iter().enumerate() {
|
|
120
|
+
let child_doc = self.format_node(child, ctx)?;
|
|
121
|
+
docs.push(child_doc);
|
|
122
|
+
|
|
123
|
+
// Add newlines between statements
|
|
124
|
+
if let Some(next_child) = children.get(i + 1) {
|
|
125
|
+
let current_end_line = child.location.end_line;
|
|
126
|
+
let next_start_line = next_child.location.start_line;
|
|
127
|
+
let line_diff = next_start_line.saturating_sub(current_end_line);
|
|
128
|
+
|
|
129
|
+
// Add 1 hardline if consecutive, 2 hardlines (1 blank line) if there was a gap
|
|
130
|
+
docs.push(hardline());
|
|
131
|
+
if line_diff > 1 {
|
|
132
|
+
docs.push(hardline());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Ok(concat(docs))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
impl Default for Formatter {
|
|
142
|
+
fn default() -> Self {
|
|
143
|
+
Self::new(Config::default())
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[cfg(test)]
|
|
148
|
+
mod tests {
|
|
149
|
+
use super::*;
|
|
150
|
+
use crate::ast::{FormattingInfo, Location};
|
|
151
|
+
use std::collections::HashMap;
|
|
152
|
+
|
|
153
|
+
fn make_program_node(children: Vec<Node>, end_line: usize) -> Node {
|
|
154
|
+
Node {
|
|
155
|
+
node_type: NodeType::ProgramNode,
|
|
156
|
+
location: Location::new(1, 0, end_line, 0, 0, 100),
|
|
157
|
+
children,
|
|
158
|
+
metadata: HashMap::new(),
|
|
159
|
+
comments: Vec::new(),
|
|
160
|
+
formatting: FormattingInfo::default(),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn make_class_node(
|
|
165
|
+
name: &str,
|
|
166
|
+
start_line: usize,
|
|
167
|
+
end_line: usize,
|
|
168
|
+
start_offset: usize,
|
|
169
|
+
end_offset: usize,
|
|
170
|
+
) -> Node {
|
|
171
|
+
let mut metadata = HashMap::new();
|
|
172
|
+
metadata.insert("name".to_string(), name.to_string());
|
|
173
|
+
|
|
174
|
+
Node {
|
|
175
|
+
node_type: NodeType::ClassNode,
|
|
176
|
+
location: Location::new(start_line, 0, end_line, 3, start_offset, end_offset),
|
|
177
|
+
children: Vec::new(),
|
|
178
|
+
metadata,
|
|
179
|
+
comments: Vec::new(),
|
|
180
|
+
formatting: FormattingInfo::default(),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[test]
|
|
185
|
+
fn test_format_simple_class() {
|
|
186
|
+
let source = "class Foo\nend";
|
|
187
|
+
let class_node = make_class_node("Foo", 1, 2, 0, 13);
|
|
188
|
+
let ast = make_program_node(vec![class_node], 2);
|
|
189
|
+
|
|
190
|
+
let formatter = Formatter::default();
|
|
191
|
+
let result = formatter.format(source, &ast).unwrap();
|
|
192
|
+
|
|
193
|
+
assert_eq!(result, "class Foo\nend\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn test_format_multiple_classes() {
|
|
198
|
+
let source = "class Foo\nend\n\nclass Bar\nend";
|
|
199
|
+
let class1 = make_class_node("Foo", 1, 2, 0, 13);
|
|
200
|
+
let class2 = make_class_node("Bar", 4, 5, 15, 28);
|
|
201
|
+
let ast = make_program_node(vec![class1, class2], 5);
|
|
202
|
+
|
|
203
|
+
let formatter = Formatter::default();
|
|
204
|
+
let result = formatter.format(source, &ast).unwrap();
|
|
205
|
+
|
|
206
|
+
// Should preserve blank line between classes
|
|
207
|
+
assert!(result.contains("class Foo\nend"));
|
|
208
|
+
assert!(result.contains("class Bar\nend"));
|
|
209
|
+
assert!(result.contains("\n\n")); // blank line preserved
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[test]
|
|
213
|
+
fn test_formatter_with_custom_config() {
|
|
214
|
+
let mut config = Config::default();
|
|
215
|
+
config.formatting.indent_width = 4;
|
|
216
|
+
|
|
217
|
+
let source = "class Foo\nend";
|
|
218
|
+
let class_node = make_class_node("Foo", 1, 2, 0, 13);
|
|
219
|
+
let ast = make_program_node(vec![class_node], 2);
|
|
220
|
+
|
|
221
|
+
let formatter = Formatter::new(config);
|
|
222
|
+
let result = formatter.format(source, &ast).unwrap();
|
|
223
|
+
|
|
224
|
+
assert_eq!(result, "class Foo\nend\n");
|
|
225
|
+
}
|
|
226
|
+
}
|