rfmt 1.5.2 → 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 +36 -0
- data/Cargo.lock +266 -92
- 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 -1760
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
//! FormatRule trait and helper functions
|
|
2
|
+
//!
|
|
3
|
+
//! This module defines the core FormatRule trait that all formatting rules implement,
|
|
4
|
+
//! along with shared helper functions for common formatting patterns.
|
|
5
|
+
|
|
6
|
+
use crate::ast::Node;
|
|
7
|
+
use crate::doc::{concat, hardline, leading_comment, trailing_comment, Doc};
|
|
8
|
+
use crate::error::Result;
|
|
9
|
+
|
|
10
|
+
use super::context::FormatContext;
|
|
11
|
+
use super::registry::RuleRegistry;
|
|
12
|
+
|
|
13
|
+
/// Trait for formatting rules.
|
|
14
|
+
///
|
|
15
|
+
/// Each rule handles a specific node type (or set of node types) and produces
|
|
16
|
+
/// a Doc IR representation of that node.
|
|
17
|
+
///
|
|
18
|
+
/// Rules are stateless and can be shared across multiple formatting contexts.
|
|
19
|
+
pub trait FormatRule: Send + Sync {
|
|
20
|
+
/// Formats a node and returns the Doc IR.
|
|
21
|
+
///
|
|
22
|
+
/// # Arguments
|
|
23
|
+
/// * `node` - The AST node to format
|
|
24
|
+
/// * `ctx` - The formatting context with source, config, and comment tracking
|
|
25
|
+
/// * `registry` - The rule registry for recursive formatting
|
|
26
|
+
///
|
|
27
|
+
/// # Returns
|
|
28
|
+
/// A Doc IR representing the formatted node
|
|
29
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Formats a child node by dispatching to the appropriate rule.
|
|
33
|
+
///
|
|
34
|
+
/// This is the primary way to recursively format child nodes within rules.
|
|
35
|
+
pub fn format_child(child: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
36
|
+
let rule = registry.get_rule(&child.node_type);
|
|
37
|
+
rule.format(child, ctx, registry)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Boxed rule type for dynamic dispatch.
|
|
41
|
+
pub type BoxedRule = Box<dyn FormatRule>;
|
|
42
|
+
|
|
43
|
+
/// Lightweight comment reference using index instead of cloning.
|
|
44
|
+
#[derive(Clone, Copy)]
|
|
45
|
+
struct CommentRef {
|
|
46
|
+
idx: usize,
|
|
47
|
+
start_line: usize,
|
|
48
|
+
end_line: usize,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Formats leading comments before a given line.
|
|
52
|
+
///
|
|
53
|
+
/// This helper collects all unemitted comments that appear before the given line,
|
|
54
|
+
/// formats them with hardlines, and marks them as emitted.
|
|
55
|
+
///
|
|
56
|
+
/// # Arguments
|
|
57
|
+
/// * `ctx` - The formatting context
|
|
58
|
+
/// * `line` - The line number to get comments before
|
|
59
|
+
///
|
|
60
|
+
/// # Returns
|
|
61
|
+
/// A Doc containing all leading comments with proper line breaks
|
|
62
|
+
pub fn format_leading_comments(ctx: &mut FormatContext, line: usize) -> Doc {
|
|
63
|
+
// Collect lightweight refs while borrowing immutably
|
|
64
|
+
let comment_refs: Vec<CommentRef> = ctx
|
|
65
|
+
.get_comment_indices_before(line)
|
|
66
|
+
.filter_map(|idx| {
|
|
67
|
+
ctx.get_comment(idx).map(|c| CommentRef {
|
|
68
|
+
idx,
|
|
69
|
+
start_line: c.location.start_line,
|
|
70
|
+
end_line: c.location.end_line,
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
.collect();
|
|
74
|
+
|
|
75
|
+
if comment_refs.is_empty() {
|
|
76
|
+
return Doc::Empty;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(comment_refs.len() * 2);
|
|
80
|
+
let mut last_end_line: Option<usize> = None;
|
|
81
|
+
let mut indices_to_mark: Vec<usize> = Vec::with_capacity(comment_refs.len());
|
|
82
|
+
|
|
83
|
+
for cref in &comment_refs {
|
|
84
|
+
// Preserve blank lines between comments
|
|
85
|
+
if let Some(prev_end) = last_end_line {
|
|
86
|
+
let gap = cref.start_line.saturating_sub(prev_end);
|
|
87
|
+
for _ in 1..gap {
|
|
88
|
+
docs.push(hardline());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if let Some(comment) = ctx.get_comment(cref.idx) {
|
|
93
|
+
docs.push(leading_comment(&comment.text, true));
|
|
94
|
+
}
|
|
95
|
+
last_end_line = Some(cref.end_line);
|
|
96
|
+
indices_to_mark.push(cref.idx);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Mark comments as emitted in batch
|
|
100
|
+
ctx.mark_comments_emitted(indices_to_mark);
|
|
101
|
+
|
|
102
|
+
// Add blank line after comments if there's a gap before the node
|
|
103
|
+
if let Some(last_end) = last_end_line {
|
|
104
|
+
if line > last_end + 1 {
|
|
105
|
+
docs.push(hardline());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
concat(docs)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Formats a trailing comment on the same line.
|
|
113
|
+
///
|
|
114
|
+
/// # Arguments
|
|
115
|
+
/// * `ctx` - The formatting context
|
|
116
|
+
/// * `line` - The line number to get trailing comments for
|
|
117
|
+
///
|
|
118
|
+
/// # Returns
|
|
119
|
+
/// A Doc containing the trailing comment, or Empty if none
|
|
120
|
+
pub fn format_trailing_comment(ctx: &mut FormatContext, line: usize) -> Doc {
|
|
121
|
+
// Collect indices while borrowing immutably
|
|
122
|
+
let indices: Vec<usize> = ctx.get_trailing_comment_indices(line).collect();
|
|
123
|
+
|
|
124
|
+
if indices.is_empty() {
|
|
125
|
+
return Doc::Empty;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(indices.len());
|
|
129
|
+
|
|
130
|
+
for &idx in &indices {
|
|
131
|
+
if let Some(comment) = ctx.get_comment(idx) {
|
|
132
|
+
docs.push(trailing_comment(&comment.text));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Mark comments as emitted in batch
|
|
137
|
+
ctx.mark_comments_emitted(indices);
|
|
138
|
+
|
|
139
|
+
concat(docs)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Formats comments that appear before the `end` keyword of a construct.
|
|
143
|
+
///
|
|
144
|
+
/// This is used for comments inside class/module/def bodies that appear
|
|
145
|
+
/// on standalone lines before the closing `end`.
|
|
146
|
+
///
|
|
147
|
+
/// # Arguments
|
|
148
|
+
/// * `ctx` - The formatting context
|
|
149
|
+
/// * `start_line` - The start line of the construct
|
|
150
|
+
/// * `end_line` - The end line of the construct (where `end` appears)
|
|
151
|
+
///
|
|
152
|
+
/// # Returns
|
|
153
|
+
/// A Doc containing the formatted comments
|
|
154
|
+
pub fn format_comments_before_end(
|
|
155
|
+
ctx: &mut FormatContext,
|
|
156
|
+
start_line: usize,
|
|
157
|
+
end_line: usize,
|
|
158
|
+
) -> Doc {
|
|
159
|
+
// Collect indices for comments in range
|
|
160
|
+
let indices: Vec<usize> = ctx
|
|
161
|
+
.get_comment_indices_in_range(start_line + 1, end_line)
|
|
162
|
+
.collect();
|
|
163
|
+
|
|
164
|
+
if indices.is_empty() {
|
|
165
|
+
return Doc::Empty;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Filter to only standalone comments
|
|
169
|
+
let standalone_refs: Vec<CommentRef> = indices
|
|
170
|
+
.iter()
|
|
171
|
+
.filter_map(|&idx| {
|
|
172
|
+
ctx.get_comment(idx).and_then(|c| {
|
|
173
|
+
if ctx.is_standalone_comment(c) && c.location.end_line < end_line {
|
|
174
|
+
Some(CommentRef {
|
|
175
|
+
idx,
|
|
176
|
+
start_line: c.location.start_line,
|
|
177
|
+
end_line: c.location.end_line,
|
|
178
|
+
})
|
|
179
|
+
} else {
|
|
180
|
+
None
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
.collect();
|
|
185
|
+
|
|
186
|
+
if standalone_refs.is_empty() {
|
|
187
|
+
return Doc::Empty;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let mut docs: Vec<Doc> = vec![hardline()];
|
|
191
|
+
let mut last_end_line: Option<usize> = None;
|
|
192
|
+
let mut indices_to_mark: Vec<usize> = Vec::with_capacity(standalone_refs.len());
|
|
193
|
+
|
|
194
|
+
for cref in &standalone_refs {
|
|
195
|
+
// Preserve blank lines between comments
|
|
196
|
+
if let Some(prev_end) = last_end_line {
|
|
197
|
+
let gap = cref.start_line.saturating_sub(prev_end);
|
|
198
|
+
for _ in 1..gap {
|
|
199
|
+
docs.push(hardline());
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if let Some(comment) = ctx.get_comment(cref.idx) {
|
|
204
|
+
docs.push(leading_comment(&comment.text, true));
|
|
205
|
+
}
|
|
206
|
+
last_end_line = Some(cref.end_line);
|
|
207
|
+
indices_to_mark.push(cref.idx);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Mark comments as emitted in batch
|
|
211
|
+
ctx.mark_comments_emitted(indices_to_mark);
|
|
212
|
+
|
|
213
|
+
concat(docs)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// Formats remaining comments at the end of the file.
|
|
217
|
+
///
|
|
218
|
+
/// This should be called after all nodes have been formatted to emit
|
|
219
|
+
/// any comments that weren't attached to specific nodes.
|
|
220
|
+
///
|
|
221
|
+
/// # Arguments
|
|
222
|
+
/// * `ctx` - The formatting context
|
|
223
|
+
/// * `last_code_line` - The last line of code in the file
|
|
224
|
+
///
|
|
225
|
+
/// # Returns
|
|
226
|
+
/// A Doc containing all remaining comments
|
|
227
|
+
pub fn format_remaining_comments(ctx: &mut FormatContext, last_code_line: usize) -> Doc {
|
|
228
|
+
// Collect remaining comment indices and their line info
|
|
229
|
+
let comment_refs: Vec<CommentRef> = ctx
|
|
230
|
+
.get_remaining_comment_indices()
|
|
231
|
+
.filter_map(|idx| {
|
|
232
|
+
ctx.get_comment(idx).map(|c| CommentRef {
|
|
233
|
+
idx,
|
|
234
|
+
start_line: c.location.start_line,
|
|
235
|
+
end_line: c.location.end_line,
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
.collect();
|
|
239
|
+
|
|
240
|
+
if comment_refs.is_empty() {
|
|
241
|
+
return Doc::Empty;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(comment_refs.len() * 2);
|
|
245
|
+
let mut last_end_line = last_code_line;
|
|
246
|
+
let mut is_first = true;
|
|
247
|
+
let mut indices_to_mark: Vec<usize> = Vec::with_capacity(comment_refs.len());
|
|
248
|
+
|
|
249
|
+
for cref in &comment_refs {
|
|
250
|
+
// Preserve blank lines
|
|
251
|
+
let gap = cref.start_line.saturating_sub(last_end_line);
|
|
252
|
+
|
|
253
|
+
// Only add newlines if not the first comment or if there's a gap
|
|
254
|
+
if !is_first || gap > 0 {
|
|
255
|
+
for _ in 0..gap.max(1) {
|
|
256
|
+
docs.push(hardline());
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if let Some(comment) = ctx.get_comment(cref.idx) {
|
|
261
|
+
docs.push(leading_comment(&comment.text, false));
|
|
262
|
+
}
|
|
263
|
+
last_end_line = cref.end_line;
|
|
264
|
+
is_first = false;
|
|
265
|
+
indices_to_mark.push(cref.idx);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Mark comments as emitted in batch
|
|
269
|
+
ctx.mark_comments_emitted(indices_to_mark);
|
|
270
|
+
|
|
271
|
+
concat(docs)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Formats a statements node as a sequence of children with proper line spacing.
|
|
275
|
+
///
|
|
276
|
+
/// This is a shared helper used by multiple formatting rules (if_unless, case,
|
|
277
|
+
/// begin, call, loops) to format StatementsNode children consistently.
|
|
278
|
+
///
|
|
279
|
+
/// # Arguments
|
|
280
|
+
/// * `node` - The StatementsNode to format
|
|
281
|
+
/// * `ctx` - The formatting context
|
|
282
|
+
/// * `registry` - The rule registry for recursive formatting
|
|
283
|
+
///
|
|
284
|
+
/// # Returns
|
|
285
|
+
/// A Doc containing all statements with proper line breaks between them
|
|
286
|
+
pub fn format_statements(
|
|
287
|
+
node: &Node,
|
|
288
|
+
ctx: &mut FormatContext,
|
|
289
|
+
registry: &RuleRegistry,
|
|
290
|
+
) -> Result<Doc> {
|
|
291
|
+
if node.children.is_empty() {
|
|
292
|
+
return Ok(Doc::Empty);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(node.children.len() * 2);
|
|
296
|
+
|
|
297
|
+
for (i, child) in node.children.iter().enumerate() {
|
|
298
|
+
let child_doc = format_child(child, ctx, registry)?;
|
|
299
|
+
docs.push(child_doc);
|
|
300
|
+
|
|
301
|
+
// Add newlines between statements
|
|
302
|
+
if let Some(next_child) = node.children.get(i + 1) {
|
|
303
|
+
let current_end_line = child.location.end_line;
|
|
304
|
+
let next_start_line = next_child.location.start_line;
|
|
305
|
+
let line_diff = next_start_line.saturating_sub(current_end_line);
|
|
306
|
+
|
|
307
|
+
docs.push(hardline());
|
|
308
|
+
if line_diff > 1 {
|
|
309
|
+
docs.push(hardline());
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Ok(concat(docs))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Reformats multiline method chain text with indented style.
|
|
318
|
+
///
|
|
319
|
+
/// Converts aligned method chains to indented style:
|
|
320
|
+
/// - First line is kept as-is (trimmed at end)
|
|
321
|
+
/// - Subsequent lines starting with `.` or `&.` are re-indented with one level of indentation
|
|
322
|
+
///
|
|
323
|
+
/// Returns `Cow::Borrowed` when no transformation is needed to avoid allocation.
|
|
324
|
+
///
|
|
325
|
+
/// # Arguments
|
|
326
|
+
/// * `source_text` - The source text containing a method chain
|
|
327
|
+
/// * `indent_width` - The number of spaces for one level of indentation
|
|
328
|
+
///
|
|
329
|
+
/// # Example
|
|
330
|
+
/// ```text
|
|
331
|
+
/// Input (indent_width=2): "foo.bar\n .baz"
|
|
332
|
+
/// Output: "foo.bar\n .baz"
|
|
333
|
+
/// ```
|
|
334
|
+
pub fn reformat_chain_lines(source_text: &str, indent_width: usize) -> std::borrow::Cow<'_, str> {
|
|
335
|
+
use std::borrow::Cow;
|
|
336
|
+
|
|
337
|
+
let lines: Vec<&str> = source_text.lines().collect();
|
|
338
|
+
if lines.len() <= 1 {
|
|
339
|
+
return Cow::Borrowed(source_text);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check if there are actual chain continuation lines (. or &.)
|
|
343
|
+
let has_chain = lines[1..].iter().any(|l| {
|
|
344
|
+
let t = l.trim_start();
|
|
345
|
+
t.starts_with('.') || t.starts_with("&.")
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if !has_chain {
|
|
349
|
+
return Cow::Borrowed(source_text);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Build the indented chain with pre-allocated capacity
|
|
353
|
+
let chain_indent = " ".repeat(indent_width);
|
|
354
|
+
let mut result = String::with_capacity(source_text.len());
|
|
355
|
+
result.push_str(lines[0].trim_end());
|
|
356
|
+
|
|
357
|
+
for line in &lines[1..] {
|
|
358
|
+
result.push('\n');
|
|
359
|
+
let trimmed = line.trim();
|
|
360
|
+
if trimmed.starts_with('.') || trimmed.starts_with("&.") {
|
|
361
|
+
result.push_str(&chain_indent);
|
|
362
|
+
result.push_str(trimmed);
|
|
363
|
+
} else {
|
|
364
|
+
// Non-chain continuation (e.g., heredoc content): preserve as-is
|
|
365
|
+
result.push_str(line);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
Cow::Owned(result)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// Marks comments within a line range as emitted.
|
|
373
|
+
///
|
|
374
|
+
/// This is used when source text is extracted directly, as any comments
|
|
375
|
+
/// within the extracted range are included in the output.
|
|
376
|
+
///
|
|
377
|
+
/// # Arguments
|
|
378
|
+
/// * `ctx` - The formatting context
|
|
379
|
+
/// * `start_line` - The start line of the range
|
|
380
|
+
/// * `end_line` - The end line of the range
|
|
381
|
+
pub fn mark_comments_in_range_emitted(ctx: &mut FormatContext, start_line: usize, end_line: usize) {
|
|
382
|
+
let indices: Vec<usize> = ctx
|
|
383
|
+
.get_comment_indices_in_range(start_line, end_line)
|
|
384
|
+
.collect();
|
|
385
|
+
ctx.mark_comments_emitted(indices);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Checks if a node is a structural node (part of definition syntax, not body).
|
|
389
|
+
///
|
|
390
|
+
/// Structural nodes are parts of class/module/method definitions that should
|
|
391
|
+
/// not be emitted as body content (e.g., constant names, parameter nodes).
|
|
392
|
+
pub fn is_structural_node(node: &Node) -> bool {
|
|
393
|
+
use crate::ast::NodeType;
|
|
394
|
+
|
|
395
|
+
matches!(
|
|
396
|
+
node.node_type,
|
|
397
|
+
NodeType::ConstantReadNode
|
|
398
|
+
| NodeType::ConstantWriteNode
|
|
399
|
+
| NodeType::ConstantPathNode
|
|
400
|
+
| NodeType::RequiredParameterNode
|
|
401
|
+
| NodeType::OptionalParameterNode
|
|
402
|
+
| NodeType::RestParameterNode
|
|
403
|
+
| NodeType::KeywordParameterNode
|
|
404
|
+
| NodeType::RequiredKeywordParameterNode
|
|
405
|
+
| NodeType::OptionalKeywordParameterNode
|
|
406
|
+
| NodeType::KeywordRestParameterNode
|
|
407
|
+
| NodeType::BlockParameterNode
|
|
408
|
+
| NodeType::ForwardingParameterNode
|
|
409
|
+
| NodeType::NoKeywordsParameterNode
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[cfg(test)]
|
|
414
|
+
mod tests {
|
|
415
|
+
use super::*;
|
|
416
|
+
use crate::ast::{
|
|
417
|
+
Comment, CommentPosition, CommentType, FormattingInfo, Location, Node, NodeType,
|
|
418
|
+
};
|
|
419
|
+
use crate::config::Config;
|
|
420
|
+
use std::collections::HashMap;
|
|
421
|
+
|
|
422
|
+
fn make_comment(text: &str, line: usize, start_offset: usize) -> Comment {
|
|
423
|
+
Comment {
|
|
424
|
+
text: text.to_string(),
|
|
425
|
+
location: Location::new(
|
|
426
|
+
line,
|
|
427
|
+
0,
|
|
428
|
+
line,
|
|
429
|
+
text.len(),
|
|
430
|
+
start_offset,
|
|
431
|
+
start_offset + text.len(),
|
|
432
|
+
),
|
|
433
|
+
comment_type: CommentType::Line,
|
|
434
|
+
position: CommentPosition::Leading,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
fn make_node_with_comments(comments: Vec<Comment>) -> Node {
|
|
439
|
+
Node {
|
|
440
|
+
node_type: NodeType::ProgramNode,
|
|
441
|
+
location: Location::new(1, 0, 10, 0, 0, 100),
|
|
442
|
+
children: Vec::new(),
|
|
443
|
+
metadata: HashMap::new(),
|
|
444
|
+
comments,
|
|
445
|
+
formatting: FormattingInfo::default(),
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn test_format_leading_comments() {
|
|
451
|
+
let config = Config::default();
|
|
452
|
+
let source = "# comment\nclass Foo\nend";
|
|
453
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
454
|
+
|
|
455
|
+
let comment = make_comment("# comment", 1, 0);
|
|
456
|
+
let node = make_node_with_comments(vec![comment]);
|
|
457
|
+
ctx.collect_comments(&node);
|
|
458
|
+
|
|
459
|
+
let doc = format_leading_comments(&mut ctx, 5);
|
|
460
|
+
assert!(!matches!(doc, Doc::Empty));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#[test]
|
|
464
|
+
fn test_format_trailing_comment() {
|
|
465
|
+
let config = Config::default();
|
|
466
|
+
let source = "code # trailing";
|
|
467
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
468
|
+
|
|
469
|
+
let comment = Comment {
|
|
470
|
+
text: "# trailing".to_string(),
|
|
471
|
+
location: Location::new(1, 5, 1, 15, 5, 15),
|
|
472
|
+
comment_type: CommentType::Line,
|
|
473
|
+
position: CommentPosition::Trailing,
|
|
474
|
+
};
|
|
475
|
+
let node = make_node_with_comments(vec![comment]);
|
|
476
|
+
ctx.collect_comments(&node);
|
|
477
|
+
|
|
478
|
+
let doc = format_trailing_comment(&mut ctx, 1);
|
|
479
|
+
assert!(!matches!(doc, Doc::Empty));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#[test]
|
|
483
|
+
fn test_is_structural_node() {
|
|
484
|
+
let structural_node = Node {
|
|
485
|
+
node_type: NodeType::ConstantReadNode,
|
|
486
|
+
location: Location::new(1, 0, 1, 3, 0, 3),
|
|
487
|
+
children: Vec::new(),
|
|
488
|
+
metadata: HashMap::new(),
|
|
489
|
+
comments: Vec::new(),
|
|
490
|
+
formatting: FormattingInfo::default(),
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
let non_structural_node = Node {
|
|
494
|
+
node_type: NodeType::CallNode,
|
|
495
|
+
location: Location::new(1, 0, 1, 10, 0, 10),
|
|
496
|
+
children: Vec::new(),
|
|
497
|
+
metadata: HashMap::new(),
|
|
498
|
+
comments: Vec::new(),
|
|
499
|
+
formatting: FormattingInfo::default(),
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
assert!(is_structural_node(&structural_node));
|
|
503
|
+
assert!(!is_structural_node(&non_structural_node));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[test]
|
|
507
|
+
fn test_format_child() {
|
|
508
|
+
let config = Config::default();
|
|
509
|
+
let source = "puts 'hello'";
|
|
510
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
511
|
+
let registry = RuleRegistry::default_registry();
|
|
512
|
+
|
|
513
|
+
let node = Node {
|
|
514
|
+
node_type: NodeType::CallNode,
|
|
515
|
+
location: Location::new(1, 0, 1, 12, 0, 12),
|
|
516
|
+
children: Vec::new(),
|
|
517
|
+
metadata: HashMap::new(),
|
|
518
|
+
comments: Vec::new(),
|
|
519
|
+
formatting: FormattingInfo::default(),
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
ctx.collect_comments(&node);
|
|
523
|
+
|
|
524
|
+
let doc = format_child(&node, &mut ctx, ®istry).unwrap();
|
|
525
|
+
assert!(!matches!(doc, Doc::Empty));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[test]
|
|
529
|
+
fn test_reformat_chain_lines_single_line() {
|
|
530
|
+
let input = "foo.bar.baz";
|
|
531
|
+
let result = reformat_chain_lines(input, 2);
|
|
532
|
+
assert_eq!(result, "foo.bar.baz");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#[test]
|
|
536
|
+
fn test_reformat_chain_lines_multiline_chain() {
|
|
537
|
+
let input = "foo.bar\n .baz\n .qux";
|
|
538
|
+
let result = reformat_chain_lines(input, 2);
|
|
539
|
+
assert_eq!(result, "foo.bar\n .baz\n .qux");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#[test]
|
|
543
|
+
fn test_reformat_chain_lines_safe_navigation() {
|
|
544
|
+
let input = "foo&.bar\n &.baz";
|
|
545
|
+
let result = reformat_chain_lines(input, 2);
|
|
546
|
+
assert_eq!(result, "foo&.bar\n &.baz");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn test_reformat_chain_lines_no_chain() {
|
|
551
|
+
let input = "foo(\n arg1,\n arg2\n)";
|
|
552
|
+
let result = reformat_chain_lines(input, 2);
|
|
553
|
+
assert_eq!(result, input);
|
|
554
|
+
}
|
|
555
|
+
}
|