Skip to content

Commit 3cc697c

Browse files
authored
Fix multilining of call writes (#756)
* Fix multiline CSends for Ripper * Fix multiline CSends for Prism * Fixture
1 parent 8a06900 commit 3cc697c

File tree

5 files changed

+119
-46
lines changed

5 files changed

+119
-46
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
v2_request
2+
.additional_data_private
3+
.sub_elements
4+
.specific_collect_pointer = Constant::Thing
5+
6+
v2_request.additional_data_private.sub_elements.specific_collect_pointer.lets_make_this_quite_a_bit_longer = Constant::Thing
7+
8+
v2_request.additional_data_private.sub_elements.specific_collect_pointer.lets_make_this_quite_a_bit_longer.over_120_chars_now = Constant::Thing
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
v2_request
2+
.additional_data_private
3+
.sub_elements
4+
.specific_collect_pointer = Constant::Thing
5+
6+
v2_request.additional_data_private.sub_elements.specific_collect_pointer.lets_make_this_quite_a_bit_longer = Constant::Thing
7+
8+
v2_request
9+
.additional_data_private
10+
.sub_elements
11+
.specific_collect_pointer
12+
.lets_make_this_quite_a_bit_longer
13+
.over_120_chars_now = Constant::Thing

librubyfmt/src/format.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,13 +1590,17 @@ pub fn format_field(ps: &mut ParserState, f: Field) {
15901590
ps.emit_indent();
15911591
}
15921592

1593+
let Field(_, expr, dot, ident_or_const) = f;
1594+
1595+
// Format as a call chain for proper multiline handling
1596+
let mut chain = (*expr).into_call_chain();
1597+
chain.push(CallChainElement::DotTypeOrOp(dot));
1598+
chain.push(CallChainElement::IdentOrOpOrKeywordOrConst(
1599+
ident_or_const.into_ident_or_op_or_keyword_or_const(),
1600+
));
1601+
15931602
ps.with_start_of_line(false, |ps| {
1594-
format_expression(ps, *f.1);
1595-
format_dot(ps, f.2);
1596-
match f.3 {
1597-
IdentOrConst::Const(c) => format_const(ps, c),
1598-
IdentOrConst::Ident(i) => format_ident(ps, i),
1599-
}
1603+
format_call_chain(ps, chain, None);
16001604
});
16011605

16021606
if ps.at_start_of_line() {

librubyfmt/src/format_prism.rs

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ pub fn format_node<'src>(ps: &mut ParserState<'src>, node: prism::Node<'src>) {
8686

8787
if is_last_call_in_chain && has_block {
8888
ps.breakable_call_chain_of(MultilineHandling::Prism(false), |ps| {
89-
format_call_node(ps, call_node, false, is_last_call_in_chain);
89+
format_call_node(ps, call_node, false, is_last_call_in_chain, false);
9090
});
9191
} else {
92-
format_call_node(ps, call_node, false, is_last_call_in_chain)
92+
format_call_node(ps, call_node, false, is_last_call_in_chain, false)
9393
}
9494
}
9595
Node::CallOperatorWriteNode { .. } => {
@@ -1711,6 +1711,7 @@ fn format_call_node<'src>(
17111711
call_node: prism::CallNode<'src>,
17121712
skip_receiver: bool,
17131713
is_final_call_in_chain: bool,
1714+
skip_attr_write_value: bool,
17141715
) {
17151716
let method_name = const_to_str(call_node.name());
17161717
let end_offset = call_node.location().end_offset();
@@ -1785,13 +1786,15 @@ fn format_call_node<'src>(
17851786
);
17861787
ps.with_start_of_line(false, |ps| format_node(ps, last_arg));
17871788
} else if call_node.is_attribute_write() {
1788-
ps.emit_ident(" = ");
1789-
let value = arguments
1790-
.arguments()
1791-
.iter()
1792-
.next()
1793-
.expect("Attribute writes must have a value");
1794-
ps.with_start_of_line(false, |ps| format_node(ps, value));
1789+
if !skip_attr_write_value {
1790+
ps.emit_ident(" = ");
1791+
let value = arguments
1792+
.arguments()
1793+
.iter()
1794+
.next()
1795+
.expect("Attribute writes must have a value");
1796+
ps.with_start_of_line(false, |ps| format_node(ps, value));
1797+
}
17951798
} else {
17961799
let should_use_parens = use_parens_for_call_node(
17971800
ps,
@@ -2108,41 +2111,66 @@ fn format_call_chain<'src>(ps: &mut ParserState<'src>, call_node: ruby_prism::Ca
21082111
ps.with_start_of_line(false, |ps| {
21092112
let segments = split_node_into_call_chains(call_node.as_node());
21102113

2111-
let is_attribute_write = segments
2112-
.last()
2113-
.and_then(|seg| seg.last())
2114-
.and_then(|n| n.as_call_node())
2115-
.map(|c| c.is_attribute_write())
2116-
.unwrap_or(false);
2117-
2118-
format_call_chain_segments(ps, segments, is_attribute_write);
2114+
format_call_chain_segments(ps, segments);
21192115
});
21202116
}
21212117

21222118
fn format_call_chain_segments<'src>(
21232119
ps: &mut ParserState<'src>,
21242120
mut segments: Vec<Vec<prism::Node<'src>>>,
2125-
is_attr_write: bool,
21262121
) {
21272122
if let Some(current) = segments.pop() {
21282123
let has_inner = !segments.is_empty();
21292124

21302125
let (chain_elements, trailing_arefs) = extract_trailing_arefs(current);
21312126

2127+
// Render the right-hand side of the write in its own breakable, so that each side can
2128+
// break independently of each other
2129+
let trailing_attr_write_value = chain_elements.last().and_then(|last| {
2130+
last.as_call_node().and_then(|call| {
2131+
let method_name = const_to_str(call.name());
2132+
// arefs are handled in format_call_node
2133+
if call.is_attribute_write() && method_name != "[]=" {
2134+
call.arguments().and_then(|args| {
2135+
debug_assert!(
2136+
args.arguments().len() == 1,
2137+
"Expected attr_write to have exactly one argument"
2138+
);
2139+
2140+
args.arguments().iter().next()
2141+
})
2142+
} else {
2143+
None
2144+
}
2145+
})
2146+
});
2147+
21322148
let is_user_multilined = call_chain_elements_are_user_multilined(ps, &chain_elements);
21332149

21342150
ps.breakable_call_chain_of(MultilineHandling::Prism(is_user_multilined), |ps| {
21352151
// Recurse and format previous segments inside this breakable
2136-
// Inner segments are never attribute writes
2137-
format_call_chain_segments(ps, segments, false);
2152+
format_call_chain_segments(ps, segments);
21382153

2139-
format_call_body(ps, chain_elements, is_attr_write, has_inner);
2154+
format_call_body(
2155+
ps,
2156+
chain_elements,
2157+
has_inner,
2158+
trailing_attr_write_value.is_some(),
2159+
);
21402160
});
21412161

2162+
// Attr_write values are formatted after the breakable so they break independently
2163+
if let Some(value) = trailing_attr_write_value {
2164+
ps.emit_space();
2165+
ps.emit_op("=".to_string());
2166+
ps.emit_space();
2167+
ps.with_start_of_line(false, |ps| format_node(ps, value));
2168+
}
2169+
21422170
// Trailing arefs are formatted after the breakable
21432171
for aref in trailing_arefs {
21442172
let aref = aref.as_call_node().unwrap();
2145-
format_call_node(ps, aref, true, false);
2173+
format_call_node(ps, aref, true, false, false);
21462174
}
21472175
}
21482176
}
@@ -2173,8 +2201,8 @@ fn extract_trailing_arefs<'src>(
21732201
fn format_call_body<'src>(
21742202
ps: &mut ParserState<'src>,
21752203
mut call_chain_elements: Vec<prism::Node<'src>>,
2176-
is_attr_write: bool,
21772204
is_continuation: bool,
2205+
skip_final_attr_write_value: bool,
21782206
) {
21792207
if call_chain_elements.is_empty() {
21802208
return;
@@ -2196,7 +2224,7 @@ fn format_call_body<'src>(
21962224
&& call_node.call_operator_loc().is_none()
21972225
{
21982226
call_chain_elements.remove(0);
2199-
format_call_node(ps, call_node, true, false);
2227+
format_call_node(ps, call_node, true, false, false);
22002228
continue;
22012229
}
22022230
break;
@@ -2209,16 +2237,15 @@ fn format_call_body<'src>(
22092237
ps.render_heredocs(true);
22102238
}
22112239

2212-
// Attribute writes like `self.foo = bar` should have both sides
2213-
// rendered separately so they can break independently
2240+
// Attribute writes like `self.foo = bar` format the LHS without call chain indent
2241+
// since there's only one element.
22142242
if !is_continuation
22152243
&& call_chain_elements.len() == 1
22162244
&& let Some(attr_write) = call_chain_elements
22172245
.first()
22182246
.and_then(|n| n.as_call_node())
22192247
.filter(|c| c.is_attribute_write())
22202248
{
2221-
// Format `.attr_name = ` directly without call chain indent
22222249
let call_operator = attr_write.call_operator_loc().map(|loc| loc_to_str(loc));
22232250
if let Some(call_operator) = call_operator {
22242251
match call_operator {
@@ -2232,14 +2259,9 @@ fn format_call_body<'src>(
22322259
ps.at_offset(start_loc_for_call_node_in_chain(&attr_write));
22332260
ps.shift_comments();
22342261

2235-
// Format just the attribute name and ` = `, then the value separately
2236-
format_call_node(ps, attr_write, true, true);
2262+
format_call_node(ps, attr_write, true, true, skip_final_attr_write_value);
22372263
} else if !call_chain_elements.is_empty() {
2238-
// attr_writes are not wrapped in a breakable, so they
2239-
// cannot emit abstract token types
2240-
if !is_attr_write {
2241-
ps.start_indent_for_call_chain();
2242-
}
2264+
ps.start_indent_for_call_chain();
22432265

22442266
ps.with_start_of_line(false, |ps| {
22452267
let call_chain_element_count = call_chain_elements.len();
@@ -2251,7 +2273,7 @@ fn format_call_body<'src>(
22512273
// it may be None in the case of arefs, e.g. foo[bar]
22522274
let call_operator = element.call_operator_loc().map(|loc| loc_to_str(loc));
22532275
if let Some(call_operator) = call_operator {
2254-
if call_operator != "::" && !is_attr_write {
2276+
if call_operator != "::" {
22552277
ps.emit_collapsing_newline();
22562278
ps.emit_soft_indent();
22572279
}
@@ -2270,12 +2292,12 @@ fn format_call_body<'src>(
22702292
ps.at_offset(start_loc_for_call_node_in_chain(&element));
22712293
ps.shift_comments();
22722294

2273-
format_call_node(ps, element, true, is_final_call);
2295+
let skip_value =
2296+
is_final_call && skip_final_attr_write_value && element.is_attribute_write();
2297+
format_call_node(ps, element, true, is_final_call, skip_value);
22742298
}
22752299
});
2276-
if !is_attr_write {
2277-
ps.end_indent_for_call_chain();
2278-
}
2300+
ps.end_indent_for_call_chain();
22792301
}
22802302
}
22812303

@@ -4207,7 +4229,7 @@ fn format_match_write_node<'src>(
42074229
ps: &mut ParserState<'src>,
42084230
match_write_node: prism::MatchWriteNode<'src>,
42094231
) {
4210-
format_call_node(ps, match_write_node.call(), false, true);
4232+
format_call_node(ps, match_write_node.call(), false, true, false);
42114233
}
42124234

42134235
fn format_multi_target_node<'src>(

librubyfmt/src/ripper_tree_types.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,32 @@ impl Expression {
299299
Expression::AnonBlockArg(AnonBlockArg(.., line_start)) => Some(*line_start),
300300
}
301301
}
302+
303+
pub fn into_call_chain(self) -> Vec<CallChainElement> {
304+
match self {
305+
Expression::MethodCall(MethodCall(_, mut chain, method, _, args, start_end)) => {
306+
chain.extend([
307+
CallChainElement::IdentOrOpOrKeywordOrConst(method),
308+
CallChainElement::ArgsAddStarOrExpressionListOrArgsForward(args, start_end),
309+
]);
310+
chain
311+
}
312+
Expression::Call(c) => CallLeft::Call(c).into_call_chain(),
313+
Expression::MethodAddArg(m) => CallLeft::MethodAddArg(m).into_call_chain(),
314+
Expression::MethodAddBlock(m) => CallLeft::MethodAddBlock(m).into_call_chain(),
315+
Expression::VarRef(v) => CallLeft::VarRef(v).into_call_chain(),
316+
Expression::VCall(v) => CallLeft::VCall(v).into_call_chain(),
317+
Expression::Paren(p) => CallLeft::Paren(p).into_call_chain(),
318+
Expression::Command(c) => CallLeft::Command(c).into_call_chain(),
319+
Expression::CommandCall(c) => CallLeft::CommandCall(c).into_call_chain(),
320+
Expression::Super(s) => CallLeft::Super(s).into_call_chain(),
321+
Expression::ZSuper(z) => CallLeft::ZSuper(z).into_call_chain(),
322+
Expression::Next(n) => CallLeft::Next(n).into_call_chain(),
323+
Expression::Yield(y) => CallLeft::Yield(y).into_call_chain(),
324+
Expression::Yield0(y) => CallLeft::Yield0(y).into_call_chain(),
325+
other => vec![CallChainElement::Expression(Box::new(other))],
326+
}
327+
}
302328
}
303329

304330
def_tag!(mlhs_tag, "mlhs");

0 commit comments

Comments
 (0)