Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/config/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -254,6 +267,7 @@ impl Default for FormatterConfig {
prefer_compact: PreferCompact::default(),
docs_style: DocCommentStyle::default(),
single_line_imports: false,
format_conditions: ConditionFormatStyle::default(),
}
}
}
1 change: 1 addition & 0 deletions crates/fmt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
19 changes: 19 additions & 0 deletions crates/fmt/src/pp/convenience.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ 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
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 {
// For empty files, remove all trailing newlines
while self.out.ends_with('\n') {
self.out.pop();
}
}
self.out
}

Expand Down
3 changes: 3 additions & 0 deletions crates/fmt/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<'_, '_> {
Expand Down Expand Up @@ -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(),
}
Expand Down
87 changes: 77 additions & 10 deletions crates/fmt/src/state/sol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -1437,7 +1442,13 @@ 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);
}
}

// Print LHS.
Expand Down Expand Up @@ -1467,9 +1478,21 @@ impl<'ast> State<'_, 'ast> {
}
}

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());

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();
}
}
Expand Down Expand Up @@ -2301,14 +2324,58 @@ 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);
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)),
);

// Set flag for if condition formatting
let was_in_if_condition = self.in_if_condition;
self.in_if_condition = true;

// 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("(");

// 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;
}

fn print_emit_or_revert(
Expand Down
16 changes: 16 additions & 0 deletions crates/fmt/testdata/ConditionFormatStyle/fmt.sol
Original file line number Diff line number Diff line change
@@ -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
}
}
}
13 changes: 13 additions & 0 deletions crates/fmt/testdata/ConditionFormatStyle/inline.fmt.sol
Original file line number Diff line number Diff line change
@@ -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
}
}
}
10 changes: 10 additions & 0 deletions crates/fmt/testdata/ConditionFormatStyle/original.sol
Original file line number Diff line number Diff line change
@@ -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
}
}
}

1 change: 1 addition & 0 deletions crates/fmt/tests/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ fmt_tests! {
ArrayExpressions,
BlockComments,
BlockCommentsFunction,
ConditionFormatStyle,
ConditionalOperatorExpression,
ConstructorDefinition,
ConstructorModifierStyle,
Expand Down
4 changes: 3 additions & 1 deletion crates/forge/tests/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ sort_imports = false
pow_no_space = false
prefer_compact = "all"
single_line_imports = false
format_conditions = "inline"

[lint]
severity = []
Expand Down Expand Up @@ -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": [],
Expand Down
Loading