Skip to content

Commit 63010f4

Browse files
authored
fix(fmt): always break consistently in calls with opts and args (#12359)
* fix(fmt): always break consistently in calls with opts and args * more tests * test: simplify
1 parent dc1d21b commit 63010f4

File tree

7 files changed

+288
-19
lines changed

7 files changed

+288
-19
lines changed

crates/fmt/src/state/common.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ impl<'ast> State<'_, 'ast> {
284284
values: &[T],
285285
mut get_span: S,
286286
format: ListFormat,
287+
manual_opening: bool,
287288
) -> bool
288289
where
289290
S: FnMut(&T) -> Span,
@@ -314,6 +315,14 @@ impl<'ast> State<'_, 'ast> {
314315
}
315316
};
316317

318+
// If manual opening flag is passed, we simply force the break, and skip the comment.
319+
// It will be dealt with when printing the item in the main loop of `commasep`.
320+
if manual_opening {
321+
self.hardbreak();
322+
self.s.offset(self.ind);
323+
return true;
324+
}
325+
317326
let cmnt_config = if format.with_delimiters {
318327
CommentConfig::skip_ws().mixed_no_break().mixed_prev_space()
319328
} else {
@@ -377,23 +386,37 @@ impl<'ast> State<'_, 'ast> {
377386
return;
378387
}
379388

380-
let first = get_span(&values[0]);
381-
// we can't simply check `peek_comment_before(pos_hi)` cause we would also account for
389+
// We can't simply check `peek_comment_before(pos_hi)` cause we would also account for
382390
// comments in the child expression, and those don't matter.
383-
let has_comments = self.peek_comment_before(first.lo()).is_some()
384-
|| self.peek_comment_between(first.hi(), pos_hi).is_some();
385-
let is_single_without_cmnts = values.len() == 1 && !format.break_single && !has_comments;
391+
let has_comments =
392+
// check for comments before the first element
393+
self.peek_comment_before(get_span(&values[0]).lo()).is_some() ||
394+
// check for comments between elements
395+
values.windows(2).any(|w| self.peek_comment_between(get_span(&w[0]).hi(), get_span(&w[1]).lo()).is_some()) ||
396+
// check for comments after the last element
397+
self.peek_comment_between(get_span(values.last().unwrap()).hi(), pos_hi).is_some();
398+
399+
// For calls with opts and args, which should break consistently, we need to skip the
400+
// wrapping cbox to prioritize call args breaking before the call opts. Because of that, we
401+
// must manually offset the breaks between args, so that they are properly indented.
402+
let manual_opening =
403+
format.is_consistent() && !format.with_delimiters && self.call_with_opts_and_args;
404+
// When there are comments, we can preserve the cbox, as they will make it break
405+
let manual_offset = !has_comments && manual_opening;
386406

407+
let is_single_without_cmnts = values.len() == 1 && !format.break_single && !has_comments;
387408
let skip_first_break = if format.with_delimiters || format.is_inline() {
388409
self.s.cbox(if format.no_ind { 0 } else { self.ind });
389410
if is_single_without_cmnts {
390411
true
391412
} else {
392-
self.commasep_opening_logic(values, &mut get_span, format)
413+
self.commasep_opening_logic(values, &mut get_span, format, manual_opening)
393414
}
394415
} else {
395-
let res = self.commasep_opening_logic(values, &mut get_span, format);
396-
self.s.cbox(if format.no_ind { 0 } else { self.ind });
416+
let res = self.commasep_opening_logic(values, &mut get_span, format, manual_opening);
417+
if !manual_offset {
418+
self.s.cbox(if format.no_ind { 0 } else { self.ind });
419+
}
397420
res
398421
};
399422

@@ -403,6 +426,9 @@ impl<'ast> State<'_, 'ast> {
403426
self.nbsp();
404427
} else if !skip_first_break && !format.is_inline() {
405428
format.print_break(true, values.len(), &mut self.s);
429+
if manual_offset {
430+
self.s.offset(self.ind);
431+
}
406432
}
407433

