rfmt 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/Cargo.lock +1 -1
- data/ext/rfmt/Cargo.toml +1 -1
- data/ext/rfmt/src/doc/printer.rs +37 -0
- data/ext/rfmt/src/format/formatter.rs +28 -4
- data/ext/rfmt/src/format/rule.rs +245 -24
- data/ext/rfmt/src/format/rules/begin.rs +161 -22
- data/ext/rfmt/src/format/rules/body_end.rs +146 -22
- data/ext/rfmt/src/format/rules/call.rs +81 -25
- data/ext/rfmt/src/format/rules/fallback.rs +20 -6
- data/ext/rfmt/src/format/rules/if_unless.rs +51 -4
- data/ext/rfmt/src/format/rules/variable_write.rs +25 -7
- data/lib/rfmt/prism_bridge.rb +43 -12
- data/lib/rfmt/version.rb +1 -1
- metadata +1 -1
|
@@ -13,6 +13,107 @@ use crate::format::rule::{
|
|
|
13
13
|
format_child, format_leading_comments, format_statements, format_trailing_comment, FormatRule,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
/// True when this BeginNode represents an implicit begin (the source does not
|
|
17
|
+
/// start with the `begin` keyword) AND carries at least one rescue/else/ensure
|
|
18
|
+
/// clause. Such bodies need the rescue/else/ensure keywords emitted at the
|
|
19
|
+
/// outer (e.g. `def`) indent level rather than at the body indent level.
|
|
20
|
+
pub(crate) fn is_implicit_begin_with_clauses(node: &Node, ctx: &FormatContext) -> bool {
|
|
21
|
+
if node.node_type != NodeType::BeginNode {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
let has_clause = node.children.iter().any(|c| {
|
|
25
|
+
matches!(
|
|
26
|
+
c.node_type,
|
|
27
|
+
NodeType::RescueNode | NodeType::EnsureNode | NodeType::ElseNode
|
|
28
|
+
)
|
|
29
|
+
});
|
|
30
|
+
if !has_clause {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
ctx.extract_source(node)
|
|
34
|
+
.map(|s| !s.trim_start().starts_with("begin"))
|
|
35
|
+
.unwrap_or(false)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Emits the body of a construct (def/class/module/block) whose body is an
|
|
39
|
+
/// implicit BeginNode with rescue/else/ensure clauses.
|
|
40
|
+
///
|
|
41
|
+
/// The returned Doc is meant to sit between the opening line (e.g. `def foo`)
|
|
42
|
+
/// and a trailing `hardline + text("end")` emitted by the caller. Body
|
|
43
|
+
/// statements are wrapped in `indent`, while rescue/else/ensure clause
|
|
44
|
+
/// keywords are emitted at the caller's current indent level so that they
|
|
45
|
+
/// align with the opener instead of the body.
|
|
46
|
+
pub(crate) fn format_implicit_begin_body(
|
|
47
|
+
node: &Node,
|
|
48
|
+
ctx: &mut FormatContext,
|
|
49
|
+
registry: &RuleRegistry,
|
|
50
|
+
) -> Result<Doc> {
|
|
51
|
+
let mut body_children: Vec<&Node> = Vec::new();
|
|
52
|
+
let mut clause_children: Vec<&Node> = Vec::new();
|
|
53
|
+
|
|
54
|
+
for child in &node.children {
|
|
55
|
+
match child.node_type {
|
|
56
|
+
NodeType::RescueNode | NodeType::EnsureNode | NodeType::ElseNode => {
|
|
57
|
+
clause_children.push(child);
|
|
58
|
+
}
|
|
59
|
+
_ => {
|
|
60
|
+
body_children.push(child);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(clause_children.len() * 2 + 2);
|
|
66
|
+
|
|
67
|
+
if !body_children.is_empty() {
|
|
68
|
+
let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2 + 1);
|
|
69
|
+
body_docs.push(hardline());
|
|
70
|
+
for (i, child) in body_children.iter().enumerate() {
|
|
71
|
+
if i > 0 {
|
|
72
|
+
body_docs.push(hardline());
|
|
73
|
+
}
|
|
74
|
+
body_docs.push(format_child(child, ctx, registry)?);
|
|
75
|
+
}
|
|
76
|
+
docs.push(indent(concat(body_docs)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for clause in clause_children {
|
|
80
|
+
docs.push(hardline());
|
|
81
|
+
docs.push(format_begin_clause(clause, ctx, registry)?);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Ok(concat(docs))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Emits a rescue/else/ensure clause the way the enclosing begin expects.
|
|
88
|
+
///
|
|
89
|
+
/// `format_child` sends ElseNode to `FallbackRule`, which slices the source
|
|
90
|
+
/// between `else` and the next keyword — but Prism's `ElseNode.location`
|
|
91
|
+
/// stretches into the *following* clause's keyword (`ensure`, or the
|
|
92
|
+
/// begin's `end`). Emitting that slice as-is duplicates whichever keyword
|
|
93
|
+
/// comes after, producing e.g. `else\n y\nensure\nensure\n z\nend` or
|
|
94
|
+
/// `else\n y\nend\nend`. Handle ElseNode explicitly so we emit only
|
|
95
|
+
/// `else\n body` and let the caller (or the subsequent EnsureRule) write
|
|
96
|
+
/// the following keyword.
|
|
97
|
+
fn format_begin_clause(
|
|
98
|
+
clause: &Node,
|
|
99
|
+
ctx: &mut FormatContext,
|
|
100
|
+
registry: &RuleRegistry,
|
|
101
|
+
) -> Result<Doc> {
|
|
102
|
+
if !matches!(clause.node_type, NodeType::ElseNode) {
|
|
103
|
+
return format_child(clause, ctx, registry);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let mut docs: Vec<Doc> = Vec::with_capacity(3);
|
|
107
|
+
docs.push(text("else"));
|
|
108
|
+
for child in &clause.children {
|
|
109
|
+
if matches!(child.node_type, NodeType::StatementsNode) {
|
|
110
|
+
let body_doc = format_statements(child, ctx, registry)?;
|
|
111
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
Ok(concat(docs))
|
|
115
|
+
}
|
|
116
|
+
|
|
16
117
|
/// Rule for formatting begin/rescue/ensure blocks.
|
|
17
118
|
pub struct BeginRule;
|
|
18
119
|
|
|
@@ -71,14 +172,36 @@ fn format_explicit_begin(
|
|
|
71
172
|
}
|
|
72
173
|
|
|
73
174
|
docs.push(text("begin"));
|
|
74
|
-
docs.push(hardline());
|
|
75
175
|
|
|
176
|
+
let mut body_children: Vec<&Node> = Vec::new();
|
|
177
|
+
let mut clause_children: Vec<&Node> = Vec::new();
|
|
76
178
|
for child in &node.children {
|
|
77
|
-
|
|
78
|
-
|
|
179
|
+
match child.node_type {
|
|
180
|
+
NodeType::RescueNode | NodeType::EnsureNode | NodeType::ElseNode => {
|
|
181
|
+
clause_children.push(child);
|
|
182
|
+
}
|
|
183
|
+
_ => body_children.push(child),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if !body_children.is_empty() {
|
|
188
|
+
let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2 + 1);
|
|
189
|
+
body_docs.push(hardline());
|
|
190
|
+
for (i, child) in body_children.iter().enumerate() {
|
|
191
|
+
if i > 0 {
|
|
192
|
+
body_docs.push(hardline());
|
|
193
|
+
}
|
|
194
|
+
body_docs.push(format_child(child, ctx, registry)?);
|
|
195
|
+
}
|
|
196
|
+
docs.push(indent(concat(body_docs)));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for clause in &clause_children {
|
|
79
200
|
docs.push(hardline());
|
|
201
|
+
docs.push(format_begin_clause(clause, ctx, registry)?);
|
|
80
202
|
}
|
|
81
203
|
|
|
204
|
+
docs.push(hardline());
|
|
82
205
|
docs.push(text("end"));
|
|
83
206
|
|
|
84
207
|
// Trailing comment on end line
|
|
@@ -122,6 +245,15 @@ fn format_rescue(
|
|
|
122
245
|
// But since we're in Doc IR, we handle this at the caller level
|
|
123
246
|
let _ = dedent_level;
|
|
124
247
|
|
|
248
|
+
// Emit any standalone comments that sit immediately before the rescue
|
|
249
|
+
// keyword. Without this they would be picked up as leading comments of
|
|
250
|
+
// the first statement inside the rescue body, which visually moves an
|
|
251
|
+
// annotation like `# retry on flaky errors` *into* the rescue branch.
|
|
252
|
+
let leading = format_leading_comments(ctx, node.location.start_line);
|
|
253
|
+
if !leading.is_empty() {
|
|
254
|
+
docs.push(leading);
|
|
255
|
+
}
|
|
256
|
+
|
|
125
257
|
docs.push(text("rescue"));
|
|
126
258
|
|
|
127
259
|
// Extract exception classes and variable from source
|
|
@@ -167,26 +299,32 @@ fn format_rescue(
|
|
|
167
299
|
}
|
|
168
300
|
}
|
|
169
301
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
302
|
+
// Emit rescue body (indented under the rescue keyword) and subsequent
|
|
303
|
+
// chained rescue clauses (at the same indent level as this rescue).
|
|
304
|
+
//
|
|
305
|
+
// The hardline lives INSIDE the `indent(...)` wrap so that the body's
|
|
306
|
+
// first statement lands at `caller_indent + indent_width`, matching the
|
|
307
|
+
// indentation applied by hardlines later inside `format_statements`.
|
|
308
|
+
let mut body_stmts: Option<&Node> = None;
|
|
309
|
+
let mut subsequent: Option<&Node> = None;
|
|
173
310
|
for child in &node.children {
|
|
174
311
|
match &child.node_type {
|
|
175
|
-
NodeType::StatementsNode =>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
NodeType::RescueNode => {
|
|
180
|
-
// Emit subsequent rescue clause
|
|
181
|
-
let rescue_doc = format_rescue(child, ctx, registry, dedent_level)?;
|
|
182
|
-
docs.push(rescue_doc);
|
|
183
|
-
}
|
|
184
|
-
_ => {
|
|
185
|
-
// Skip exception classes and variable (already handled above)
|
|
186
|
-
}
|
|
312
|
+
NodeType::StatementsNode => body_stmts = Some(child),
|
|
313
|
+
NodeType::RescueNode => subsequent = Some(child),
|
|
314
|
+
_ => {}
|
|
187
315
|
}
|
|
188
316
|
}
|
|
189
317
|
|
|
318
|
+
if let Some(stmts) = body_stmts {
|
|
319
|
+
let body_doc = format_statements(stmts, ctx, registry)?;
|
|
320
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if let Some(sub) = subsequent {
|
|
324
|
+
docs.push(hardline());
|
|
325
|
+
docs.push(format_rescue(sub, ctx, registry, dedent_level)?);
|
|
326
|
+
}
|
|
327
|
+
|
|
190
328
|
Ok(concat(docs))
|
|
191
329
|
}
|
|
192
330
|
|
|
@@ -207,18 +345,19 @@ fn format_ensure(
|
|
|
207
345
|
}
|
|
208
346
|
|
|
209
347
|
docs.push(text("ensure"));
|
|
210
|
-
docs.push(hardline());
|
|
211
348
|
|
|
212
|
-
// Emit ensure body
|
|
349
|
+
// Emit ensure body. The hardline lives INSIDE the `indent(...)` wrap so
|
|
350
|
+
// that every body statement — including the first one — lands at
|
|
351
|
+
// `caller_indent + indent_width`.
|
|
213
352
|
for child in &node.children {
|
|
214
353
|
match &child.node_type {
|
|
215
354
|
NodeType::StatementsNode => {
|
|
216
355
|
let body_doc = format_statements(child, ctx, registry)?;
|
|
217
|
-
docs.push(indent(body_doc));
|
|
356
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
218
357
|
}
|
|
219
358
|
_ => {
|
|
220
359
|
let child_doc = format_child(child, ctx, registry)?;
|
|
221
|
-
docs.push(indent(child_doc));
|
|
360
|
+
docs.push(indent(concat(vec![hardline(), child_doc])));
|
|
222
361
|
}
|
|
223
362
|
}
|
|
224
363
|
}
|
|
@@ -10,9 +10,11 @@ use crate::format::context::FormatContext;
|
|
|
10
10
|
use crate::format::registry::RuleRegistry;
|
|
11
11
|
use crate::format::rule::{
|
|
12
12
|
format_child, format_comments_before_end, format_leading_comments, format_trailing_comment,
|
|
13
|
-
is_structural_node,
|
|
13
|
+
is_structural_node, mark_comments_in_range_emitted,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
use super::begin::{format_implicit_begin_body, is_implicit_begin_with_clauses};
|
|
17
|
+
|
|
16
18
|
/// Configuration for formatting a body-with-end construct.
|
|
17
19
|
pub struct BodyEndConfig<'a> {
|
|
18
20
|
/// The keyword (e.g., "class", "module", "def")
|
|
@@ -51,6 +53,79 @@ pub fn format_body_end(
|
|
|
51
53
|
docs.push(leading);
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
// Single-line form: `def foo = expr` (endless), `def foo; body; end`,
|
|
57
|
+
// `class Foo; end`, etc. Emit the source verbatim instead of forcing
|
|
58
|
+
// a multi-line `def ... end` layout. This preserves Ruby 3+ endless
|
|
59
|
+
// methods and the `Error < StandardError; end` exception-hierarchy
|
|
60
|
+
// idiom that is pervasive in Rails code.
|
|
61
|
+
if start_line == end_line {
|
|
62
|
+
if let Some(source_text) = ctx.extract_source(config.node) {
|
|
63
|
+
docs.push(text(source_text.to_string()));
|
|
64
|
+
mark_comments_in_range_emitted(ctx, start_line, end_line);
|
|
65
|
+
|
|
66
|
+
let trailing = format_trailing_comment(ctx, end_line);
|
|
67
|
+
if !trailing.is_empty() {
|
|
68
|
+
docs.push(trailing);
|
|
69
|
+
}
|
|
70
|
+
return Ok(concat(docs));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Body children (filtered) — reused by both the multi-line header path
|
|
75
|
+
// below and the normal path further down.
|
|
76
|
+
let body_children: Vec<&Node> = config
|
|
77
|
+
.node
|
|
78
|
+
.children
|
|
79
|
+
.iter()
|
|
80
|
+
.filter(|c| {
|
|
81
|
+
if config.skip_same_line_children && c.location.start_line == start_line {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
!is_structural_node(c)
|
|
85
|
+
})
|
|
86
|
+
.collect();
|
|
87
|
+
|
|
88
|
+
// Multi-line header form: `def foo(a, # c1\n b, # c2\n c) # c3`.
|
|
89
|
+
//
|
|
90
|
+
// Rebuilding the header from `parameters_text` + a reconstructed `)`
|
|
91
|
+
// drops the closing line's trailing comment and, worse, lets comments
|
|
92
|
+
// that the params slice already contains be re-picked up by the later
|
|
93
|
+
// `format_trailing_comment` + `format_leading_comments` calls. The net
|
|
94
|
+
// effect is that the first inline comment is duplicated, the rest are
|
|
95
|
+
// moved into the body, and the file stops being idempotent because the
|
|
96
|
+
// relocated comments keep accumulating every format pass. Detect this
|
|
97
|
+
// case early and emit the header verbatim from source.
|
|
98
|
+
let params_text = config.node.metadata.get("parameters_text");
|
|
99
|
+
let header_is_multiline = params_text.is_some_and(|t| t.contains('\n'));
|
|
100
|
+
if header_is_multiline {
|
|
101
|
+
let header_end_line = body_children
|
|
102
|
+
.first()
|
|
103
|
+
.map(|c| c.location.start_line.saturating_sub(1))
|
|
104
|
+
.unwrap_or(start_line);
|
|
105
|
+
if header_end_line >= start_line {
|
|
106
|
+
let header_end_offset = line_end_offset(ctx.source(), header_end_line);
|
|
107
|
+
if let Some(header_src) = ctx
|
|
108
|
+
.source()
|
|
109
|
+
.get(config.node.location.start_offset..header_end_offset)
|
|
110
|
+
{
|
|
111
|
+
docs.push(text(header_src.to_string()));
|
|
112
|
+
mark_comments_in_range_emitted(ctx, start_line, header_end_line + 1);
|
|
113
|
+
|
|
114
|
+
// Body + "end" (same as the normal path; no header trailing
|
|
115
|
+
// comment — it's already baked into the source slice above).
|
|
116
|
+
push_body_and_end(
|
|
117
|
+
&mut docs,
|
|
118
|
+
ctx,
|
|
119
|
+
registry,
|
|
120
|
+
&body_children,
|
|
121
|
+
start_line,
|
|
122
|
+
end_line,
|
|
123
|
+
)?;
|
|
124
|
+
return Ok(concat(docs));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
54
129
|
// 2. Build header: "keyword ..."
|
|
55
130
|
let mut header_parts: Vec<Doc> = vec![text(config.keyword), text(" ")];
|
|
56
131
|
header_parts.extend((config.header_builder)(config.node));
|
|
@@ -62,28 +137,21 @@ pub fn format_body_end(
|
|
|
62
137
|
docs.push(trailing);
|
|
63
138
|
}
|
|
64
139
|
|
|
65
|
-
// 4. Body (children), skipping structural nodes
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
140
|
+
// 4. Body (children), skipping structural nodes — already collected above.
|
|
141
|
+
|
|
142
|
+
// Special case: body is an implicit BeginNode carrying rescue/else/ensure.
|
|
143
|
+
// In that case the clause keywords must align with the opener, not with
|
|
144
|
+
// the body statements — so we split the body and clause emission instead
|
|
145
|
+
// of wrapping everything in a single `indent(...)`.
|
|
146
|
+
if body_children.len() == 1 && is_implicit_begin_with_clauses(body_children[0], ctx) {
|
|
147
|
+
docs.push(format_implicit_begin_body(body_children[0], ctx, registry)?);
|
|
148
|
+
} else if !body_children.is_empty() {
|
|
149
|
+
let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2);
|
|
150
|
+
for child in &body_children {
|
|
151
|
+
let child_doc = format_child(child, ctx, registry)?;
|
|
152
|
+
body_docs.push(hardline());
|
|
153
|
+
body_docs.push(child_doc);
|
|
73
154
|
}
|
|
74
|
-
if is_structural_node(child) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
has_body_content = true;
|
|
79
|
-
|
|
80
|
-
// Format the child node using recursive rule dispatch
|
|
81
|
-
let child_doc = format_child(child, ctx, registry)?;
|
|
82
|
-
body_docs.push(hardline());
|
|
83
|
-
body_docs.push(child_doc);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if has_body_content {
|
|
87
155
|
docs.push(indent(concat(body_docs)));
|
|
88
156
|
}
|
|
89
157
|
|
|
@@ -107,3 +175,59 @@ pub fn format_body_end(
|
|
|
107
175
|
|
|
108
176
|
Ok(concat(docs))
|
|
109
177
|
}
|
|
178
|
+
|
|
179
|
+
/// Returns the byte offset of the character immediately past the end of the
|
|
180
|
+
/// given 1-based line (i.e. the index of the trailing `\n`, or `source.len()`
|
|
181
|
+
/// if the line has no trailing newline).
|
|
182
|
+
fn line_end_offset(source: &str, line: usize) -> usize {
|
|
183
|
+
let mut current = 1;
|
|
184
|
+
for (i, b) in source.bytes().enumerate() {
|
|
185
|
+
if b == b'\n' {
|
|
186
|
+
if current == line {
|
|
187
|
+
return i;
|
|
188
|
+
}
|
|
189
|
+
current += 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
source.len()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Emits the body (possibly an implicit BeginNode with rescue clauses) and the
|
|
196
|
+
/// trailing `end` keyword. Extracted so that both the standard
|
|
197
|
+
/// header-reconstruction path and the multi-line source-extraction path can
|
|
198
|
+
/// share the emission logic.
|
|
199
|
+
fn push_body_and_end(
|
|
200
|
+
docs: &mut Vec<Doc>,
|
|
201
|
+
ctx: &mut FormatContext,
|
|
202
|
+
registry: &RuleRegistry,
|
|
203
|
+
body_children: &[&Node],
|
|
204
|
+
start_line: usize,
|
|
205
|
+
end_line: usize,
|
|
206
|
+
) -> Result<()> {
|
|
207
|
+
if body_children.len() == 1 && is_implicit_begin_with_clauses(body_children[0], ctx) {
|
|
208
|
+
docs.push(format_implicit_begin_body(body_children[0], ctx, registry)?);
|
|
209
|
+
} else if !body_children.is_empty() {
|
|
210
|
+
let mut body_docs: Vec<Doc> = Vec::with_capacity(body_children.len() * 2);
|
|
211
|
+
for child in body_children {
|
|
212
|
+
let child_doc = format_child(child, ctx, registry)?;
|
|
213
|
+
body_docs.push(hardline());
|
|
214
|
+
body_docs.push(child_doc);
|
|
215
|
+
}
|
|
216
|
+
docs.push(indent(concat(body_docs)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let comments_before_end = format_comments_before_end(ctx, start_line, end_line);
|
|
220
|
+
if !comments_before_end.is_empty() {
|
|
221
|
+
docs.push(indent(comments_before_end));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
docs.push(hardline());
|
|
225
|
+
docs.push(text("end"));
|
|
226
|
+
|
|
227
|
+
let end_trailing = format_trailing_comment(ctx, end_line);
|
|
228
|
+
if !end_trailing.is_empty() {
|
|
229
|
+
docs.push(end_trailing);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Ok(())
|
|
233
|
+
}
|
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
//! - Calls with blocks: `foo.bar do ... end` or `foo.bar { ... }`
|
|
6
6
|
//! - Method chains: `foo.bar.baz`
|
|
7
7
|
|
|
8
|
+
use std::borrow::Cow;
|
|
9
|
+
|
|
8
10
|
use crate::ast::{Node, NodeType};
|
|
9
|
-
use crate::doc::{concat, hardline, indent, text, Doc};
|
|
11
|
+
use crate::doc::{align, concat, hardline, indent, text, Doc};
|
|
10
12
|
use crate::error::Result;
|
|
11
13
|
use crate::format::context::FormatContext;
|
|
12
14
|
use crate::format::registry::RuleRegistry;
|
|
13
15
|
use crate::format::rule::{
|
|
14
|
-
format_child, format_leading_comments, format_statements,
|
|
15
|
-
|
|
16
|
+
format_child, format_comments_before_end, format_leading_comments, format_statements,
|
|
17
|
+
format_trailing_comment, line_leading_indent, mark_comments_in_range_emitted,
|
|
18
|
+
reformat_chain_lines, strip_one_trailing_newline, FormatRule,
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
/// Rule for formatting method calls.
|
|
@@ -100,11 +103,25 @@ fn format_call(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) ->
|
|
|
100
103
|
.unwrap_or(false);
|
|
101
104
|
|
|
102
105
|
if !has_block {
|
|
103
|
-
// Simple call - use source extraction with chain reformatting
|
|
106
|
+
// Simple call - use source extraction with chain reformatting.
|
|
107
|
+
//
|
|
108
|
+
// A CallNode that carries a heredoc argument (e.g.
|
|
109
|
+
// `query(<<~SQL)\n …\nSQL`) reports its end_offset past the
|
|
110
|
+
// heredoc terminator's trailing newline. Leaving that newline in
|
|
111
|
+
// the emitted `Doc::Text` combines with the later `hardline + end`
|
|
112
|
+
// or inter-statement hardline to produce a spurious blank line.
|
|
113
|
+
// Strip at most one trailing newline so this doesn't happen; using
|
|
114
|
+
// the full `trim_end` here would instead eat a blank separator line
|
|
115
|
+
// that legitimately belongs between statements.
|
|
104
116
|
if let Some(source_text) = ctx.extract_source(node) {
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
let base_indent = line_leading_indent(ctx.source(), node.location.start_offset);
|
|
118
|
+
let reformatted = reformat_chain_lines(
|
|
119
|
+
source_text,
|
|
120
|
+
base_indent,
|
|
121
|
+
ctx.config().formatting.indent_width,
|
|
122
|
+
);
|
|
123
|
+
let trimmed = strip_one_trailing_newline(&reformatted);
|
|
124
|
+
docs.push(text(trimmed.to_string()));
|
|
108
125
|
}
|
|
109
126
|
|
|
110
127
|
// Mark comments in this range as emitted (they're in source extraction)
|
|
@@ -123,16 +140,26 @@ fn format_call(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) ->
|
|
|
123
140
|
let block_node = node.children.last().unwrap();
|
|
124
141
|
let block_style = detect_block_style(block_node, ctx);
|
|
125
142
|
|
|
126
|
-
// Emit the call part (receiver.method(args)) from source with chain
|
|
143
|
+
// Emit the call part (receiver.method(args)) from source with chain
|
|
144
|
+
// reformatting. Track whether reformatting actually fired so the block
|
|
145
|
+
// body can be re-aligned to match the chain's new depth.
|
|
127
146
|
let call_end_offset = block_node.location.start_offset;
|
|
128
|
-
if let Some(call_text) = ctx
|
|
147
|
+
let chain_reformatted = if let Some(call_text) = ctx
|
|
129
148
|
.source()
|
|
130
149
|
.get(node.location.start_offset..call_end_offset)
|
|
131
150
|
{
|
|
132
|
-
let
|
|
133
|
-
|
|
151
|
+
let base_indent = line_leading_indent(ctx.source(), node.location.start_offset);
|
|
152
|
+
let reformatted = reformat_chain_lines(
|
|
153
|
+
call_text.trim_end(),
|
|
154
|
+
base_indent,
|
|
155
|
+
ctx.config().formatting.indent_width,
|
|
156
|
+
);
|
|
157
|
+
let changed = matches!(reformatted, Cow::Owned(_));
|
|
134
158
|
docs.push(text(reformatted));
|
|
135
|
-
|
|
159
|
+
changed
|
|
160
|
+
} else {
|
|
161
|
+
false
|
|
162
|
+
};
|
|
136
163
|
|
|
137
164
|
// Mark comments in the call part (before block) as emitted
|
|
138
165
|
// This includes trailing comments that are part of the extracted source
|
|
@@ -142,16 +169,21 @@ fn format_call(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) ->
|
|
|
142
169
|
block_node.location.start_line,
|
|
143
170
|
);
|
|
144
171
|
|
|
145
|
-
// Format the block
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
// Format the block. When the receiver's chain was re-indented, the
|
|
173
|
+
// `do`-line ends up one level below `base_indent` instead of at
|
|
174
|
+
// `base_indent` itself, so the default `indent(body)` wrap inside the
|
|
175
|
+
// block formatter now places the body *at* the chain depth rather than
|
|
176
|
+
// one level below it (and the `end` keyword floats up to `base_indent`).
|
|
177
|
+
// Push both down with `Align` so the `do…end` body is indented relative
|
|
178
|
+
// to the chain's last line, matching what a human would write.
|
|
179
|
+
let block_doc = match block_style {
|
|
180
|
+
BlockStyle::DoEnd => format_do_end_block(block_node, ctx, registry)?,
|
|
181
|
+
BlockStyle::Braces => format_brace_block(block_node, ctx, registry)?,
|
|
182
|
+
};
|
|
183
|
+
if chain_reformatted {
|
|
184
|
+
docs.push(align(ctx.config().formatting.indent_width, block_doc));
|
|
185
|
+
} else {
|
|
186
|
+
docs.push(block_doc);
|
|
155
187
|
}
|
|
156
188
|
|
|
157
189
|
Ok(concat(docs))
|
|
@@ -201,9 +233,16 @@ fn format_do_end_block(
|
|
|
201
233
|
break;
|
|
202
234
|
}
|
|
203
235
|
NodeType::BeginNode => {
|
|
204
|
-
// Block with rescue/ensure
|
|
205
|
-
|
|
206
|
-
|
|
236
|
+
// Block with rescue/else/ensure needs the clause keywords at
|
|
237
|
+
// the block opener's indent level, not at the body indent.
|
|
238
|
+
if super::begin::is_implicit_begin_with_clauses(child, ctx) {
|
|
239
|
+
docs.push(super::begin::format_implicit_begin_body(
|
|
240
|
+
child, ctx, registry,
|
|
241
|
+
)?);
|
|
242
|
+
} else {
|
|
243
|
+
let body_doc = format_child(child, ctx, registry)?;
|
|
244
|
+
docs.push(indent(concat(vec![hardline(), body_doc])));
|
|
245
|
+
}
|
|
207
246
|
break;
|
|
208
247
|
}
|
|
209
248
|
_ => {
|
|
@@ -212,6 +251,23 @@ fn format_do_end_block(
|
|
|
212
251
|
}
|
|
213
252
|
}
|
|
214
253
|
|
|
254
|
+
// Emit any standalone comments between the last body statement and `end`.
|
|
255
|
+
//
|
|
256
|
+
// Without this the orphan comments inside a `do…end` block (e.g. the
|
|
257
|
+
// commented-out config stanzas in a generated `spec_helper.rb`) never
|
|
258
|
+
// get claimed by any `format_leading_comments` call, fall through to
|
|
259
|
+
// `format_remaining_comments` at the end of the file, and get emitted
|
|
260
|
+
// *after* the block's own `end` — producing `end# comment…` with no
|
|
261
|
+
// separator and dropping the body indent.
|
|
262
|
+
let comments_before_end = format_comments_before_end(
|
|
263
|
+
ctx,
|
|
264
|
+
block_node.location.start_line,
|
|
265
|
+
block_node.location.end_line,
|
|
266
|
+
);
|
|
267
|
+
if !comments_before_end.is_empty() {
|
|
268
|
+
docs.push(indent(comments_before_end));
|
|
269
|
+
}
|
|
270
|
+
|
|
215
271
|
// Emit 'end'
|
|
216
272
|
docs.push(hardline());
|
|
217
273
|
docs.push(text("end"));
|
|
@@ -10,8 +10,8 @@ use crate::error::Result;
|
|
|
10
10
|
use crate::format::context::FormatContext;
|
|
11
11
|
use crate::format::registry::RuleRegistry;
|
|
12
12
|
use crate::format::rule::{
|
|
13
|
-
format_leading_comments, format_trailing_comment,
|
|
14
|
-
reformat_chain_lines, FormatRule,
|
|
13
|
+
format_leading_comments, format_trailing_comment, line_leading_indent,
|
|
14
|
+
mark_comments_in_range_emitted, reformat_chain_lines, strip_one_trailing_newline, FormatRule,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/// Fallback rule that extracts source text directly.
|
|
@@ -36,11 +36,25 @@ impl FormatRule for FallbackRule {
|
|
|
36
36
|
docs.push(leading);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Extract source text with chain reformatting
|
|
39
|
+
// Extract source text with chain reformatting.
|
|
40
|
+
//
|
|
41
|
+
// Nodes such as ConstantWriteNode for `CONST = <<~HEREDOC ... HEREDOC`
|
|
42
|
+
// report an end_offset that sits past the heredoc terminator's
|
|
43
|
+
// newline. Preserving that newline verbatim combines with the
|
|
44
|
+
// surrounding `format_statements` hardline to produce a spurious
|
|
45
|
+
// blank line before the following statement or `end`. Strip at
|
|
46
|
+
// most one trailing newline (not all trailing whitespace, which
|
|
47
|
+
// could swallow an intentional blank line captured by the node's
|
|
48
|
+
// extent).
|
|
40
49
|
if let Some(source_text) = ctx.extract_source(node) {
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
let base_indent = line_leading_indent(ctx.source(), node.location.start_offset);
|
|
51
|
+
let reformatted = reformat_chain_lines(
|
|
52
|
+
source_text,
|
|
53
|
+
base_indent,
|
|
54
|
+
ctx.config().formatting.indent_width,
|
|
55
|
+
);
|
|
56
|
+
let trimmed = strip_one_trailing_newline(&reformatted);
|
|
57
|
+
docs.push(text(trimmed.to_string()));
|
|
44
58
|
|
|
45
59
|
// Mark any comments within this node's range as emitted
|
|
46
60
|
// (they are included in the source extraction)
|