From 79ca2d1bcccbf2e9efb1c54f3f883ea24a81268e Mon Sep 17 00:00:00 2001 From: Vladislav <4pex1oh@gmail.com> Date: Sat, 22 Nov 2025 14:26:03 +0700 Subject: [PATCH 1/6] feat(fmt): add format_conditions config option --- crates/config/src/fmt.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/config/src/fmt.rs b/crates/config/src/fmt.rs index 673b692fbe4fd..50ba0ec123fc2 100644 --- a/crates/config/src/fmt.rs +++ b/crates/config/src/fmt.rs @@ -44,6 +44,8 @@ pub struct FormatterConfig { pub prefer_compact: PreferCompact, /// Keep single imports on a single line even if they exceed line length. pub single_line_imports: bool, + /// Style of condition formatting in if statements + pub format_conditions: ConditionFormatStyle, } /// Style of integer types. @@ -223,6 +225,17 @@ impl PreferCompact { } } +/// Style of condition formatting in if statements +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ConditionFormatStyle { + /// Inline all conditions on a single line + #[default] + Inline, + /// Place each condition on a separate line + Multi, +} + /// Style of indent #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -254,6 +267,7 @@ impl Default for FormatterConfig { prefer_compact: PreferCompact::default(), docs_style: DocCommentStyle::default(), single_line_imports: false, + format_conditions: ConditionFormatStyle::default(), } } } From 1c333d6321e133e62d5d924dffb6c62949d25658 Mon Sep 17 00:00:00 2001 From: Vladislav <4pex1oh@gmail.com> Date: Sat, 22 Nov 2025 18:33:35 +0700 Subject: [PATCH 2/6] feat(fmt): multi-line condition formatting --- crates/fmt/src/state/mod.rs | 3 +++ crates/fmt/src/state/sol.rs | 48 ++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/crates/fmt/src/state/mod.rs b/crates/fmt/src/state/mod.rs index ada5eadd55bf0..b8dcbfd4eea01 100644 --- a/crates/fmt/src/state/mod.rs +++ b/crates/fmt/src/state/mod.rs @@ -132,6 +132,8 @@ pub(super) struct State<'sess, 'ast> { emit_or_revert: bool, // Whether inside a variable initialization expression, or not. var_init: bool, + // Whether inside an if condition expression, or not. + in_if_condition: bool, } impl std::ops::Deref for State<'_, '_> { @@ -226,6 +228,7 @@ impl<'sess> State<'sess, '_> { return_bin_expr: false, emit_or_revert: false, var_init: false, + in_if_condition: false, block_depth: 0, call_stack: CallStack::default(), } diff --git a/crates/fmt/src/state/sol.rs b/crates/fmt/src/state/sol.rs index 52fb7fe0172f1..071a94fabe292 100644 --- a/crates/fmt/src/state/sol.rs +++ b/crates/fmt/src/state/sol.rs @@ -1424,6 +1424,11 @@ impl<'ast> State<'_, 'ast> { let prev_chain = self.binary_expr; let is_chain = prev_chain.is_some_and(|prev| prev == bin_op.kind.group()); + // Check if we should use multi-line formatting for if conditions + let use_multi_line_conditions = self.in_if_condition + && matches!(self.config.format_conditions, config::ConditionFormatStyle::Multi) + && matches!(bin_op.kind.group(), BinOpGroup::Logical); + // Opening box if starting a new operator chain. if !is_chain { self.binary_expr = Some(bin_op.kind.group()); @@ -1437,7 +1442,21 @@ impl<'ast> State<'_, 'ast> { } else { self.ind }; - self.s.ibox(indent); + // Use consistent box for multi-line condition formatting to ensure all breaks happen + // together + if use_multi_line_conditions { + self.s.cbox(indent); + } else { + self.s.ibox(indent); + } + } + + // For multi-line condition formatting, break before the operator (except the first one) + if use_multi_line_conditions && is_chain { + // Break before the operator to start each condition on its own line + if !self.is_bol_or_only_ind() { + self.hardbreak(); + } } // Print LHS. @@ -1469,7 +1488,14 @@ impl<'ast> State<'_, 'ast> { self.word(bin_op.kind.to_str()); - if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) { + // For multi-line condition formatting, break after logical operators in if conditions + if use_multi_line_conditions { + // Force a hard break after logical operators in multi-line mode + if !self.is_bol_or_only_ind() { + self.hardbreak(); + } + self.s.offset(self.ind); + } else if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) { self.nbsp(); } } @@ -2301,14 +2327,30 @@ impl<'ast> State<'_, 'ast> { fn print_if_cond(&mut self, kw: &'static str, cond: &'ast ast::Expr<'ast>, pos_hi: BytePos) { self.print_word(kw); self.print_sep_unhandled(Separator::Nbsp); + + // Set flag for if condition formatting + let was_in_if_condition = self.in_if_condition; + self.in_if_condition = true; + + // Choose ListFormat based on config + let list_format = + if matches!(self.config.format_conditions, config::ConditionFormatStyle::Multi) { + ListFormat::consistent().break_cmnts().break_single(true) + } else { + ListFormat::compact().break_cmnts().break_single(is_binary_expr(&cond.kind)) + }; + self.print_tuple( std::slice::from_ref(cond), cond.span.lo(), pos_hi, Self::print_expr, get_span!(), - ListFormat::compact().break_cmnts().break_single(is_binary_expr(&cond.kind)), + list_format, ); + + // Reset flag + self.in_if_condition = was_in_if_condition; } fn print_emit_or_revert( From ab29062d84b5a14cb5a6e9fc9a56cef4c6e2c250 Mon Sep 17 00:00:00 2001 From: Vladislav <4pex1oh@gmail.com> Date: Sat, 22 Nov 2025 19:12:11 +0700 Subject: [PATCH 3/6] fix(tests): added missing parameter --- crates/forge/tests/cli/config.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 6af175ae15a6d..a2b346351c7a2 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -140,6 +140,7 @@ sort_imports = false pow_no_space = false prefer_compact = "all" single_line_imports = false +format_conditions = "inline" [lint] severity = [] @@ -1332,7 +1333,8 @@ forgetest_init!(test_default_config, |prj, cmd| { "sort_imports": false, "pow_no_space": false, "prefer_compact": "all", - "single_line_imports": false + "single_line_imports": false, + "format_conditions": "inline" }, "lint": { "severity": [], From 70c395f3bc5dd9a7ecc07c7c7e064a522e93c35b Mon Sep 17 00:00:00 2001 From: Vladislav <4pex1oh@gmail.com> Date: Thu, 27 Nov 2025 14:17:40 +0700 Subject: [PATCH 4/6] feat(fmt): multiline correction + tests + doc --- crates/fmt/README.md | 1 + crates/fmt/src/pp/convenience.rs | 11 +++ crates/fmt/src/state/sol.rs | 84 ++++++++++++------- .../fmt/testdata/ConditionFormatStyle/fmt.sol | 16 ++++ .../ConditionFormatStyle/inline.fmt.sol | 13 +++ .../ConditionFormatStyle/original.sol | 10 +++ crates/fmt/tests/formatter.rs | 1 + 7 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 crates/fmt/testdata/ConditionFormatStyle/fmt.sol create mode 100644 crates/fmt/testdata/ConditionFormatStyle/inline.fmt.sol create mode 100644 crates/fmt/testdata/ConditionFormatStyle/original.sol diff --git a/crates/fmt/README.md b/crates/fmt/README.md index 65f05d6c82847..6ac543643f90c 100644 --- a/crates/fmt/README.md +++ b/crates/fmt/README.md @@ -129,6 +129,7 @@ The formatter supports multiple configuration options defined in `foundry.toml`. | `sort_imports` | `false` | Sort import statements alphabetically in groups. A group is a set of imports separated by a newline. | | `pow_no_space` | `false` | Suppress spaces around the power operator (`**`). | | `single_line_imports` | `false` | Keep single imports on a single line, even if they exceed the line length limit. | +| `format_conditions` | `inline` | Style for formatting conditional expressions in control flow statements. Options: `inline`, `multi`. | > Check [`FormatterConfig`](../config/src/fmt.rs) for a more detailed explanation. diff --git a/crates/fmt/src/pp/convenience.rs b/crates/fmt/src/pp/convenience.rs index 65e66d5d42097..01b2b1fd59636 100644 --- a/crates/fmt/src/pp/convenience.rs +++ b/crates/fmt/src/pp/convenience.rs @@ -31,6 +31,17 @@ impl Printer { pub fn eof(mut self) -> String { self.scan_eof(); + // Normalize trailing newlines: ensure exactly one \n at the end + // This must happen AFTER scan_eof() to catch any newlines added during EOF-flush + // If there's no newline at all, add one + if !self.out.ends_with('\n') { + self.out.push('\n'); + } else { + // If there are multiple newlines, remove extras to leave exactly one + while self.out.ends_with("\n\n") { + self.out.pop(); + } + } self.out } diff --git a/crates/fmt/src/state/sol.rs b/crates/fmt/src/state/sol.rs index 071a94fabe292..573fe1e28e65e 100644 --- a/crates/fmt/src/state/sol.rs +++ b/crates/fmt/src/state/sol.rs @@ -1451,14 +1451,6 @@ impl<'ast> State<'_, 'ast> { } } - // For multi-line condition formatting, break before the operator (except the first one) - if use_multi_line_conditions && is_chain { - // Break before the operator to start each condition on its own line - if !self.is_bol_or_only_ind() { - self.hardbreak(); - } - } - // Print LHS. self.print_expr(lhs); @@ -1486,16 +1478,18 @@ impl<'ast> State<'_, 'ast> { } } + if use_multi_line_conditions && is_chain { + if !self.is_bol_or_only_ind() && !self.last_token_is_break() { + self.s.break_offset(SIZE_INFINITY as usize, self.ind); + } + } + self.word(bin_op.kind.to_str()); - // For multi-line condition formatting, break after logical operators in if conditions - if use_multi_line_conditions { - // Force a hard break after logical operators in multi-line mode - if !self.is_bol_or_only_ind() { - self.hardbreak(); - } - self.s.offset(self.ind); - } else if !self.config.pow_no_space || !matches!(bin_op.kind, ast::BinOpKind::Pow) { + if use_multi_line_conditions + || !self.config.pow_no_space + || !matches!(bin_op.kind, ast::BinOpKind::Pow) + { self.nbsp(); } } @@ -2332,22 +2326,50 @@ impl<'ast> State<'_, 'ast> { let was_in_if_condition = self.in_if_condition; self.in_if_condition = true; - // Choose ListFormat based on config - let list_format = - if matches!(self.config.format_conditions, config::ConditionFormatStyle::Multi) { - ListFormat::consistent().break_cmnts().break_single(true) - } else { - ListFormat::compact().break_cmnts().break_single(is_binary_expr(&cond.kind)) - }; + // For multi-line condition formatting, use a special formatting approach + if matches!(self.config.format_conditions, config::ConditionFormatStyle::Multi) { + // Print opening parenthesis + self.print_word("("); - self.print_tuple( - std::slice::from_ref(cond), - cond.span.lo(), - pos_hi, - Self::print_expr, - get_span!(), - list_format, - ); + // Open a box with indentation for the condition + self.s.cbox(self.ind); + + // Force a break after opening parenthesis + self.hardbreak(); + + // Print comments before condition + self.print_comments( + cond.span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ); + + // Print the condition (which will format multi-line for logical operators) + self.print_expr(cond); + + // Print trailing comments + self.print_trailing_comment(cond.span.hi(), Some(pos_hi)); + + self.end(); + + if !self.is_bol_or_only_ind() { + self.hardbreak(); + } + + self.print_word(")"); + } else { + // Choose ListFormat based on config for inline formatting + let list_format = + ListFormat::compact().break_cmnts().break_single(is_binary_expr(&cond.kind)); + + self.print_tuple( + std::slice::from_ref(cond), + cond.span.lo(), + pos_hi, + Self::print_expr, + get_span!(), + list_format, + ); + } // Reset flag self.in_if_condition = was_in_if_condition; diff --git a/crates/fmt/testdata/ConditionFormatStyle/fmt.sol b/crates/fmt/testdata/ConditionFormatStyle/fmt.sol new file mode 100644 index 0000000000000..1b49247072817 --- /dev/null +++ b/crates/fmt/testdata/ConditionFormatStyle/fmt.sol @@ -0,0 +1,16 @@ +// config: format_conditions = "multi" +contract TestConditionFormatting { + function testConditions() public { + uint256 newNumber = 5; + + if ( + newNumber % 2 == 0 + || newNumber % 2 == 1 + || newNumber != 0 + || newNumber != 1 + || newNumber != 2 + ) { + // do something + } + } +} diff --git a/crates/fmt/testdata/ConditionFormatStyle/inline.fmt.sol b/crates/fmt/testdata/ConditionFormatStyle/inline.fmt.sol new file mode 100644 index 0000000000000..6a9b5b7a641a3 --- /dev/null +++ b/crates/fmt/testdata/ConditionFormatStyle/inline.fmt.sol @@ -0,0 +1,13 @@ +// config: format_conditions = "inline" +contract TestConditionFormatting { + function testConditions() public { + uint256 newNumber = 5; + + if ( + newNumber % 2 == 0 || newNumber % 2 == 1 || newNumber != 0 + || newNumber != 1 || newNumber != 2 + ) { + // do something + } + } +} diff --git a/crates/fmt/testdata/ConditionFormatStyle/original.sol b/crates/fmt/testdata/ConditionFormatStyle/original.sol new file mode 100644 index 0000000000000..aeaa2ba3cea33 --- /dev/null +++ b/crates/fmt/testdata/ConditionFormatStyle/original.sol @@ -0,0 +1,10 @@ +contract TestConditionFormatting { + function testConditions() public { + uint256 newNumber = 5; + + if (newNumber % 2 == 0 || newNumber % 2 == 1 || newNumber != 0 || newNumber != 1 || newNumber != 2) { + // do something + } + } +} + diff --git a/crates/fmt/tests/formatter.rs b/crates/fmt/tests/formatter.rs index 953dc6b48dbba..70a0fbb06abc3 100644 --- a/crates/fmt/tests/formatter.rs +++ b/crates/fmt/tests/formatter.rs @@ -169,6 +169,7 @@ fmt_tests! { ArrayExpressions, BlockComments, BlockCommentsFunction, + ConditionFormatStyle, ConditionalOperatorExpression, ConstructorDefinition, ConstructorModifierStyle, From 0abf67b59668639e0c5c6cf9986d5daedddbd003 Mon Sep 17 00:00:00 2001 From: Vladislav <4pex1oh@gmail.com> Date: Thu, 27 Nov 2025 14:28:42 +0700 Subject: [PATCH 5/6] chore(fmt): clippy fix --- crates/fmt/src/state/sol.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/fmt/src/state/sol.rs b/crates/fmt/src/state/sol.rs index 573fe1e28e65e..64007be1f93be 100644 --- a/crates/fmt/src/state/sol.rs +++ b/crates/fmt/src/state/sol.rs @@ -1478,10 +1478,13 @@ impl<'ast> State<'_, 'ast> { } } - if use_multi_line_conditions && is_chain { - if !self.is_bol_or_only_ind() && !self.last_token_is_break() { - self.s.break_offset(SIZE_INFINITY as usize, self.ind); - } + if use_multi_line_conditions + && is_chain + && !self.is_bol_or_only_ind() + && !self.last_token_is_break() + { + // Break before operator with double indent (fixed offset, not cumulative) + self.s.break_offset(SIZE_INFINITY as usize, self.ind); } self.word(bin_op.kind.to_str()); From 38035ca84fb3ab47a8215b4968a471fc2bd2b128 Mon Sep 17 00:00:00 2001 From: Vladislav <4pex1oh@gmail.com> Date: Thu, 27 Nov 2025 14:56:53 +0700 Subject: [PATCH 6/6] chore(fmt): fix convenience formatting with \n --- crates/fmt/src/pp/convenience.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/fmt/src/pp/convenience.rs b/crates/fmt/src/pp/convenience.rs index 01b2b1fd59636..a8f22936a4bea 100644 --- a/crates/fmt/src/pp/convenience.rs +++ b/crates/fmt/src/pp/convenience.rs @@ -33,12 +33,20 @@ impl Printer { self.scan_eof(); // Normalize trailing newlines: ensure exactly one \n at the end // This must happen AFTER scan_eof() to catch any newlines added during EOF-flush - // If there's no newline at all, add one - if !self.out.ends_with('\n') { - self.out.push('\n'); + let has_content = !self.out.trim().is_empty(); + if has_content { + // If there's no newline at all, add one + if !self.out.ends_with('\n') { + self.out.push('\n'); + } else { + // If there are multiple newlines, remove extras to leave exactly one + while self.out.ends_with("\n\n") { + self.out.pop(); + } + } } else { - // If there are multiple newlines, remove extras to leave exactly one - while self.out.ends_with("\n\n") { + // For empty files, remove all trailing newlines + while self.out.ends_with('\n') { self.out.pop(); } }