408434
if format.is_compact() && !(format.breaks_with_comments() && has_comments) {
@@ -485,6 +511,9 @@ impl<'ast> State<'_, 'ast> {
485511
&& !next_span.is_dummy()
486512
{
487513
format.print_break(false, values.len(), &mut self.s);
514+
if manual_offset {
515+
self.s.offset(self.ind);
516+
}
488517
}
489518
}
490519

@@ -507,7 +536,9 @@ impl<'ast> State<'_, 'ast> {
507536
self.word(sym);
508537
}
509538

510-
self.end();
539+
if !manual_offset {
540+
self.end();
541+
}
511542
self.cursor.advance_to(pos_hi, true);
512543

513544
if last_delimiter_break {
@@ -800,6 +831,10 @@ impl ListFormat {
800831
if let ListFormatKind::Yul { sym_post, .. } = self.kind { sym_post } else { None }
801832
}
802833

834+
pub(crate) fn is_consistent(&self) -> bool {
835+
matches!(self.kind, ListFormatKind::Consistent)
836+
}
837+
803838
pub(crate) fn is_compact(&self) -> bool {
804839
matches!(self.kind, ListFormatKind::Compact)
805840
}

crates/fmt/src/state/sol.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,8 +1308,15 @@ impl<'ast> State<'_, 'ast> {
13081308
self.call_with_opts_and_args = cache;
13091309
}
13101310
ast::ExprKind::CallOptions(expr, named_args) => {
1311+
// the flag is only meant to be used to format the call args
1312+
let cache = self.call_with_opts_and_args;
1313+
self.call_with_opts_and_args = false;
1314+
13111315
self.print_expr(expr);
13121316
self.print_named_args(named_args, span.hi());
1317+
1318+
// restore cached value
1319+
self.call_with_opts_and_args = cache;
13131320
}
13141321
ast::ExprKind::Delete(expr) => {
13151322
self.word("delete ");
@@ -1672,7 +1679,7 @@ impl<'ast> State<'_, 'ast> {
16721679

16731680
let callee_fits_line = self.space_left() > callee_size + 1;
16741681
let total_fits_line = self.space_left() > expr_size + member_or_args.size() + 2;
1675-
let no_mixed_comment =
1682+
let no_cmnt_or_mixed =
16761683
self.peek_comment_before(child_expr.span.hi()).is_none_or(|c| c.style.is_mixed());
16771684

16781685
// If call with options, add an extra box to prioritize breaking the call args
@@ -1682,7 +1689,7 @@ impl<'ast> State<'_, 'ast> {
16821689
}
16831690

