diff --git a/Cargo.lock b/Cargo.lock index 6058d159cddf4..1fb104b622335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,9 +133,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6c2905bafc2df7ccd32ca3af13f0b0d82f2e2ff9dfbeb12196c0d978d5c0deb" +checksum = "3fdff496dd4e98a81f4861e66f7eaf5f2488971848bb42d9c892f871730245c8" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -283,9 +283,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2acb6637a9c0e1cdf8971e0ced8f3fa34c04c5e9dccf6bb184f6a64fe0e37d8" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -378,9 +378,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b77f7d5e60ad8ae6bd2200b8097919712a07a6db622a4b201e7ead6166f02e5" +checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" dependencies = [ "alloy-rlp", "arbitrary", @@ -776,9 +776,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c84c3637bee9b5c4a4d2b93360ee16553d299c3b932712353caf1cea76d0e6" +checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -790,9 +790,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a882aa4e1790063362434b9b40d358942b188477ac1c44cfb8a52816ffc0cc17" +checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -809,9 +809,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5772107f9bb265d8d8c86e0733937bb20d0857ea5425b1b6ddf51a9804042" +checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" dependencies = [ "alloy-json-abi", "const-hex", @@ -827,9 +827,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e188b939aa4793edfaaa099cb1be4e620036a775b4bdf24fdc56f1cd6fd45890" +checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" dependencies = [ "serde", "winnow", @@ -837,9 +837,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c8a9a909872097caffc05df134e5ef2253a1cdb56d3a9cf0052a042ac763f9" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -1070,7 +1070,7 @@ dependencies = [ [[package]] name = "anvil" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-consensus", @@ -1136,7 +1136,7 @@ dependencies = [ [[package]] name = "anvil-core" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -1161,7 +1161,7 @@ dependencies = [ [[package]] name = "anvil-rpc" -version = "1.4.1" +version = "1.4.2" dependencies = [ "serde", "serde_json", @@ -1169,7 +1169,7 @@ dependencies = [ [[package]] name = "anvil-server" -version = "1.4.1" +version = "1.4.2" dependencies = [ "anvil-rpc", "async-trait", @@ -2538,7 +2538,7 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cast" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-consensus", @@ -2642,7 +2642,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chisel" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4036,7 +4036,7 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "forge" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-dyn-abi", @@ -4117,7 +4117,7 @@ dependencies = [ [[package]] name = "forge-doc" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-primitives", "derive_more", @@ -4141,7 +4141,7 @@ dependencies = [ [[package]] name = "forge-fmt" -version = "1.4.1" +version = "1.4.2" dependencies = [ "foundry-common", "foundry-config", @@ -4157,7 +4157,7 @@ dependencies = [ [[package]] name = "forge-lint" -version = "1.4.1" +version = "1.4.2" dependencies = [ "eyre", "foundry-common", @@ -4171,7 +4171,7 @@ dependencies = [ [[package]] name = "forge-script" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4215,7 +4215,7 @@ dependencies = [ [[package]] name = "forge-script-sequence" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-network", "alloy-primitives", @@ -4231,7 +4231,7 @@ dependencies = [ [[package]] name = "forge-sol-macro-gen" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -4246,7 +4246,7 @@ dependencies = [ [[package]] name = "forge-verify" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4326,7 +4326,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4377,7 +4377,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes-spec" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-sol-types", "foundry-macros", @@ -4388,7 +4388,7 @@ dependencies = [ [[package]] name = "foundry-cli" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-dyn-abi", @@ -4436,7 +4436,7 @@ dependencies = [ [[package]] name = "foundry-common" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4492,7 +4492,7 @@ dependencies = [ [[package]] name = "foundry-common-fmt" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -4613,7 +4613,7 @@ dependencies = [ [[package]] name = "foundry-config" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-primitives", @@ -4653,7 +4653,7 @@ dependencies = [ [[package]] name = "foundry-debugger" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-primitives", "crossterm 0.29.0", @@ -4671,7 +4671,7 @@ dependencies = [ [[package]] name = "foundry-evm" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-dyn-abi", "alloy-evm", @@ -4703,7 +4703,7 @@ dependencies = [ [[package]] name = "foundry-evm-abi" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -4715,7 +4715,7 @@ dependencies = [ [[package]] name = "foundry-evm-core" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4755,7 +4755,7 @@ dependencies = [ [[package]] name = "foundry-evm-coverage" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-primitives", "eyre", @@ -4771,7 +4771,7 @@ dependencies = [ [[package]] name = "foundry-evm-fuzz" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4795,7 +4795,7 @@ dependencies = [ [[package]] name = "foundry-evm-networks" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-chains", "alloy-evm", @@ -4807,7 +4807,7 @@ dependencies = [ [[package]] name = "foundry-evm-traces" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4861,7 +4861,7 @@ dependencies = [ [[package]] name = "foundry-linking" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-primitives", "foundry-compilers", @@ -4872,7 +4872,7 @@ dependencies = [ [[package]] name = "foundry-macros" -version = "1.4.1" +version = "1.4.2" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -4896,7 +4896,7 @@ dependencies = [ [[package]] name = "foundry-test-utils" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-primitives", "alloy-provider", @@ -4923,7 +4923,7 @@ dependencies = [ [[package]] name = "foundry-wallets" -version = "1.4.1" +version = "1.4.2" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -9601,9 +9601,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2375c17f6067adc651d8c2c51658019cef32edfff4a982adaf1d7fd1c039f08b" +checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" dependencies = [ "paste", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index e2f0c260ffaae..2aeee79605395 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.4.1" +version = "1.4.2" edition = "2024" # Remember to update clippy.toml as well rust-version = "1.89" @@ -249,7 +249,7 @@ alloy-hardforks = { version = "0.3.2", default-features = false } alloy-op-hardforks = { version = "0.3.2", default-features = false } ## alloy-core -alloy-dyn-abi = "1.3.1" +alloy-dyn-abi = "1.4.1" alloy-json-abi = "1.3.0" alloy-primitives = { version = "1.3.1", features = [ "getrandom", diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 1d3e35ff45cbf..b0644896db124 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -774,7 +774,7 @@ impl EthApi { if let Some(fork) = self.get_fork() { // check if the number predates the fork, if in fork mode if let BlockRequest::Number(number) = self.block_request(block_number).await? - && fork.predates_fork(number) + && fork.predates_fork_inclusive(number) { // if this predates the fork we need to fetch balance, nonce, code individually // because the provider might not support this endpoint diff --git a/crates/anvil/tests/it/fork.rs b/crates/anvil/tests/it/fork.rs index c9d9b2c80b8fc..885bb046d8148 100644 --- a/crates/anvil/tests/it/fork.rs +++ b/crates/anvil/tests/it/fork.rs @@ -1638,6 +1638,22 @@ async fn test_fork_get_account_info() { code: Default::default(), } ); + + // Check account info at block number, see https://github.com/foundry-rs/foundry/issues/12072 + let info = provider + .get_account_info(address!("0x19e53a7397bE5AA7908fE9eA991B03710bdC74Fd")) + // predates fork + .number(BLOCK_NUMBER) + .await + .unwrap(); + assert_eq!( + info, + AccountInfo { + balance: U256::from(14352720829244098514u64), + nonce: 6690, + code: Default::default(), + } + ); } fn assert_hardfork_config( diff --git a/crates/doc/src/builder.rs b/crates/doc/src/builder.rs index 1b9bf00efb9c9..a4019db4a6a3a 100644 --- a/crates/doc/src/builder.rs +++ b/crates/doc/src/builder.rs @@ -156,7 +156,7 @@ impl DocBuilder { }; // Visit the parse tree - let mut doc = Parser::new(comments, source); + let mut doc = Parser::new(comments, source, self.fmt.tab_width); source_unit .visit(&mut doc) .map_err(|err| eyre::eyre!("Failed to parse source: {err}"))?; diff --git a/crates/doc/src/parser/item.rs b/crates/doc/src/parser/item.rs index 7eb7f95678ea6..fbd5276fec3fe 100644 --- a/crates/doc/src/parser/item.rs +++ b/crates/doc/src/parser/item.rs @@ -80,18 +80,21 @@ impl ParseItem { /// Set the source code of this [ParseItem]. /// /// The parameter should be the full source file where this parse item originated from. - pub fn with_code(mut self, source: &str) -> Self { + pub fn with_code(mut self, source: &str, tab_width: usize) -> Self { let mut code = source[self.source.range()].to_string(); // Special function case, add `;` at the end of definition. - if let ParseSource::Function(_) = self.source { + if let ParseSource::Function(_) | ParseSource::Error(_) | ParseSource::Event(_) = + self.source + { code.push(';'); } // Remove extra indent from source lines. + let prefix = &" ".repeat(tab_width); self.code = code .lines() - .map(|line| line.strip_prefix(" ").unwrap_or(line)) + .map(|line| line.strip_prefix(prefix).unwrap_or(line)) .collect::>() .join("\n"); self diff --git a/crates/doc/src/parser/mod.rs b/crates/doc/src/parser/mod.rs index 0007409b5a963..76da5d032382d 100644 --- a/crates/doc/src/parser/mod.rs +++ b/crates/doc/src/parser/mod.rs @@ -37,6 +37,8 @@ pub struct Parser { items: Vec, /// Source file. source: String, + /// Tab width used to format code. + tab_width: usize, } /// [Parser] context. @@ -50,8 +52,8 @@ struct ParserContext { impl Parser { /// Create a new instance of [Parser]. - pub fn new(comments: Vec, source: String) -> Self { - Self { comments, source, ..Default::default() } + pub fn new(comments: Vec, source: String, tab_width: usize) -> Self { + Self { comments, source, tab_width, ..Default::default() } } /// Return the parsed items. Consumes the parser. @@ -93,7 +95,7 @@ impl Parser { /// Create new [ParseItem] with comments and formatted code. fn new_item(&mut self, source: ParseSource, loc_start: usize) -> ParserResult { let docs = self.parse_docs(loc_start)?; - Ok(ParseItem::new(source).with_comments(docs).with_code(&self.source)) + Ok(ParseItem::new(source).with_comments(docs).with_code(&self.source, self.tab_width)) } /// Parse the doc comments from the current start location. @@ -218,7 +220,7 @@ mod tests { fn parse_source(src: &str) -> Vec { let (mut source, comments) = parse(src, 0).expect("failed to parse source"); - let mut doc = Parser::new(comments, src.to_owned()); + let mut doc = Parser::new(comments, src.to_owned(), 4); source.visit(&mut doc).expect("failed to visit source"); doc.items() } diff --git a/crates/fmt/src/state/common.rs b/crates/fmt/src/state/common.rs index 87bafc0f3fcba..57261d9012f3f 100644 --- a/crates/fmt/src/state/common.rs +++ b/crates/fmt/src/state/common.rs @@ -23,7 +23,7 @@ impl<'ast> LitExt<'ast> for ast::Lit<'ast> { /// Language-specific pretty printing. Common for both: Solidity + Yul. impl<'ast> State<'_, 'ast> { - pub(super) fn print_lit(&mut self, lit: &'ast ast::Lit<'ast>) { + pub(super) fn print_lit_inner(&mut self, lit: &'ast ast::Lit<'ast>, is_yul: bool) { let ast::Lit { span, symbol, ref kind } = *lit; if self.handle_span(span, false) { return; @@ -48,7 +48,7 @@ impl<'ast> State<'_, 'ast> { self.end(); } ast::LitKind::Number(_) | ast::LitKind::Rational(_) => { - self.print_num_literal(symbol.as_str()); + self.print_num_literal(symbol.as_str(), is_yul); } ast::LitKind::Address(value) => self.word(value.to_string()), ast::LitKind::Bool(value) => self.word(if value { "true" } else { "false" }), @@ -56,7 +56,7 @@ impl<'ast> State<'_, 'ast> { } } - fn print_num_literal(&mut self, source: &str) { + fn print_num_literal(&mut self, source: &str, is_yul: bool) { fn strip_underscores_if(b: bool, s: &str) -> Cow<'_, str> { if b && s.contains('_') { Cow::Owned(s.replace('_', "")) } else { Cow::Borrowed(s) } } @@ -66,9 +66,13 @@ impl<'ast> State<'_, 'ast> { config: config::NumberUnderscore, string: &str, is_dec: bool, + is_yul: bool, reversed: bool, ) { - if !config.is_thousands() || !is_dec || string.len() < 5 { + // The underscore thousand separator is only valid in Solidity decimal numbers. + // It is not supported by hex numbers, nor Yul literals. + // https://github.com/foundry-rs/foundry/issues/12111 + if !config.is_thousands() || !is_dec || is_yul || string.len() < 5 { out.push_str(string); return; } @@ -96,7 +100,7 @@ impl<'ast> State<'_, 'ast> { }; let (val, fract) = val.split_once('.').unwrap_or((val, "")); - let strip_underscores = !config.is_preserve(); + let strip_underscores = !config.is_preserve() || is_yul; let mut val = &strip_underscores_if(strip_underscores, val)[..]; let mut exp = &strip_underscores_if(strip_underscores, exp)[..]; let mut fract = &strip_underscores_if(strip_underscores, fract)[..]; @@ -115,12 +119,12 @@ impl<'ast> State<'_, 'ast> { if val.is_empty() { out.push('0'); } else { - add_underscores(&mut out, config, val, is_dec, false); + add_underscores(&mut out, config, val, is_dec, is_yul, false); } if source.contains('.') { out.push('.'); if !fract.is_empty() { - add_underscores(&mut out, config, fract, is_dec, true); + add_underscores(&mut out, config, fract, is_dec, is_yul, true); } else { out.push('0'); } @@ -128,7 +132,7 @@ impl<'ast> State<'_, 'ast> { if !exp.is_empty() { out.push('e'); out.push_str(exp_sign); - add_underscores(&mut out, config, exp, is_dec, false); + add_underscores(&mut out, config, exp, is_dec, is_yul, false); } self.word(out); @@ -346,7 +350,7 @@ impl<'ast> State<'_, 'ast> { } if !values.is_empty() && !format.with_delimiters { - self.zerobreak(); + format.print_break(true, values.len(), &mut self.s); self.s.offset(self.ind); return true; } @@ -490,7 +494,7 @@ impl<'ast> State<'_, 'ast> { self.cursor.advance_to(pos_hi, true); if last_delimiter_break { - self.zerobreak(); + format.print_break(true, values.len(), &mut self.s); } } @@ -849,9 +853,9 @@ impl ListFormat { self } - pub(crate) fn no_delimiters(mut self) -> Self { + pub(crate) fn with_delimiters(mut self, with: bool) -> Self { if matches!(self.kind, ListFormatKind::Compact | ListFormatKind::Consistent) { - self.with_delimiters = false; + self.with_delimiters = with; } self } diff --git a/crates/fmt/src/state/mod.rs b/crates/fmt/src/state/mod.rs index 7c49cb775df79..a4080c3189511 100644 --- a/crates/fmt/src/state/mod.rs +++ b/crates/fmt/src/state/mod.rs @@ -98,6 +98,7 @@ impl CallStack { } pub(super) struct State<'sess, 'ast> { + // CORE COMPONENTS pub(super) s: pp::Printer, ind: isize, @@ -107,15 +108,28 @@ pub(super) struct State<'sess, 'ast> { inline_config: InlineConfig<()>, cursor: SourcePos, + // FORMATTING CONTEXT: + // Whether the source file uses CRLF (`\r\n`) line endings. has_crlf: bool, + // The current contract being formatted, if inside a contract definition. contract: Option<&'ast ast::ItemContract<'ast>>, + // Current block nesting depth (incremented for each `{...}` block entered). + block_depth: usize, + // Stack tracking nested and chained function calls. + call_stack: CallStack, + + // Whether the current statement should be formatted as a single line, or not. single_line_stmt: Option, - named_call_expr: bool, + // The current binary expression chain context, if inside one. binary_expr: Option, + // Whether inside a `return` statement that contains a binary expression, or not. return_bin_expr: bool, + // Whether inside a named argument call, or not. + named_call_expr: bool, + // Whether inside an `emit` or `revert` call with a qualified path, or not. + emit_or_revert: bool, + // Whether inside a variable initialization expression, or not. var_init: bool, - block_depth: usize, - call_stack: CallStack, } impl std::ops::Deref for State<'_, '_> { @@ -207,6 +221,7 @@ impl<'sess> State<'sess, '_> { named_call_expr: false, binary_expr: None, return_bin_expr: false, + emit_or_revert: false, var_init: false, block_depth: 0, call_stack: CallStack::default(), @@ -389,11 +404,11 @@ impl State<'_, '_> { } else if !first && let Some(char) = line.chars().next() { // A line break or a space are required if this line: // - starts with an operator. + // - starts with one of the ternary operators // - starts with a bracket and fmt config forces bracket spacing. match char { - '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' => { - size += 1 - } + '&' | '|' | '=' | '>' | '<' | '+' | '-' | '*' | '/' | '%' | '^' | '?' + | ':' => size += 1, '}' | ')' | ']' if self.config.bracket_spacing => size += 1, _ => (), } diff --git a/crates/fmt/src/state/sol.rs b/crates/fmt/src/state/sol.rs index bc507ed384e89..6b63e756d21d2 100644 --- a/crates/fmt/src/state/sol.rs +++ b/crates/fmt/src/state/sol.rs @@ -288,6 +288,14 @@ impl<'ast> State<'_, 'ast> { self.print_ident(name); self.nbsp(); + if let Some(layout) = layout + && !self.handle_span(layout.span, false) + { + self.word("layout at "); + self.print_expr(layout.slot); + self.print_sep(Separator::Space); + } + if let Some(first) = bases.first().map(|base| base.span()) && let Some(last) = bases.last().map(|base| base.span()) && self.inline_config.is_disabled(first.to(last)) @@ -296,26 +304,30 @@ impl<'ast> State<'_, 'ast> { } else if !bases.is_empty() { self.word("is"); self.space(); - for (pos, base) in bases.iter().delimited() { + let last = bases.len() - 1; + for (i, base) in bases.iter().enumerate() { if !self.handle_span(base.span(), false) { self.print_modifier_call(base, false); - if !pos.is_last { + if i != last { self.word(","); - self.space(); + if self + .print_comments( + bases[i + 1].span().lo(), + CommentConfig::skip_ws().mixed_prev_space().mixed_post_nbsp(), + ) + .is_none() + { + self.space(); + } } } } - self.space(); + if !self.print_trailing_comment(bases.last().unwrap().span().hi(), None) { + self.space(); + } self.s.offset(-self.ind); } self.end(); - if let Some(layout) = layout - && !self.handle_span(layout.span, false) - { - self.word("layout at "); - self.print_expr(layout.slot); - self.print_sep(Separator::Space); - } self.print_word("{"); self.end(); @@ -461,6 +473,11 @@ impl<'ast> State<'_, 'ast> { let header_style = self.config.multiline_func_header; let params_format = match header_style { MultilineFuncHeaderStyle::ParamsFirst => ListFormat::always_break(), + MultilineFuncHeaderStyle::All + if header.parameters.len() > 1 && !self.can_header_be_inlined(header) => + { + ListFormat::always_break() + } MultilineFuncHeaderStyle::AllParams if !header.parameters.is_empty() && !self.can_header_be_inlined(header) => { @@ -728,6 +745,140 @@ impl<'ast> State<'_, 'ast> { self.word(";"); } + /// Prints the RHS of an assignment or variable initializer. + fn print_assign_rhs( + &mut self, + rhs: &'ast ast::Expr<'ast>, + lhs_size: usize, + space_left: usize, + ty: Option<&ast::TypeKind<'ast>>, + cache: bool, + ) { + // Check if the total expression overflows but the RHS would fit alone on a new line. + // This helps keep the RHS together on a single line when possible. + let rhs_size = self.estimate_size(rhs.span); + let overflows = lhs_size + rhs_size >= space_left; + let fits_alone = rhs_size + self.config.tab_width < space_left; + let fits_alone_no_cmnts = + fits_alone && !self.has_comment_between(rhs.span.lo(), rhs.span.hi()); + let force_break = overflows && fits_alone_no_cmnts; + + // Set up precall size tracking + if lhs_size <= space_left { + self.neverbreak(); + self.call_stack.add_precall(lhs_size + 1); + } else { + self.call_stack.add_precall(space_left + self.config.tab_width); + } + + // Handle comments before the RHS expression + if let Some(cmnt) = self.peek_comment_before(rhs.span.lo()) + && self.inline_config.is_disabled(cmnt.span) + { + self.print_sep(Separator::Nbsp); + } + if self + .print_comments( + rhs.span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ) + .is_some_and(|cmnt| cmnt.is_trailing()) + { + self.break_offset_if_not_bol(SIZE_INFINITY as usize, self.ind, false); + } + + // Match on expression kind to determine formatting strategy + match &rhs.kind { + ast::ExprKind::Lit(lit, ..) if lit.is_str_concatenation() => { + // String concatenations stay on the same line with nbsp + self.print_sep(Separator::Nbsp); + self.neverbreak(); + self.s.ibox(self.ind); + self.print_expr(rhs); + self.end(); + } + ast::ExprKind::Lit(..) if ty.is_none() && !fits_alone => { + // Long string in assign expr goes on its own line + self.print_sep(Separator::Space); + self.s.offset(self.ind); + self.print_expr(rhs); + } + ast::ExprKind::Binary(_, op, _) => { + // Binary expressions: check if we need to break and indent + if force_break || self.estimate_lhs_size(rhs, op) + lhs_size > space_left { + if !self.is_bol_or_only_ind() { + if force_break { + self.print_sep(Separator::Hardbreak); + } else { + self.print_sep(Separator::Space); + } + } + self.s.offset(self.ind); + self.s.ibox(self.ind); + self.print_expr(rhs); + self.end(); + } else { + self.print_sep(Separator::Nbsp); + self.neverbreak(); + self.print_expr(rhs); + } + } + _ => { + // General case: handle calls, complex successors, and other expressions + let callee_doesnt_fit = if let ast::ExprKind::Call(call_expr, ..) = &rhs.kind { + let callee_size = get_callee_head_size(call_expr); + callee_size + lhs_size > space_left + && callee_size + self.config.tab_width < space_left + } else { + false + }; + + if (lhs_size + 1 >= space_left && !is_call_chain(&rhs.kind, false)) + || callee_doesnt_fit + { + self.s.ibox(self.ind); + } else { + self.s.ibox(0); + }; + + if has_complex_successor(&rhs.kind, true) + && !matches!(&rhs.kind, ast::ExprKind::Member(..)) + { + // delegate breakpoints to `self.commasep(..)` for complex successors + if !self.is_bol_or_only_ind() { + let needs_offset = !callee_doesnt_fit + && rhs_size + lhs_size + 1 >= space_left + && fits_alone_no_cmnts; + let separator = if callee_doesnt_fit || needs_offset { + Separator::Space + } else { + Separator::Nbsp + }; + self.print_sep(separator); + if needs_offset { + self.s.offset(self.ind); + } + } + } else { + if !self.is_bol_or_only_ind() { + self.print_sep_unhandled(Separator::Space); + } + // apply type-dependent indentation if type info is available + if let Some(ty) = ty + && matches!(ty, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..)) + { + self.s.offset(self.ind); + } + } + self.print_expr(rhs); + self.end(); + } + } + + self.var_init = cache; + self.call_stack.reset_precall(); + } + fn print_var(&mut self, var: &'ast ast::VariableDefinition<'ast>, is_var_def: bool) { let ast::VariableDefinition { span, @@ -800,94 +951,8 @@ impl<'ast> State<'_, 'ast> { self.end(); } self.end(); - if pre_init_size <= init_space_left { - self.neverbreak(); - pre_init_size += 1; - self.call_stack.add_precall(pre_init_size); - } else { - self.call_stack.add_precall(init_space_left + self.config.tab_width); - } - - if let Some(cmnt) = self.peek_comment_before(init.span.lo()) - && self.inline_config.is_disabled(cmnt.span) - { - self.print_sep(Separator::Nbsp); - } - if self - .print_comments( - init.span.lo(), - CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), - ) - .is_some_and(|cmnt| cmnt.is_trailing()) - { - self.break_offset_if_not_bol(SIZE_INFINITY as usize, self.ind, false); - } - - if let ast::ExprKind::Lit(lit, ..) = &init.kind - && lit.is_str_concatenation() - { - self.print_sep(Separator::Nbsp); - self.neverbreak(); - self.s.ibox(self.ind); - self.print_expr(init); - self.end(); - } else if is_binary_expr(&init.kind) { - if !self.is_bol_or_only_ind() { - self.print_sep_unhandled(Separator::Space); - } - if matches!(ty.kind, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..)) { - self.s.offset(self.ind); - } - self.print_expr(init); - } else { - let callee_doesnt_fit = if let ast::ExprKind::Call(call_expr, ..) = &init.kind { - let callee_size = get_callee_head_size(call_expr); - callee_size + pre_init_size > init_space_left - && callee_size + self.config.tab_width < init_space_left - } else { - false - }; - if (pre_init_size + 1 >= init_space_left && !is_call_chain(&init.kind, false)) - || callee_doesnt_fit - { - self.s.ibox(self.ind); - } else { - self.s.ibox(0); - }; - - if has_complex_successor(&init.kind, true) - && !matches!(&init.kind, ast::ExprKind::Member(..)) - { - // delegate breakpoints to `self.commasep(..)` - if !self.is_bol_or_only_ind() { - let init_size = self.estimate_size(init.span); - if callee_doesnt_fit { - self.print_sep(Separator::Space); - } else if init_size + pre_init_size + 1 >= init_space_left - && init_size + self.config.tab_width < init_space_left - && !self.has_comment_between(init.span.lo(), init.span.hi()) - { - self.print_sep(Separator::Space); - self.s.offset(self.ind); - } else { - self.print_sep(Separator::Nbsp); - } - } - } else { - if !self.is_bol_or_only_ind() { - self.print_sep_unhandled(Separator::Space); - } - if matches!(ty.kind, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..)) - { - self.s.offset(self.ind); - } - } - self.print_expr(init); - self.end(); - } - self.var_init = cache; - self.call_stack.reset_precall(); + self.print_assign_rhs(init, pre_init_size, init_space_left, Some(&ty.kind), cache); } else { self.end(); } @@ -939,6 +1004,10 @@ impl<'ast> State<'_, 'ast> { self.print_str_lit(ast::StrKind::Str, strlit.span.lo(), strlit.value.as_str()); } + fn print_lit(&mut self, lit: &'ast ast::Lit<'ast>) { + self.print_lit_inner(lit, false); + } + fn print_ty(&mut self, ty: &'ast ast::Type<'ast>) { if self.handle_span(ty.span, false) { return; @@ -1266,55 +1335,14 @@ impl<'ast> State<'_, 'ast> { /// Prints a simple assignment expression of the form `lhs = rhs`. fn print_assign_expr(&mut self, lhs: &'ast ast::Expr<'ast>, rhs: &'ast ast::Expr<'ast>) { - let prev_var_init = self.var_init; + let cache = self.var_init; self.var_init = true; - // Estimate layout constraints let space_left = self.space_left(); let lhs_size = self.estimate_size(lhs.span); - let rhs_size = self.estimate_size(rhs.span); - - let total_size = lhs_size + rhs_size + 4; // 'lhs' + ' = ' + 'rhs' + ';' - let overflows = total_size >= space_left; - let fits_alone = rhs_size + self.config.tab_width < space_left; - - self.call_stack.add_precall(lhs_size); - - let is_simple_rhs = matches!(rhs.kind, ast::ExprKind::Lit(..) | ast::ExprKind::Ident(..)); - - if (overflows && fits_alone) || is_simple_rhs { - self.s.ibox(self.ind) - } else { - self.s.ibox(0) - } - - // Print LHS and '=' self.print_expr(lhs); self.word(" ="); - - // Handle RHS printing strategy - match &rhs.kind { - ast::ExprKind::Lit(lit, ..) if lit.is_str_concatenation() => { - self.print_sep(Separator::Nbsp); - self.neverbreak(); - self.s.ibox(self.ind); - self.print_expr(rhs); - self.end(); - } - _ if overflows && (fits_alone || is_simple_rhs) => { - self.print_sep(Separator::Space); - self.print_expr(rhs); - } - _ => { - self.print_sep(Separator::Nbsp); - self.neverbreak(); - self.print_expr(rhs); - } - } - - self.end(); - self.var_init = prev_var_init; - self.call_stack.reset_precall(); + self.print_assign_rhs(rhs, lhs_size + 2, space_left, None, cache); } /// Prints a binary operator expression. Handles operator chains and formatting. @@ -1658,32 +1686,39 @@ impl<'ast> State<'_, 'ast> { self.word("{"); // Use the start position of the first argument's name for comment processing. - let list_lo = args.first().map_or(pos_hi, |arg| arg.name.span.lo()); - - self.commasep( - args, - list_lo, - pos_hi, - // Closure to print a single named argument (`name: value`) - |s, arg| { - s.cbox(0); - s.print_ident(&arg.name); - s.word(":"); - if s.same_source_line(arg.name.span.hi(), arg.value.span.hi()) - || !s.print_trailing_comment(arg.name.span.hi(), None) - { - s.nbsp(); - } - s.print_comments( - arg.value.span.lo(), - CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(), - ); - s.print_expr(arg.value); - s.end(); - }, - |arg| arg.name.span.until(arg.value.span), - list_format.break_cmnts().break_single(true).without_ind(self.call_stack.is_chain()), - ); + if let Some(first_arg) = args.first() { + let list_lo = first_arg.name.span.lo(); + self.commasep( + args, + list_lo, + pos_hi, + // Closure to print a single named argument (`name: value`) + |s, arg| { + s.cbox(0); + s.print_ident(&arg.name); + s.word(":"); + if s.same_source_line(arg.name.span.hi(), arg.value.span.hi()) + || !s.print_trailing_comment(arg.name.span.hi(), None) + { + s.nbsp(); + } + s.print_comments( + arg.value.span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(), + ); + s.print_expr(arg.value); + s.end(); + }, + |arg| arg.name.span.until(arg.value.span), + list_format + .break_cmnts() + .break_single(true) + .without_ind(self.call_stack.is_chain()) + .with_delimiters(!self.emit_or_revert), + ); + } else if self.config.bracket_spacing { + self.nbsp(); + } self.word("}"); if !cache { @@ -2042,6 +2077,7 @@ impl<'ast> State<'_, 'ast> { if let Some((first, other)) = clauses.split_first() { // Print the 'try' clause let ast::TryCatchClause { args, block, span: try_span, .. } = first; + self.cbox(0); self.ibox(0); self.print_word("try "); self.print_comments(expr.span.lo(), CommentConfig::skip_ws()); @@ -2058,12 +2094,22 @@ impl<'ast> State<'_, 'ast> { if !args.is_empty() { self.print_word("returns "); - self.print_parameter_list( + self.print_word("("); + self.zerobreak(); + self.end(); + let span = args.span.with_hi(block.span.lo()); + self.commasep( args, - args.span.with_hi(block.span.lo()), - ListFormat::compact(), + span.lo(), + span.hi(), + |fmt, var| fmt.print_var(var, false), + get_span!(), + ListFormat::compact().with_delimiters(false), ); + self.print_word(")"); self.nbsp(); + } else { + self.end(); } if block.is_empty() { self.print_block(block, *try_span); @@ -2197,15 +2243,17 @@ impl<'ast> State<'_, 'ast> { }; self.s.cbox(0); self.print_path(path, false); + self.emit_or_revert = path.segments().len() > 1; self.print_call_args( args, - if args.len() == 1 { - ListFormat::compact().break_cmnts() + if self.config.call_compact_args { + ListFormat::compact().break_cmnts().with_delimiters(args.len() == 1) } else { - ListFormat::compact().break_cmnts().no_delimiters() + ListFormat::consistent().break_cmnts().with_delimiters(args.len() == 1) }, path.to_string().len(), ); + self.emit_or_revert = false; self.end(); } @@ -2429,6 +2477,8 @@ impl<'ast> State<'_, 'ast> { } fn can_header_be_inlined(&mut self, header: &ast::FunctionHeader<'_>) -> bool { + const FUNCTION: usize = 8; + // ' ' + visibility let visibility = header.visibility.map_or(0, |v| self.estimate_size(v.span) + 1); // ' ' + state mutability @@ -2445,7 +2495,8 @@ impl<'ast> State<'_, 'ast> { .fold(0, |len, p| if len != 0 { len + 2 } else { 8 } + self.estimate_size(p.span)) }); - self.estimate_header_params_size(header) + FUNCTION + + self.estimate_header_params_size(header) + visibility + mutability + modifiers @@ -2470,6 +2521,15 @@ impl<'ast> State<'_, 'ast> { self.estimate_header_params_size(header) <= self.space_left() } + fn estimate_lhs_size(&self, expr: &ast::Expr<'_>, parent_op: &ast::BinOp) -> usize { + match &expr.kind { + ast::ExprKind::Binary(lhs, op, _) if op.kind.group() == parent_op.kind.group() => { + self.estimate_lhs_size(lhs, op) + } + _ => self.estimate_size(expr.span), + } + } + fn has_comments_between_elements(&self, limits: Span, elements: I) -> bool where I: IntoIterator>, diff --git a/crates/fmt/src/state/yul.rs b/crates/fmt/src/state/yul.rs index 6386e34089c0a..120d98312e0b2 100644 --- a/crates/fmt/src/state/yul.rs +++ b/crates/fmt/src/state/yul.rs @@ -14,6 +14,10 @@ macro_rules! get_span { /// Language-specific pretty printing: Yul. impl<'ast> State<'_, 'ast> { + fn print_lit_yul(&mut self, lit: &'ast ast::Lit<'ast>) { + self.print_lit_inner(lit, true); + } + pub(crate) fn print_yul_stmt(&mut self, stmt: &'ast yul::Stmt<'ast>) { let yul::Stmt { ref docs, span, ref kind } = *stmt; self.print_docs(docs); @@ -85,7 +89,7 @@ impl<'ast> State<'_, 'ast> { CommentConfig::default().mixed_prev_space(), ); self.word("case "); - self.print_lit(constant); + self.print_lit_yul(constant); self.nbsp(); } else { self.print_comments( @@ -186,7 +190,7 @@ impl<'ast> State<'_, 'ast> { if matches!(&lit.kind, ast::LitKind::Address(_)) { self.print_span_cold(lit.span); } else { - self.print_lit(lit); + self.print_lit_yul(lit); } } } diff --git a/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol b/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol index 20894a47a3262..25074229db558 100644 --- a/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol @@ -39,3 +39,15 @@ contract ERC20DecimalsMock is ERC20 { _decimals = decimals_; } } + +contract SomeContract is + ERC165Upgradeable, // 1 inherited component + ISomeContract // 4 inherited components +{ } + +contract AnotherContract is + Adminable, /* 1 inherited components */ + UUPSUpgradeable /* 1 inherited component */ +{ } + +contract WithLayoutAndBase layout at 69 is Base { } diff --git a/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol b/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol index 2e9661f956dcf..990845d3d1349 100644 --- a/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol @@ -50,3 +50,15 @@ contract ERC20DecimalsMock is ERC20 { } } + +contract SomeContract is + ERC165Upgradeable, // 1 inherited component + ISomeContract // 4 inherited components +{} + +contract AnotherContract is + Adminable, /* 1 inherited components */ + UUPSUpgradeable /* 1 inherited component */ +{} + +contract WithLayoutAndBase layout at 69 is Base {} diff --git a/crates/fmt/testdata/ContractDefinition/fmt.sol b/crates/fmt/testdata/ContractDefinition/fmt.sol index 551e84decfc5b..93cddcd2c6a20 100644 --- a/crates/fmt/testdata/ContractDefinition/fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/fmt.sol @@ -45,3 +45,15 @@ contract ERC20DecimalsMock is ERC20 { _decimals = decimals_; } } + +contract SomeContract is + ERC165Upgradeable, // 1 inherited component + ISomeContract // 4 inherited components +{} + +contract AnotherContract is + Adminable, /* 1 inherited components */ + UUPSUpgradeable /* 1 inherited component */ +{} + +contract WithLayoutAndBase layout at 69 is Base {} diff --git a/crates/fmt/testdata/ContractDefinition/original.sol b/crates/fmt/testdata/ContractDefinition/original.sol index 4c671985bda7b..c0aa88a7d6d17 100644 --- a/crates/fmt/testdata/ContractDefinition/original.sol +++ b/crates/fmt/testdata/ContractDefinition/original.sol @@ -17,7 +17,7 @@ contract SampleContract { // comment 16 external /* comment 17 */ pure - returns(uint256) + returns(uint256) // comment 18 { // comment 19 return arg1 > arg2 ? arg1 : arg2; @@ -38,3 +38,15 @@ contract ERC20DecimalsMock is ERC20 { _decimals = decimals_; } } + +contract SomeContract is + ERC165Upgradeable, // 1 inherited component + ISomeContract // 4 inherited components +{ } + +contract AnotherContract is + Adminable, /* 1 inherited components */ + UUPSUpgradeable /* 1 inherited component */ +{ } + +contract WithLayoutAndBase layout at 69 is Base {} diff --git a/crates/fmt/testdata/EmitStatement/120.fmt.sol b/crates/fmt/testdata/EmitStatement/120.fmt.sol index 78107e8209932..0f0363828a38d 100644 --- a/crates/fmt/testdata/EmitStatement/120.fmt.sol +++ b/crates/fmt/testdata/EmitStatement/120.fmt.sol @@ -28,4 +28,10 @@ function emitEvent() { strategyMock, depositAmount / 6 // 1 withdrawal not queued so decreased ); + + // https://github.com/foundry-rs/foundry/issues/12146 + emit ISablierComptroller.DisableCustomFeeUSD(protocol, caller, users.sender, 0, feeUSD); + emit ISablierComptroller.DisableCustomFeeUSD({ + protocol: protocol, caller: caller, user: users.sender, previousMinFeeUSD: 0, newMinFeeUSD: feeUSD + }); } diff --git a/crates/fmt/testdata/EmitStatement/fmt.sol b/crates/fmt/testdata/EmitStatement/fmt.sol index d05d5eb9c6de3..00fefc10005e4 100644 --- a/crates/fmt/testdata/EmitStatement/fmt.sol +++ b/crates/fmt/testdata/EmitStatement/fmt.sol @@ -35,4 +35,16 @@ function emitEvent() { strategyMock, depositAmount / 6 // 1 withdrawal not queued so decreased ); + + // https://github.com/foundry-rs/foundry/issues/12146 + emit ISablierComptroller.DisableCustomFeeUSD( + protocol, caller, users.sender, 0, feeUSD + ); + emit ISablierComptroller.DisableCustomFeeUSD({ + protocol: protocol, + caller: caller, + user: users.sender, + previousMinFeeUSD: 0, + newMinFeeUSD: feeUSD + }); } diff --git a/crates/fmt/testdata/EmitStatement/original.sol b/crates/fmt/testdata/EmitStatement/original.sol index a498474d7262d..a6c61935432c4 100644 --- a/crates/fmt/testdata/EmitStatement/original.sol +++ b/crates/fmt/testdata/EmitStatement/original.sol @@ -29,4 +29,8 @@ function emitEvent() { strategyMock, depositAmount / 6 // 1 withdrawal not queued so decreased ); + + // https://github.com/foundry-rs/foundry/issues/12146 + emit ISablierComptroller.DisableCustomFeeUSD(protocol, caller, users.sender, 0, feeUSD); + emit ISablierComptroller.DisableCustomFeeUSD({ protocol: protocol, caller: caller, user: users.sender, previousMinFeeUSD: 0, newMinFeeUSD: feeUSD }); } diff --git a/crates/fmt/testdata/FunctionDefinition/all.fmt.sol b/crates/fmt/testdata/FunctionDefinition/all.fmt.sol index 4afce0bf0f304..10272e81a04ec 100644 --- a/crates/fmt/testdata/FunctionDefinition/all.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/all.fmt.sol @@ -727,7 +727,10 @@ contract FunctionOverrides is a = 1; } - function simple(address _target, bytes memory _payload) + function simple( + address _target, + bytes memory _payload + ) internal { a = 1; diff --git a/crates/fmt/testdata/IfStatement2/120.fmt.sol b/crates/fmt/testdata/IfStatement2/120.fmt.sol new file mode 100644 index 0000000000000..e5d36087fe410 --- /dev/null +++ b/crates/fmt/testdata/IfStatement2/120.fmt.sol @@ -0,0 +1,20 @@ +// config: line_length = 120 +contract IfStatement { + function test() external { + bool anotherLongCondition; + + if (condition && ((condition || anotherLongCondition))) execute(); + } + + // https://github.com/foundry-rs/foundry/issues/12102 + function repro() external { + for (uint256 i; i < len; ++i) { + proportions[i] = totalDepositedTvl == 0 + ? 0 + : Math.mulDiv(vaultUsdValue[i], 1e18, totalDepositedTvl, Math.Rounding.Floor); + proportions[i] = totalDepositedTvl == 0 + ? 0 + : Math.mulDiv(vaultUsdValue[i], 1e18, totalDepositedTvl, Math.Rounding.Floor); + } + } +} diff --git a/crates/fmt/testdata/IfStatement2/fmt.sol b/crates/fmt/testdata/IfStatement2/fmt.sol index 10ae43601d4ad..4553642128243 100644 --- a/crates/fmt/testdata/IfStatement2/fmt.sol +++ b/crates/fmt/testdata/IfStatement2/fmt.sol @@ -4,4 +4,26 @@ contract IfStatement { if (condition && ((condition || anotherLongCondition))) execute(); } + + // https://github.com/foundry-rs/foundry/issues/12102 + function repro() external { + for (uint256 i; i < len; ++i) { + proportions[i] = totalDepositedTvl == 0 + ? 0 + : Math.mulDiv( + vaultUsdValue[i], + 1e18, + totalDepositedTvl, + Math.Rounding.Floor + ); + proportions[i] = totalDepositedTvl == 0 + ? 0 + : Math.mulDiv( + vaultUsdValue[i], + 1e18, + totalDepositedTvl, + Math.Rounding.Floor + ); + } + } } diff --git a/crates/fmt/testdata/IfStatement2/original.sol b/crates/fmt/testdata/IfStatement2/original.sol index df020c04bbe33..909320da01093 100644 --- a/crates/fmt/testdata/IfStatement2/original.sol +++ b/crates/fmt/testdata/IfStatement2/original.sol @@ -7,4 +7,15 @@ contract IfStatement { ) ) execute(); } -} \ No newline at end of file + + // https://github.com/foundry-rs/foundry/issues/12102 + function repro() external { + for (uint i; i < len; ++i) { + proportions[i] = + totalDepositedTvl == 0 ? 0 : Math.mulDiv(vaultUsdValue[i], 1e18, totalDepositedTvl, Math.Rounding.Floor); + proportions[i] = totalDepositedTvl == 0 + ? 0 + : Math.mulDiv(vaultUsdValue[i], 1e18, totalDepositedTvl, Math.Rounding.Floor); + } + } +} diff --git a/crates/fmt/testdata/OperatorExpressions/120.fmt.sol b/crates/fmt/testdata/OperatorExpressions/120.fmt.sol index 3b108a7f87398..d50d5f2ba9021 100644 --- a/crates/fmt/testdata/OperatorExpressions/120.fmt.sol +++ b/crates/fmt/testdata/OperatorExpressions/120.fmt.sol @@ -66,3 +66,21 @@ function new_y(uint256 x, uint256 dx, uint256 x_basis, uint256 y, uint256 y_basi y * _VELODROME_TOKEN_BASIS / y_basis ) * y_basis / _VELODROME_TOKEN_BASIS * aReallyLongIdentifierThatMakesTheOperatorExpressionBreak; } + +contract Repro { + bytes4 public constant MINIMAL_INTERFACE_ID = this.calculateMinFeeWeiFor.selector ^ this.convertUSDFeeToWei.selector + ^ this.execute.selector ^ this.getMinFeeUSDFor.selector; + bool isTestnet = chainId == ARBITRUM_SEPOLIA || chainId == BASE_SEPOLIA || chainId == MODE_SEPOLIA + || chainId == OPTIMISM_SEPOLIA || chainId == SEPOLIA; + + function test() { + assign = this.calculateMinFeeWeiFor.selector ^ this.convertUSDFeeToWei.selector ^ this.execute.selector + ^ this.getMinFeeUSDFor.selector; + isMainnet = chainId == ABSTRACT || chainId == ARBITRUM || chainId == AVALANCHE || chainId == BASE + || chainId == BERACHAIN || chainId == BLAST || chainId == BSC || chainId == CHILIZ || chainId == COREDAO + || chainId == ETHEREUM || chainId == GNOSIS || chainId == HYPEREVM || chainId == LIGHTLINK + || chainId == LINEA || chainId == MODE || chainId == MORPH || chainId == OPTIMISM || chainId == POLYGON + || chainId == SCROLL || chainId == SEI || chainId == SOPHON || chainId == SUPERSEED || chainId == SONIC + || chainId == UNICHAIN || chainId == XDC || chainId == ZKSYNC; + } +} diff --git a/crates/fmt/testdata/OperatorExpressions/fmt.sol b/crates/fmt/testdata/OperatorExpressions/fmt.sol index ee292ab6106ca..e7ebdf77d1747 100644 --- a/crates/fmt/testdata/OperatorExpressions/fmt.sol +++ b/crates/fmt/testdata/OperatorExpressions/fmt.sol @@ -76,3 +76,27 @@ function new_y( ) * y_basis / _VELODROME_TOKEN_BASIS * aReallyLongIdentifierThatMakesTheOperatorExpressionBreak; } + +contract Repro { + bytes4 public constant MINIMAL_INTERFACE_ID = + this.calculateMinFeeWeiFor.selector ^ this.convertUSDFeeToWei.selector + ^ this.execute.selector ^ this.getMinFeeUSDFor.selector; + bool isTestnet = chainId == ARBITRUM_SEPOLIA || chainId == BASE_SEPOLIA + || chainId == MODE_SEPOLIA || chainId == OPTIMISM_SEPOLIA + || chainId == SEPOLIA; + + function test() { + assign = this.calculateMinFeeWeiFor.selector + ^ this.convertUSDFeeToWei.selector ^ this.execute.selector + ^ this.getMinFeeUSDFor.selector; + isMainnet = chainId == ABSTRACT || chainId == ARBITRUM + || chainId == AVALANCHE || chainId == BASE || chainId == BERACHAIN + || chainId == BLAST || chainId == BSC || chainId == CHILIZ + || chainId == COREDAO || chainId == ETHEREUM || chainId == GNOSIS + || chainId == HYPEREVM || chainId == LIGHTLINK || chainId == LINEA + || chainId == MODE || chainId == MORPH || chainId == OPTIMISM + || chainId == POLYGON || chainId == SCROLL || chainId == SEI + || chainId == SOPHON || chainId == SUPERSEED || chainId == SONIC + || chainId == UNICHAIN || chainId == XDC || chainId == ZKSYNC; + } +} diff --git a/crates/fmt/testdata/OperatorExpressions/original.sol b/crates/fmt/testdata/OperatorExpressions/original.sol index b852c48e75bad..0b897c881c7ca 100644 --- a/crates/fmt/testdata/OperatorExpressions/original.sol +++ b/crates/fmt/testdata/OperatorExpressions/original.sol @@ -58,3 +58,18 @@ function new_y(uint256 x, uint256 dx, uint256 x_basis, uint256 y, uint256 y_basi y * _VELODROME_TOKEN_BASIS / y_basis ) * y_basis / _VELODROME_TOKEN_BASIS * aReallyLongIdentifierThatMakesTheOperatorExpressionBreak; } + +contract Repro { + bytes4 public constant MINIMAL_INTERFACE_ID = this.calculateMinFeeWeiFor.selector ^ this.convertUSDFeeToWei.selector ^ this.execute.selector ^ this.getMinFeeUSDFor.selector; + bool isTestnet = chainId == ARBITRUM_SEPOLIA || chainId == BASE_SEPOLIA || chainId == MODE_SEPOLIA || chainId == OPTIMISM_SEPOLIA || chainId == SEPOLIA; + + function test() { + assign = this.calculateMinFeeWeiFor.selector ^ this.convertUSDFeeToWei.selector ^ this.execute.selector ^ this.getMinFeeUSDFor.selector; + isMainnet = chainId == ABSTRACT || chainId == ARBITRUM || chainId == AVALANCHE || chainId == BASE + || chainId == BERACHAIN || chainId == BLAST || chainId == BSC || chainId == CHILIZ || chainId == COREDAO + || chainId == ETHEREUM || chainId == GNOSIS || chainId == HYPEREVM || chainId == LIGHTLINK || chainId == LINEA + || chainId == MODE || chainId == MORPH || chainId == OPTIMISM || chainId == POLYGON || chainId == SCROLL + || chainId == SEI || chainId == SOPHON || chainId == SUPERSEED || chainId == SONIC || chainId == UNICHAIN + || chainId == XDC || chainId == ZKSYNC; + } +} diff --git a/crates/fmt/testdata/OperatorExpressions/pow-no-space.fmt.sol b/crates/fmt/testdata/OperatorExpressions/pow-no-space.fmt.sol index 0d367c6c237e0..81655935a1271 100644 --- a/crates/fmt/testdata/OperatorExpressions/pow-no-space.fmt.sol +++ b/crates/fmt/testdata/OperatorExpressions/pow-no-space.fmt.sol @@ -77,3 +77,27 @@ function new_y( ) * y_basis / _VELODROME_TOKEN_BASIS * aReallyLongIdentifierThatMakesTheOperatorExpressionBreak; } + +contract Repro { + bytes4 public constant MINIMAL_INTERFACE_ID = + this.calculateMinFeeWeiFor.selector ^ this.convertUSDFeeToWei.selector + ^ this.execute.selector ^ this.getMinFeeUSDFor.selector; + bool isTestnet = chainId == ARBITRUM_SEPOLIA || chainId == BASE_SEPOLIA + || chainId == MODE_SEPOLIA || chainId == OPTIMISM_SEPOLIA + || chainId == SEPOLIA; + + function test() { + assign = this.calculateMinFeeWeiFor.selector + ^ this.convertUSDFeeToWei.selector ^ this.execute.selector + ^ this.getMinFeeUSDFor.selector; + isMainnet = chainId == ABSTRACT || chainId == ARBITRUM + || chainId == AVALANCHE || chainId == BASE || chainId == BERACHAIN + || chainId == BLAST || chainId == BSC || chainId == CHILIZ + || chainId == COREDAO || chainId == ETHEREUM || chainId == GNOSIS + || chainId == HYPEREVM || chainId == LIGHTLINK || chainId == LINEA + || chainId == MODE || chainId == MORPH || chainId == OPTIMISM + || chainId == POLYGON || chainId == SCROLL || chainId == SEI + || chainId == SOPHON || chainId == SUPERSEED || chainId == SONIC + || chainId == UNICHAIN || chainId == XDC || chainId == ZKSYNC; + } +} diff --git a/crates/fmt/testdata/ReprosFunctionDefs/all.120.fmt.sol b/crates/fmt/testdata/ReprosFunctionDefs/all.120.fmt.sol new file mode 100644 index 0000000000000..c1ac1c0e42c1f --- /dev/null +++ b/crates/fmt/testdata/ReprosFunctionDefs/all.120.fmt.sol @@ -0,0 +1,15 @@ +// config: line_length = 120 +// config: multiline_func_header = "all" +contract Repros { + // https://github.com/foundry-rs/foundry/issues/12109 + function calculateStreamedPercentage( + uint128 streamedAmount, + uint128 depositedAmount + ) + internal + pure + returns (uint256) + { + a = 1; + } +} diff --git a/crates/fmt/testdata/ReprosFunctionDefs/original.sol b/crates/fmt/testdata/ReprosFunctionDefs/original.sol new file mode 100644 index 0000000000000..c757d03a05900 --- /dev/null +++ b/crates/fmt/testdata/ReprosFunctionDefs/original.sol @@ -0,0 +1,4 @@ +contract Repros { + // https://github.com/foundry-rs/foundry/issues/12109 + function calculateStreamedPercentage(uint128 streamedAmount, uint128 depositedAmount) internal pure returns (uint256) { a = 1; } +} diff --git a/crates/fmt/testdata/RevertNamedArgsStatement/bracket-spacing.fmt.sol b/crates/fmt/testdata/RevertNamedArgsStatement/bracket-spacing.fmt.sol new file mode 100644 index 0000000000000..e5161a1ee7d08 --- /dev/null +++ b/crates/fmt/testdata/RevertNamedArgsStatement/bracket-spacing.fmt.sol @@ -0,0 +1,38 @@ +// config: call_compact_args = false +// config: bracket_spacing = true +contract RevertNamedArgsStatement { + error EmptyError(); + error SimpleError(uint256 val); + error ComplexError(uint256 val, uint256 ts, string message); + error SomeVeryVeryVeryLongErrorNameWithNamedArgumentsThatExceedsMaximumLength( + uint256 val, uint256 ts, string message + ); + + function test() external { + revert({ }); + + revert EmptyError({ }); + + revert SimpleError({ val: 0 }); + + revert ComplexError({ + val: 0, + ts: block.timestamp, + message: "some reason" + }); + + revert SomeVeryVeryVeryLongErrorNameWithNamedArgumentsThatExceedsMaximumLength({ + val: 0, + ts: 0x00, + message: "something unpredictable happened that caused execution to revert" + }); + + revert({ }); // comment1 + + revert /* comment2 */ SimpleError({ /* comment3 */ // comment4 + val: 0 // comment 5 + }); + + revert Errors.Unauthorized({ caller: msg.sender, neededRole: role }); + } +} diff --git a/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol b/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol index 6ef5966b8912e..ddb9228dfdc30 100644 --- a/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol +++ b/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol @@ -31,5 +31,7 @@ contract RevertNamedArgsStatement { revert /* comment2 */ SimpleError({ /* comment3 */ // comment4 val: 0 // comment 5 }); + + revert Errors.Unauthorized({caller: msg.sender, neededRole: role}); } } diff --git a/crates/fmt/testdata/RevertNamedArgsStatement/original.sol b/crates/fmt/testdata/RevertNamedArgsStatement/original.sol index 2c9e35ba3d16c..1713aec13b47c 100644 --- a/crates/fmt/testdata/RevertNamedArgsStatement/original.sol +++ b/crates/fmt/testdata/RevertNamedArgsStatement/original.sol @@ -19,14 +19,16 @@ contract RevertNamedArgsStatement { ts: block.timestamp, message: "some reason" }); - + revert SomeVeryVeryVeryLongErrorNameWithNamedArgumentsThatExceedsMaximumLength({ val: 0, ts: 0x00, message: "something unpredictable happened that caused execution to revert"}); - revert // comment1 + revert // comment1 ({}); - revert /* comment2 */ SimpleError /* comment3 */ ({ // comment4 + revert /* comment2 */ SimpleError /* comment3 */ ({ // comment4 val:0 // comment 5 }); + + revert Errors.Unauthorized({ caller: msg.sender, neededRole: role }); } } diff --git a/crates/fmt/testdata/TryStatement/fmt.sol b/crates/fmt/testdata/TryStatement/fmt.sol index 1b4d30d028ccd..9d48d66f5063d 100644 --- a/crates/fmt/testdata/TryStatement/fmt.sol +++ b/crates/fmt/testdata/TryStatement/fmt.sol @@ -93,4 +93,12 @@ contract TryStatement { revert(); } } + + function try_reallyLongCall() { + try AggregatorV3Interface(oracle).latestRoundData() returns ( + uint80, int256 _price, uint256, uint256 _updatedAt, uint80 + ) { + return true; + } catch {} + } } diff --git a/crates/fmt/testdata/TryStatement/original.sol b/crates/fmt/testdata/TryStatement/original.sol index 8d6b50d0767f1..fe50355ecb4fc 100644 --- a/crates/fmt/testdata/TryStatement/original.sol +++ b/crates/fmt/testdata/TryStatement/original.sol @@ -85,4 +85,10 @@ contract TryStatement { revert(); } } + + function try_reallyLongCall() { + try AggregatorV3Interface(oracle).latestRoundData() returns (uint80, int256 _price, uint256, uint256 _updatedAt, uint80) { + return true; + } catch {} + } } diff --git a/crates/fmt/testdata/Yul/fmt.sol b/crates/fmt/testdata/Yul/fmt.sol index 6a5a1d7d5ad58..3db955e48ea69 100644 --- a/crates/fmt/testdata/Yul/fmt.sol +++ b/crates/fmt/testdata/Yul/fmt.sol @@ -1,3 +1,4 @@ +// config: number_underscore = "thousands" contract Yul { function test() external { // https://github.com/euler-xyz/euler-contracts/blob/d4f207a4ac5a6e8ab7447a0f09d1399150c41ef4/contracts/vendor/MerkleProof.sol#L54 diff --git a/crates/fmt/testdata/Yul/original.sol b/crates/fmt/testdata/Yul/original.sol index 4784872a59b4d..11347d492e911 100644 --- a/crates/fmt/testdata/Yul/original.sol +++ b/crates/fmt/testdata/Yul/original.sol @@ -93,7 +93,7 @@ contract Yul { // empty bytes mstore(0xe0, 0x80) - let s2 := call(sub(gas(), 5000), pair, 0, 0x7c, 0xa4, 0, 0) + let s2 := call(sub(gas(), 5_000), pair, 0, 0x7c, 0xa4, 0, 0) if iszero(s2) { revert(3, 3) } diff --git a/crates/fmt/tests/formatter.rs b/crates/fmt/tests/formatter.rs index 61ce1db6d8255..953dc6b48dbba 100644 --- a/crates/fmt/tests/formatter.rs +++ b/crates/fmt/tests/formatter.rs @@ -202,6 +202,7 @@ fmt_tests! { PragmaDirective, Repros, ReprosCalls, + ReprosFunctionDefs, ReturnStatement, RevertNamedArgsStatement, RevertStatement, diff --git a/crates/forge/src/cmd/fmt.rs b/crates/forge/src/cmd/fmt.rs index d73db4ca077c2..2b4842df9fe7e 100644 --- a/crates/forge/src/cmd/fmt.rs +++ b/crates/forge/src/cmd/fmt.rs @@ -51,21 +51,43 @@ impl_figment_convert_basic!(FmtArgs); impl FmtArgs { pub fn run(self) -> Result<()> { let config = self.load_config()?; + let cwd = std::env::current_dir()?; // Expand ignore globs and canonicalize from the get go let ignored = expand_globs(&config.root, config.fmt.ignore.iter())? .iter() - .flat_map(foundry_common::fs::canonicalize_path) + .flat_map(fs::canonicalize_path) .collect::>(); - let cwd = std::env::current_dir()?; + // Expand lib globs separately - we only exclude these during discovery, not explicit paths + let libs = expand_globs(&config.root, config.libs.iter().filter_map(|p| p.to_str()))? + .iter() + .flat_map(fs::canonicalize_path) + .collect::>(); + + // Helper to check if a file path is under any ignored or lib directory + let is_under_ignored_dir = |file_path: &Path, include_libs: bool| -> bool { + let check_against_dir = |dir: &PathBuf| { + file_path.starts_with(dir) + || cwd.join(file_path).starts_with(dir) + || fs::canonicalize_path(file_path).is_ok_and(|p| p.starts_with(dir)) + }; + + ignored.iter().any(&check_against_dir) + || (include_libs && libs.iter().any(&check_against_dir)) + }; + let input = match &self.paths[..] { [] => { - // Retrieve the project paths, and filter out the ignored ones. + // Retrieve the project paths, and filter out the ignored ones and libs. let project_paths: Vec = config .project_paths::() .input_files_iter() - .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p)))) + .filter(|p| { + !(ignored.contains(p) + || ignored.contains(&cwd.join(p)) + || is_under_ignored_dir(p, true)) + }) .collect(); Input::Paths(project_paths) } @@ -73,6 +95,7 @@ impl FmtArgs { paths => { let mut inputs = Vec::with_capacity(paths.len()); for path in paths { + // Check if path is in ignored directories if !ignored.is_empty() && ((path.is_absolute() && ignored.contains(path)) || ignored.contains(&cwd.join(path))) @@ -81,11 +104,18 @@ impl FmtArgs { } if path.is_dir() { - inputs.extend(foundry_compilers::utils::source_files_iter( - path, - SOLC_EXTENSIONS, - )); + // If the input directory is not a lib directory, make sure to ignore libs. + let exclude_libs = !is_under_ignored_dir(path, true); + inputs.extend( + foundry_compilers::utils::source_files_iter(path, SOLC_EXTENSIONS) + .filter(|p| { + !(ignored.contains(p) + || ignored.contains(&cwd.join(p)) + || is_under_ignored_dir(p, exclude_libs)) + }), + ); } else if path.is_sol() { + // Explicit file paths are always included, even if in a lib inputs.push(path.to_path_buf()); } else { warn!("Cannot process path {}", path.display()); diff --git a/crates/forge/tests/cli/fmt.rs b/crates/forge/tests/cli/fmt.rs index 9948c85def841..926f7e44d717b 100644 --- a/crates/forge/tests/cli/fmt.rs +++ b/crates/forge/tests/cli/fmt.rs @@ -24,6 +24,19 @@ contract Test { } "#; +forgetest_init!(fmt_exclude_libs_in_recursion, |prj, cmd| { + prj.wipe_contracts(); + prj.update_config(|config| config.fmt.ignore = vec!["src/ignore/".to_string()]); + + prj.add_lib("SomeLib.sol", UNFORMATTED); + prj.add_raw_source("ignore/IgnoredContract.sol", UNFORMATTED); + cmd.args(["fmt", ".", "--check"]); + cmd.assert_success(); + + cmd.forge_fuse().args(["fmt", "lib/SomeLib.sol", "--check"]); + cmd.assert_failure(); +}); + // Test that fmt can format a simple contract file forgetest_init!(fmt_file, |prj, cmd| { prj.add_raw_source("FmtTest.sol", UNFORMATTED);