rfmt 1.5.3 → 1.6.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 +51 -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 +721 -0
- data/ext/rfmt/src/format/context.rs +448 -0
- data/ext/rfmt/src/format/formatter.rs +250 -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 +726 -0
- data/ext/rfmt/src/format/rules/begin.rs +434 -0
- data/ext/rfmt/src/format/rules/body_end.rs +233 -0
- data/ext/rfmt/src/format/rules/call.rs +448 -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 +130 -0
- data/ext/rfmt/src/format/rules/if_unless.rs +454 -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 +314 -0
- data/ext/rfmt/src/lib.rs +8 -5
- data/ext/rfmt/src/parser/prism_adapter.rs +157 -2
- data/lib/rfmt/prism_bridge.rb +43 -12
- 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,325 @@
|
|
|
1
|
+
//! Loop rules - Formats Ruby while/until/for loops
|
|
2
|
+
//!
|
|
3
|
+
//! Handles:
|
|
4
|
+
//! - while loops: `while cond ... end`
|
|
5
|
+
//! - until loops: `until cond ... end`
|
|
6
|
+
//! - for loops: `for x in collection ... end`
|
|
7
|
+
//! - Postfix forms: `expr while/until cond`
|
|
8
|
+
|
|
9
|
+
use crate::ast::{Node, NodeType};
|
|
10
|
+
use crate::doc::{concat, hardline, indent, text, Doc};
|
|
11
|
+
use crate::error::Result;
|
|
12
|
+
use crate::format::context::FormatContext;
|
|
13
|
+
use crate::format::registry::RuleRegistry;
|
|
14
|
+
use crate::format::rule::{
|
|
15
|
+
format_leading_comments, format_statements, format_trailing_comment, FormatRule,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/// Rule for formatting while loops.
|
|
19
|
+
pub struct WhileRule;
|
|
20
|
+
|
|
21
|
+
/// Rule for formatting until loops.
|
|
22
|
+
pub struct UntilRule;
|
|
23
|
+
|
|
24
|
+
/// Rule for formatting for loops.
|
|
25
|
+
pub struct ForRule;
|
|
26
|
+
|
|
27
|
+
impl FormatRule for WhileRule {
|
|
28
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
29
|
+
format_while_until(node, ctx, registry, "while")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl FormatRule for UntilRule {
|
|
34
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
35
|
+
format_while_until(node, ctx, registry, "until")
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl FormatRule for ForRule {
|
|
40
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
41
|
+
format_for(node, ctx, registry)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Formats while/until loop
|
|
46
|
+
fn format_while_until(
|
|
47
|
+
node: &Node,
|
|
48
|
+
ctx: &mut FormatContext,
|
|
49
|
+
registry: &RuleRegistry,
|
|
50
|
+
keyword: &str,
|
|
51
|
+
) -> Result<Doc> {
|
|
52
|
+
// Check if this is a postfix while/until (modifier form)
|
|
53
|
+
// In postfix form: "statement while/until condition"
|
|
54
|
+
let is_postfix = if node.children.len() >= 2 {
|
|
55
|
+
let predicate = &node.children[0];
|
|
56
|
+
let body = &node.children[1];
|
|
57
|
+
body.location.start_offset < predicate.location.start_offset
|
|
58
|
+
} else {
|
|
59
|
+
false
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if is_postfix {
|
|
63
|
+
return format_postfix_while_until(node, ctx, keyword);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Normal while/until with do...end
|
|
67
|
+
format_normal_while_until(node, ctx, registry, keyword)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Formats postfix while/until
|
|
71
|
+
fn format_postfix_while_until(node: &Node, ctx: &mut FormatContext, keyword: &str) -> Result<Doc> {
|
|
72
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(6);
|
|
73
|
+
|
|
74
|
+
// Leading comments
|
|
75
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
76
|
+
if !leading.is_empty() {
|
|
77
|
+
docs.push(leading);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract from source for postfix form
|
|
81
|
+
if let Some(source_text) = ctx.extract_source(node) {
|
|
82
|
+
docs.push(text(source_text));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Trailing comment
|
|
86
|
+
let trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
87
|
+
if !trailing.is_empty() {
|
|
88
|
+
docs.push(trailing);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let _ = keyword; // Used for potential future formatting
|
|
92
|
+
|
|
93
|
+
Ok(concat(docs))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Formats normal while/until with do...end
|
|
97
|
+
fn format_normal_while_until(
|
|
98
|
+
node: &Node,
|
|
99
|
+
ctx: &mut FormatContext,
|
|
100
|
+
registry: &RuleRegistry,
|
|
101
|
+
keyword: &str,
|
|
102
|
+
) -> Result<Doc> {
|
|
103
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(8);
|
|
104
|
+
|
|
105
|
+
// Leading comments
|
|
106
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
107
|
+
if !leading.is_empty() {
|
|
108
|
+
docs.push(leading);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
docs.push(text(keyword));
|
|
112
|
+
docs.push(text(" "));
|
|
113
|
+
|
|
114
|
+
// Emit predicate (condition) - first child
|
|
115
|
+
if let Some(predicate) = node.children.first() {
|
|
116
|
+
if let Some(source_text) = ctx.extract_source(predicate) {
|
|
117
|
+
docs.push(text(source_text));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Trailing comment on same line as while/until
|
|
122
|
+
let trailing = format_trailing_comment(ctx, node.location.start_line);
|
|
123
|
+
if !trailing.is_empty() {
|
|
124
|
+
docs.push(trailing);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Emit body - second child (StatementsNode)
|
|
128
|
+
if let Some(body) = node.children.get(1) {
|
|
129
|
+
if matches!(body.node_type, NodeType::StatementsNode) {
|
|
130
|
+
let body_doc = format_statements(body, ctx, registry)?;
|
|
131
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
docs.push(hardline());
|
|
136
|
+
docs.push(text("end"));
|
|
137
|
+
|
|
138
|
+
// Trailing comment on end line
|
|
139
|
+
let end_trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
140
|
+
if !end_trailing.is_empty() {
|
|
141
|
+
docs.push(end_trailing);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Ok(concat(docs))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Formats for loop
|
|
148
|
+
fn format_for(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
149
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(10);
|
|
150
|
+
|
|
151
|
+
// Leading comments
|
|
152
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
153
|
+
if !leading.is_empty() {
|
|
154
|
+
docs.push(leading);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
docs.push(text("for "));
|
|
158
|
+
|
|
159
|
+
// node.children: [index, collection, statements]
|
|
160
|
+
// index: LocalVariableTargetNode or MultiTargetNode
|
|
161
|
+
// collection: expression
|
|
162
|
+
// statements: StatementsNode
|
|
163
|
+
|
|
164
|
+
// Emit index variable - first child
|
|
165
|
+
if let Some(index) = node.children.first() {
|
|
166
|
+
if let Some(source_text) = ctx.extract_source(index) {
|
|
167
|
+
docs.push(text(source_text));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
docs.push(text(" in "));
|
|
172
|
+
|
|
173
|
+
// Emit collection - second child
|
|
174
|
+
if let Some(collection) = node.children.get(1) {
|
|
175
|
+
if let Some(source_text) = ctx.extract_source(collection) {
|
|
176
|
+
docs.push(text(source_text));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Emit body - third child (StatementsNode)
|
|
181
|
+
if let Some(body) = node.children.get(2) {
|
|
182
|
+
if matches!(body.node_type, NodeType::StatementsNode) {
|
|
183
|
+
let body_doc = format_statements(body, ctx, registry)?;
|
|
184
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
docs.push(hardline());
|
|
189
|
+
docs.push(text("end"));
|
|
190
|
+
|
|
191
|
+
// Trailing comment on end line
|
|
192
|
+
let trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
193
|
+
if !trailing.is_empty() {
|
|
194
|
+
docs.push(trailing);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Ok(concat(docs))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[cfg(test)]
|
|
201
|
+
mod tests {
|
|
202
|
+
use super::*;
|
|
203
|
+
use crate::ast::{FormattingInfo, Location};
|
|
204
|
+
use crate::config::Config;
|
|
205
|
+
use crate::doc::Printer;
|
|
206
|
+
use std::collections::HashMap;
|
|
207
|
+
|
|
208
|
+
fn make_while_node(
|
|
209
|
+
children: Vec<Node>,
|
|
210
|
+
start_line: usize,
|
|
211
|
+
end_line: usize,
|
|
212
|
+
start_offset: usize,
|
|
213
|
+
end_offset: usize,
|
|
214
|
+
) -> Node {
|
|
215
|
+
Node {
|
|
216
|
+
node_type: NodeType::WhileNode,
|
|
217
|
+
location: Location::new(start_line, 0, end_line, 3, start_offset, end_offset),
|
|
218
|
+
children,
|
|
219
|
+
metadata: HashMap::new(),
|
|
220
|
+
comments: Vec::new(),
|
|
221
|
+
formatting: FormattingInfo::default(),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn make_for_node(children: Vec<Node>, start_line: usize, end_line: usize) -> Node {
|
|
226
|
+
Node {
|
|
227
|
+
node_type: NodeType::ForNode,
|
|
228
|
+
location: Location::new(start_line, 0, end_line, 3, 0, 50),
|
|
229
|
+
children,
|
|
230
|
+
metadata: HashMap::new(),
|
|
231
|
+
comments: Vec::new(),
|
|
232
|
+
formatting: FormattingInfo::default(),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[test]
|
|
237
|
+
fn test_simple_while() {
|
|
238
|
+
let config = Config::default();
|
|
239
|
+
let source = "while true\n puts 'loop'\nend";
|
|
240
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
241
|
+
let registry = RuleRegistry::default_registry();
|
|
242
|
+
|
|
243
|
+
// predicate: "true" at offset 6-10
|
|
244
|
+
let predicate = Node {
|
|
245
|
+
node_type: NodeType::TrueNode,
|
|
246
|
+
location: Location::new(1, 6, 1, 10, 6, 10),
|
|
247
|
+
children: Vec::new(),
|
|
248
|
+
metadata: HashMap::new(),
|
|
249
|
+
comments: Vec::new(),
|
|
250
|
+
formatting: FormattingInfo::default(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// body statements at offset 13-25
|
|
254
|
+
let body = Node {
|
|
255
|
+
node_type: NodeType::StatementsNode,
|
|
256
|
+
location: Location::new(2, 2, 2, 14, 13, 25),
|
|
257
|
+
children: Vec::new(),
|
|
258
|
+
metadata: HashMap::new(),
|
|
259
|
+
comments: Vec::new(),
|
|
260
|
+
formatting: FormattingInfo::default(),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
let node = make_while_node(vec![predicate, body], 1, 3, 0, 29);
|
|
264
|
+
ctx.collect_comments(&node);
|
|
265
|
+
|
|
266
|
+
let rule = WhileRule;
|
|
267
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
268
|
+
|
|
269
|
+
let mut printer = Printer::new(&config);
|
|
270
|
+
let result = printer.print(&doc);
|
|
271
|
+
|
|
272
|
+
assert!(result.contains("while true"));
|
|
273
|
+
assert!(result.contains("end"));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#[test]
|
|
277
|
+
fn test_simple_for() {
|
|
278
|
+
let config = Config::default();
|
|
279
|
+
let source = "for x in items\n puts x\nend";
|
|
280
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
281
|
+
let registry = RuleRegistry::default_registry();
|
|
282
|
+
|
|
283
|
+
// index: "x" at offset 4-5
|
|
284
|
+
let index = Node {
|
|
285
|
+
node_type: NodeType::LocalVariableReadNode,
|
|
286
|
+
location: Location::new(1, 4, 1, 5, 4, 5),
|
|
287
|
+
children: Vec::new(),
|
|
288
|
+
metadata: HashMap::new(),
|
|
289
|
+
comments: Vec::new(),
|
|
290
|
+
formatting: FormattingInfo::default(),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// collection: "items" at offset 9-14
|
|
294
|
+
let collection = Node {
|
|
295
|
+
node_type: NodeType::LocalVariableReadNode,
|
|
296
|
+
location: Location::new(1, 9, 1, 14, 9, 14),
|
|
297
|
+
children: Vec::new(),
|
|
298
|
+
metadata: HashMap::new(),
|
|
299
|
+
comments: Vec::new(),
|
|
300
|
+
formatting: FormattingInfo::default(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// body
|
|
304
|
+
let body = Node {
|
|
305
|
+
node_type: NodeType::StatementsNode,
|
|
306
|
+
location: Location::new(2, 2, 2, 8, 17, 23),
|
|
307
|
+
children: Vec::new(),
|
|
308
|
+
metadata: HashMap::new(),
|
|
309
|
+
comments: Vec::new(),
|
|
310
|
+
formatting: FormattingInfo::default(),
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
let node = make_for_node(vec![index, collection, body], 1, 3);
|
|
314
|
+
ctx.collect_comments(&node);
|
|
315
|
+
|
|
316
|
+
let rule = ForRule;
|
|
317
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
318
|
+
|
|
319
|
+
let mut printer = Printer::new(&config);
|
|
320
|
+
let result = printer.print(&doc);
|
|
321
|
+
|
|
322
|
+
assert!(result.contains("for x in items"));
|
|
323
|
+
assert!(result.contains("end"));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//! Formatting rules for different AST node types
|
|
2
|
+
//!
|
|
3
|
+
//! This module contains the FormatRule implementations for each supported
|
|
4
|
+
//! node type. Each rule is responsible for converting its node type to Doc IR.
|
|
5
|
+
|
|
6
|
+
mod begin;
|
|
7
|
+
mod body_end;
|
|
8
|
+
mod call;
|
|
9
|
+
mod case;
|
|
10
|
+
mod class;
|
|
11
|
+
mod def;
|
|
12
|
+
mod fallback;
|
|
13
|
+
mod if_unless;
|
|
14
|
+
mod loops;
|
|
15
|
+
mod module;
|
|
16
|
+
mod singleton_class;
|
|
17
|
+
mod statements;
|
|
18
|
+
mod variable_write;
|
|
19
|
+
|
|
20
|
+
pub use begin::{BeginRule, EnsureRule, RescueRule};
|
|
21
|
+
pub use call::{BlockRule, CallRule, LambdaRule};
|
|
22
|
+
pub use case::{CaseMatchRule, CaseRule, InRule, WhenRule};
|
|
23
|
+
pub use class::ClassRule;
|
|
24
|
+
pub use def::DefRule;
|
|
25
|
+
pub use fallback::FallbackRule;
|
|
26
|
+
pub use if_unless::{IfRule, UnlessRule};
|
|
27
|
+
pub use loops::{ForRule, UntilRule, WhileRule};
|
|
28
|
+
pub use module::ModuleRule;
|
|
29
|
+
pub use singleton_class::SingletonClassRule;
|
|
30
|
+
pub use statements::StatementsRule;
|
|
31
|
+
pub use variable_write::{InstanceVariableWriteRule, LocalVariableWriteRule};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//! ModuleRule - Formats Ruby module definitions
|
|
2
|
+
//!
|
|
3
|
+
//! Handles module definitions including:
|
|
4
|
+
//! - Simple modules: `module Foo`
|
|
5
|
+
//! - Nested modules: `module Foo::Bar`
|
|
6
|
+
//! - Module bodies with methods and other declarations
|
|
7
|
+
//! - Leading and trailing comments
|
|
8
|
+
|
|
9
|
+
use crate::ast::Node;
|
|
10
|
+
use crate::doc::{text, Doc};
|
|
11
|
+
use crate::error::Result;
|
|
12
|
+
use crate::format::context::FormatContext;
|
|
13
|
+
use crate::format::registry::RuleRegistry;
|
|
14
|
+
use crate::format::rule::FormatRule;
|
|
15
|
+
|
|
16
|
+
use super::body_end::{format_body_end, BodyEndConfig};
|
|
17
|
+
|
|
18
|
+
/// Rule for formatting module definitions.
|
|
19
|
+
pub struct ModuleRule;
|
|
20
|
+
|
|
21
|
+
impl FormatRule for ModuleRule {
|
|
22
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
23
|
+
format_body_end(
|
|
24
|
+
ctx,
|
|
25
|
+
registry,
|
|
26
|
+
BodyEndConfig {
|
|
27
|
+
keyword: "module",
|
|
28
|
+
node,
|
|
29
|
+
header_builder: Box::new(build_module_header),
|
|
30
|
+
skip_same_line_children: false,
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Builds the header portion for a module definition.
|
|
37
|
+
///
|
|
38
|
+
/// Returns: `ModuleName`
|
|
39
|
+
fn build_module_header(node: &Node) -> Vec<Doc> {
|
|
40
|
+
let mut parts: Vec<Doc> = Vec::with_capacity(1);
|
|
41
|
+
|
|
42
|
+
// Get module name from metadata
|
|
43
|
+
if let Some(name) = node.metadata.get("name") {
|
|
44
|
+
parts.push(text(name));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
parts
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[cfg(test)]
|
|
51
|
+
mod tests {
|
|
52
|
+
use super::*;
|
|
53
|
+
use crate::ast::{FormattingInfo, Location, NodeType};
|
|
54
|
+
use crate::config::Config;
|
|
55
|
+
use crate::doc::Printer;
|
|
56
|
+
use std::collections::HashMap;
|
|
57
|
+
|
|
58
|
+
fn make_module_node(
|
|
59
|
+
name: &str,
|
|
60
|
+
children: Vec<Node>,
|
|
61
|
+
start_line: usize,
|
|
62
|
+
end_line: usize,
|
|
63
|
+
) -> Node {
|
|
64
|
+
let mut metadata = HashMap::new();
|
|
65
|
+
metadata.insert("name".to_string(), name.to_string());
|
|
66
|
+
|
|
67
|
+
Node {
|
|
68
|
+
node_type: NodeType::ModuleNode,
|
|
69
|
+
location: Location::new(start_line, 0, end_line, 3, 0, 50),
|
|
70
|
+
children,
|
|
71
|
+
metadata,
|
|
72
|
+
comments: Vec::new(),
|
|
73
|
+
formatting: FormattingInfo::default(),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn test_simple_module() {
|
|
79
|
+
let config = Config::default();
|
|
80
|
+
let source = "module Foo\nend";
|
|
81
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
82
|
+
let registry = RuleRegistry::default_registry();
|
|
83
|
+
|
|
84
|
+
let node = make_module_node("Foo", Vec::new(), 1, 2);
|
|
85
|
+
ctx.collect_comments(&node);
|
|
86
|
+
|
|
87
|
+
let rule = ModuleRule;
|
|
88
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
89
|
+
|
|
90
|
+
let mut printer = Printer::new(&config);
|
|
91
|
+
let result = printer.print(&doc);
|
|
92
|
+
|
|
93
|
+
assert_eq!(result.trim(), "module Foo\nend");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn test_nested_module_name() {
|
|
98
|
+
let config = Config::default();
|
|
99
|
+
let source = "module Foo::Bar\nend";
|
|
100
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
101
|
+
let registry = RuleRegistry::default_registry();
|
|
102
|
+
|
|
103
|
+
let node = make_module_node("Foo::Bar", Vec::new(), 1, 2);
|
|
104
|
+
ctx.collect_comments(&node);
|
|
105
|
+
|
|
106
|
+
let rule = ModuleRule;
|
|
107
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
108
|
+
|
|
109
|
+
let mut printer = Printer::new(&config);
|
|
110
|
+
let result = printer.print(&doc);
|
|
111
|
+
|
|
112
|
+
assert_eq!(result.trim(), "module Foo::Bar\nend");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn test_module_with_body() {
|
|
117
|
+
let config = Config::default();
|
|
118
|
+
let source = "module Foo\n def bar\n end\nend";
|
|
119
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
120
|
+
let registry = RuleRegistry::default_registry();
|
|
121
|
+
|
|
122
|
+
// Create a method node as child
|
|
123
|
+
let method_node = Node {
|
|
124
|
+
node_type: NodeType::DefNode,
|
|
125
|
+
location: Location::new(2, 2, 3, 5, 13, 25),
|
|
126
|
+
children: Vec::new(),
|
|
127
|
+
metadata: {
|
|
128
|
+
let mut m = HashMap::new();
|
|
129
|
+
m.insert("name".to_string(), "bar".to_string());
|
|
130
|
+
m
|
|
131
|
+
},
|
|
132
|
+
comments: Vec::new(),
|
|
133
|
+
formatting: FormattingInfo::default(),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let node = make_module_node("Foo", vec![method_node], 1, 4);
|
|
137
|
+
ctx.collect_comments(&node);
|
|
138
|
+
|
|
139
|
+
let rule = ModuleRule;
|
|
140
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
141
|
+
|
|
142
|
+
let mut printer = Printer::new(&config);
|
|
143
|
+
let result = printer.print(&doc);
|
|
144
|
+
|
|
145
|
+
// Should have the module, body, and end
|
|
146
|
+
assert!(result.contains("module Foo"));
|
|
147
|
+
assert!(result.contains("def bar"));
|
|
148
|
+
assert!(result.contains("end"));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//! SingletonClassRule - Formats Ruby singleton class definitions
|
|
2
|
+
//!
|
|
3
|
+
//! Handles singleton class definitions:
|
|
4
|
+
//! - `class << self ... end`
|
|
5
|
+
//! - `class << object ... end`
|
|
6
|
+
|
|
7
|
+
use crate::ast::{Node, NodeType};
|
|
8
|
+
use crate::doc::{concat, hardline, indent, text, Doc};
|
|
9
|
+
use crate::error::Result;
|
|
10
|
+
use crate::format::context::FormatContext;
|
|
11
|
+
use crate::format::registry::RuleRegistry;
|
|
12
|
+
use crate::format::rule::{
|
|
13
|
+
format_child, format_comments_before_end, format_leading_comments, format_statements,
|
|
14
|
+
format_trailing_comment, FormatRule,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/// Rule for formatting singleton class definitions.
|
|
18
|
+
///
|
|
19
|
+
/// Handles `class << self` and `class << object` patterns.
|
|
20
|
+
pub struct SingletonClassRule;
|
|
21
|
+
|
|
22
|
+
impl FormatRule for SingletonClassRule {
|
|
23
|
+
fn format(&self, node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) -> Result<Doc> {
|
|
24
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(8);
|
|
25
|
+
|
|
26
|
+
let start_line = node.location.start_line;
|
|
27
|
+
let end_line = node.location.end_line;
|
|
28
|
+
|
|
29
|
+
// 1. Leading comments before definition
|
|
30
|
+
let leading = format_leading_comments(ctx, start_line);
|
|
31
|
+
if !leading.is_empty() {
|
|
32
|
+
docs.push(leading);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Build header: "class << "
|
|
36
|
+
docs.push(text("class << "));
|
|
37
|
+
|
|
38
|
+
// 3. First child is the expression (self or an object)
|
|
39
|
+
if let Some(expression) = node.children.first() {
|
|
40
|
+
if let Some(expr_text) = ctx.extract_source(expression) {
|
|
41
|
+
docs.push(text(expr_text.to_string()));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 4. Trailing comment on definition line
|
|
46
|
+
let trailing = format_trailing_comment(ctx, start_line);
|
|
47
|
+
if !trailing.is_empty() {
|
|
48
|
+
docs.push(trailing);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 5. Body (children), skipping the first child (expression)
|
|
52
|
+
let mut body_docs: Vec<Doc> = Vec::new();
|
|
53
|
+
let mut has_body_content = false;
|
|
54
|
+
|
|
55
|
+
for (i, child) in node.children.iter().enumerate() {
|
|
56
|
+
// Skip the first child (expression: self or object)
|
|
57
|
+
if i == 0 {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if matches!(child.node_type, NodeType::StatementsNode) {
|
|
62
|
+
has_body_content = true;
|
|
63
|
+
body_docs.push(hardline());
|
|
64
|
+
body_docs.push(format_statements(child, ctx, registry)?);
|
|
65
|
+
} else if !is_structural_node_for_singleton(child) {
|
|
66
|
+
has_body_content = true;
|
|
67
|
+
body_docs.push(hardline());
|
|
68
|
+
body_docs.push(format_child(child, ctx, registry)?);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if has_body_content {
|
|
73
|
+
docs.push(indent(concat(body_docs)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 6. Comments before end
|
|
77
|
+
let comments_before_end = format_comments_before_end(ctx, start_line, end_line);
|
|
78
|
+
if !comments_before_end.is_empty() {
|
|
79
|
+
docs.push(indent(comments_before_end));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 7. Add newline before end
|
|
83
|
+
docs.push(hardline());
|
|
84
|
+
|
|
85
|
+
// 8. End keyword
|
|
86
|
+
docs.push(text("end"));
|
|
87
|
+
|
|
88
|
+
// 9. Trailing comment on end line
|
|
89
|
+
let end_trailing = format_trailing_comment(ctx, end_line);
|
|
90
|
+
if !end_trailing.is_empty() {
|
|
91
|
+
docs.push(end_trailing);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Ok(concat(docs))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Check if a node is structural (should be skipped in body).
|
|
99
|
+
fn is_structural_node_for_singleton(node: &Node) -> bool {
|
|
100
|
+
matches!(
|
|
101
|
+
node.node_type,
|
|
102
|
+
NodeType::ConstantReadNode
|
|
103
|
+
| NodeType::ConstantWriteNode
|
|
104
|
+
| NodeType::ConstantPathNode
|
|
105
|
+
| NodeType::SelfNode
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[cfg(test)]
|
|
110
|
+
mod tests {
|
|
111
|
+
use super::*;
|
|
112
|
+
use crate::ast::{FormattingInfo, Location};
|
|
113
|
+
use crate::config::Config;
|
|
114
|
+
use crate::doc::Printer;
|
|
115
|
+
use std::collections::HashMap;
|
|
116
|
+
|
|
117
|
+
fn make_singleton_class_node(children: Vec<Node>, start_line: usize, end_line: usize) -> Node {
|
|
118
|
+
Node {
|
|
119
|
+
node_type: NodeType::SingletonClassNode,
|
|
120
|
+
location: Location::new(start_line, 0, end_line, 3, 0, 50),
|
|
121
|
+
children,
|
|
122
|
+
metadata: HashMap::new(),
|
|
123
|
+
comments: Vec::new(),
|
|
124
|
+
formatting: FormattingInfo::default(),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn make_self_node(line: usize, start_offset: usize) -> Node {
|
|
129
|
+
Node {
|
|
130
|
+
node_type: NodeType::SelfNode,
|
|
131
|
+
location: Location::new(line, 9, line, 13, start_offset, start_offset + 4),
|
|
132
|
+
children: Vec::new(),
|
|
133
|
+
metadata: HashMap::new(),
|
|
134
|
+
comments: Vec::new(),
|
|
135
|
+
formatting: FormattingInfo::default(),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn test_simple_singleton_class() {
|
|
141
|
+
let config = Config::default();
|
|
142
|
+
let source = "class << self\nend";
|
|
143
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
144
|
+
let registry = RuleRegistry::default_registry();
|
|
145
|
+
|
|
146
|
+
let self_node = make_self_node(1, 9);
|
|
147
|
+
let node = make_singleton_class_node(vec![self_node], 1, 2);
|
|
148
|
+
ctx.collect_comments(&node);
|
|
149
|
+
|
|
150
|
+
let rule = SingletonClassRule;
|
|
151
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
152
|
+
|
|
153
|
+
let mut printer = Printer::new(&config);
|
|
154
|
+
let result = printer.print(&doc);
|
|
155
|
+
|
|
156
|
+
assert_eq!(result.trim(), "class << self\nend");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[test]
|
|
160
|
+
fn test_singleton_class_with_body() {
|
|
161
|
+
let config = Config::default();
|
|
162
|
+
let source = "class << self\n def foo\n end\nend";
|
|
163
|
+
let mut ctx = FormatContext::new(&config, source);
|
|
164
|
+
let registry = RuleRegistry::default_registry();
|
|
165
|
+
|
|
166
|
+
let self_node = make_self_node(1, 9);
|
|
167
|
+
let method_node = Node {
|
|
168
|
+
node_type: NodeType::DefNode,
|
|
169
|
+
location: Location::new(2, 2, 3, 5, 16, 28),
|
|
170
|
+
children: Vec::new(),
|
|
171
|
+
metadata: {
|
|
172
|
+
let mut m = HashMap::new();
|
|
173
|
+
m.insert("name".to_string(), "foo".to_string());
|
|
174
|
+
m
|
|
175
|
+
},
|
|
176
|
+
comments: Vec::new(),
|
|
177
|
+
formatting: FormattingInfo::default(),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
let statements_node = Node {
|
|
181
|
+
node_type: NodeType::StatementsNode,
|
|
182
|
+
location: Location::new(2, 2, 3, 5, 16, 28),
|
|
183
|
+
children: vec![method_node],
|
|
184
|
+
metadata: HashMap::new(),
|
|
185
|
+
comments: Vec::new(),
|
|
186
|
+
formatting: FormattingInfo::default(),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
let node = make_singleton_class_node(vec![self_node, statements_node], 1, 4);
|
|
190
|
+
ctx.collect_comments(&node);
|
|
191
|
+
|
|
192
|
+
let rule = SingletonClassRule;
|
|
193
|
+
let doc = rule.format(&node, &mut ctx, ®istry).unwrap();
|
|
194
|
+
|
|
195
|
+
let mut printer = Printer::new(&config);
|
|
196
|
+
let result = printer.print(&doc);
|
|
197
|
+
|
|
198
|
+
assert!(result.contains("class << self"));
|
|
199
|
+
assert!(result.contains("def foo"));
|
|
200
|
+
assert!(result.contains("end"));
|
|
201
|
+
}
|
|
202
|
+
}
|