16841691
if !is_call_chain(&child_expr.kind, true)
1685-
&& no_mixed_comment
1692+
&& (no_cmnt_or_mixed || matches!(&child_expr.kind, ast::ExprKind::CallOptions(..)))
16861693
&& callee_fits_line
16871694
&& (member_depth(0, child_expr) < 2
16881695
// calls with cmnts between the args always break

crates/fmt/testdata/ReprosCalls/110.fmt.sol

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// config: line_length = 110
2-
function test() public {
2+
function repros() public {
33
require(
44
keccak256(abi.encodePacked("this is a long string"))
55
== keccak256(abi.encodePacked("some other long string")),
@@ -86,7 +86,7 @@ function returnLongBinaryOp() returns (bytes32) {
8686
);
8787
}
8888

89-
contract Orchestrator {
89+
contract Repros {
9090
function test() public {
9191
uint256 globalBuyAmount =
9292
Take.take(state, notes, uint32(IPoolManager.take.selector), recipient, minBuyAmount);
@@ -145,4 +145,22 @@ contract Orchestrator {
145145
{
146146
a = 1;
147147
}
148+
149+
// https://github.com/foundry-rs/foundry/issues/12324
150+
function test_longCallWithOpts() {
151+
flow.withdraw{value: FLOW_MIN_FEE_WEI}({
152+
streamId: defaultStreamId, to: users.eve, amount: WITHDRAW_AMOUNT_6D
153+
});
154+
flow.withdraw{
155+
value: FLOW_MIN_FEE_WEI /* cmnt */
156+
}({
157+
streamId: defaultStreamId,
158+
to: users.eve,
159+
/* cmnt */
160+
amount: WITHDRAW_AMOUNT_6D
161+
});
162+
flow.withdraw{value: FLOW_MIN_FEE_WEI}({ // cmnt
163+
streamId: defaultStreamId, to: users.eve, amount: WITHDRAW_AMOUNT_6D
164+
});
165+
}
148166
}

crates/fmt/testdata/ReprosCalls/120.fmt.sol

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// config: line_length = 120
22
// config: bracket_spacing = true
3-
function test() public {
3+
function repros() public {
44
require(
55
keccak256(abi.encodePacked("this is a long string")) == keccak256(abi.encodePacked("some other long string")),
66
"string mismatch"
@@ -79,7 +79,7 @@ function returnLongBinaryOp() returns (bytes32) {
7979
bytes32(uint256(Feature.unwrap(feature)) << 128 | uint256(block.chainid) << 64 | uint256(Nonce.unwrap(nonce)));
8080
}
8181

82-
contract Orchestrator {
82+
contract Repros {
8383
function test() public {
8484
uint256 globalBuyAmount = Take.take(state, notes, uint32(IPoolManager.take.selector), recipient, minBuyAmount);
8585
uint256 globalBuyAmount = Take.take(state, notes, uint32(IPoolManager.take.selector), recipient, minBuyAmount);
@@ -131,4 +131,22 @@ contract Orchestrator {
131131
function test_ffi_fuzz_addLiquidity_defaultPool(IPoolManager.ModifyLiquidityParams memory paramSeed) public {
132132
a = 1;
133133
}
134+
135+
// https://github.com/foundry-rs/foundry/issues/12324
136+
function test_longCallWithOpts() {
137+
flow.withdraw{ value: FLOW_MIN_FEE_WEI }({
138+
streamId: defaultStreamId, to: users.eve, amount: WITHDRAW_AMOUNT_6D
139+
});
140+
flow.withdraw{
141+
value: FLOW_MIN_FEE_WEI /* cmnt */
142+
}({
143+
streamId: defaultStreamId,
144+
to: users.eve,
145+
/* cmnt */
146+
amount: WITHDRAW_AMOUNT_6D
147+
});
148+
flow.withdraw{ value: FLOW_MIN_FEE_WEI }({ // cmnt
149+
streamId: defaultStreamId, to: users.eve, amount: WITHDRAW_AMOUNT_6D
150+
});
151+
}
134152
}

crates/fmt/testdata/ReprosCalls/80.fmt.sol

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// config: line_length = 80
2-
function test() public {
2+
function repros() public {
33
require(
44
keccak256(abi.encodePacked("this is a long string"))
55
== keccak256(abi.encodePacked("some other long string")),
@@ -119,7 +119,7 @@ function returnLongBinaryOp() returns (bytes32) {
119119
);
120120
}
121121

122-
contract Orchestrator {
122+
contract Repros {
123123
function test() public {
124124
uint256 globalBuyAmount = Take.take(
125125
state,
@@ -205,4 +205,22 @@ contract Orchestrator {
205205
) public {
206206
a = 1;
207207
}
208+
209+
// https://github.com/foundry-rs/foundry/issues/12324
210+
function test_longCallWithOpts() {
211+
flow.withdraw{value: FLOW_MIN_FEE_WEI}({
212+
streamId: defaultStreamId, to: users.eve, amount: WITHDRAW_AMOUNT_6D
213+
});
214+
flow.withdraw{
215+
value: FLOW_MIN_FEE_WEI /* cmnt */
216+
}({
217+
streamId: defaultStreamId,
218+
to: users.eve,
219+
/* cmnt */
220+
amount: WITHDRAW_AMOUNT_6D
221+
});
222+
flow.withdraw{value: FLOW_MIN_FEE_WEI}({ // cmnt
223+
streamId: defaultStreamId, to: users.eve, amount: WITHDRAW_AMOUNT_6D
224+
});
225+
}
208226
}

0 commit comments

Comments
 (0)