diff --git a/crates/config/src/fmt.rs b/crates/config/src/fmt.rs index 69381171989be..223b46d2ffd6b 100644 --- a/crates/config/src/fmt.rs +++ b/crates/config/src/fmt.rs @@ -7,8 +7,10 @@ use serde::{Deserialize, Serialize}; pub struct FormatterConfig { /// Maximum line length where formatter will try to wrap the line pub line_length: usize, - /// Number of spaces per indentation level + /// Number of spaces per indentation level. Ignored if style is Tab pub tab_width: usize, + /// Style of indent + pub style: IndentStyle, /// Print spaces between brackets pub bracket_spacing: bool, /// Style of uint/int256 types @@ -166,11 +168,21 @@ pub enum MultilineFuncHeaderStyle { AllParams, } +/// Style of indent +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IndentStyle { + #[default] + Space, + Tab, +} + impl Default for FormatterConfig { fn default() -> Self { Self { line_length: 120, tab_width: 4, + style: IndentStyle::Space, bracket_spacing: false, int_types: IntTypes::Long, multiline_func_header: MultilineFuncHeaderStyle::AttributesFirst, diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index d48bfd66f59e2..9d265867a11d3 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -2597,6 +2597,7 @@ mod tests { cache::{CachedChains, CachedEndpoints}, endpoints::RpcEndpointType, etherscan::ResolvedEtherscanConfigs, + fmt::IndentStyle, }; use NamedChain::Moonbeam; use endpoints::{RpcAuth, RpcEndpointConfig}; @@ -4510,12 +4511,13 @@ mod tests { figment::Jail::expect_with(|jail| { jail.create_file( "foundry.toml", - r" + r#" [fmt] line_length = 100 tab_width = 2 bracket_spacing = true - ", + style = "space" + "#, )?; let loaded = Config::load().unwrap().sanitized(); assert_eq!( @@ -4524,6 +4526,7 @@ mod tests { line_length: 100, tab_width: 2, bracket_spacing: true, + style: IndentStyle::Space, ..Default::default() } ); diff --git a/crates/fmt/README.md b/crates/fmt/README.md index 3cd4331738b73..014988bef019d 100644 --- a/crates/fmt/README.md +++ b/crates/fmt/README.md @@ -130,6 +130,7 @@ The formatter supports multiple configuration options defined in `FormatterConfi | ignore | [] | Globs to ignore | | contract_new_lines | false | Add new line at start and end of contract declarations | | sort_imports | false | Sort import statements alphabetically in groups | +| style | space | Configures if spaces or tabs should be used for indents. `tab_width` will be ignored if set to `tab`. Available options: `space`, `tab` | ### Disable Line diff --git a/crates/fmt/src/buffer.rs b/crates/fmt/src/buffer.rs index 773ebb483c695..c9281faed4a09 100644 --- a/crates/fmt/src/buffer.rs +++ b/crates/fmt/src/buffer.rs @@ -4,6 +4,7 @@ use crate::{ comments::{CommentState, CommentStringExt}, string::{QuoteState, QuotedStringExt}, }; +use foundry_config::fmt::IndentStyle; use std::fmt::Write; /// An indent group. The group may optionally skip the first line @@ -44,6 +45,7 @@ pub struct FormatBuffer { indents: Vec, base_indent_len: usize, tab_width: usize, + style: IndentStyle, last_char: Option, current_line_len: usize, restrict_to_single_line: bool, @@ -51,10 +53,11 @@ pub struct FormatBuffer { } impl FormatBuffer { - pub fn new(w: W, tab_width: usize) -> Self { + pub fn new(w: W, tab_width: usize, style: IndentStyle) -> Self { Self { w, tab_width, + style, base_indent_len: 0, indents: vec![], current_line_len: 0, @@ -67,7 +70,7 @@ impl FormatBuffer { /// Create a new temporary buffer based on an existing buffer which retains information about /// the buffer state, but has a blank String as its underlying `Write` interface pub fn create_temp_buf(&self) -> FormatBuffer { - let mut new = FormatBuffer::new(String::new(), self.tab_width); + let mut new = FormatBuffer::new(String::new(), self.tab_width, self.style); new.base_indent_len = self.total_indent_len(); new.current_line_len = self.current_line_len(); new.last_char = self.last_char; @@ -114,9 +117,28 @@ impl FormatBuffer { } } - /// Get the current indent size (level * tab_width) + /// Get the current indent size. level * tab_width for spaces and level for tabs pub fn current_indent_len(&self) -> usize { - self.level() * self.tab_width + match self.style { + IndentStyle::Space => self.level() * self.tab_width, + IndentStyle::Tab => self.level(), + } + } + + /// Get the char used for indent + pub fn indent_char(&self) -> char { + match self.style { + IndentStyle::Space => ' ', + IndentStyle::Tab => '\t', + } + } + + /// Get the indent len for the given level + pub fn get_indent_len(&self, level: usize) -> usize { + match self.style { + IndentStyle::Space => level * self.tab_width, + IndentStyle::Tab => level, + } } /// Get the total indent size @@ -209,7 +231,7 @@ impl Write for FormatBuffer { return Ok(()); } - let mut indent = " ".repeat(self.current_indent_len()); + let mut indent = self.indent_char().to_string().repeat(self.current_indent_len()); loop { match self.state { @@ -232,11 +254,14 @@ impl Write for FormatBuffer { self.w.write_str(head)?; self.w.write_str(&indent)?; self.current_line_len = 0; - self.last_char = Some(' '); + self.last_char = Some(self.indent_char()); // a newline has been inserted if len > 0 { if self.last_indent_group_skipped() { - indent = " ".repeat(self.current_indent_len() + self.tab_width); + indent = self + .indent_char() + .to_string() + .repeat(self.get_indent_len(self.level() + 1)); self.set_last_indent_group_skipped(false); } if comment_state == CommentState::Line { @@ -340,10 +365,11 @@ mod tests { fn test_buffer_indents() -> std::fmt::Result { let delta = 1; - let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH); + let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); assert_eq!(buf.indents.len(), 0); assert_eq!(buf.level(), 0); assert_eq!(buf.current_indent_len(), 0); + assert_eq!(buf.style, IndentStyle::Space); buf.indent(delta); assert_eq!(buf.indents.len(), delta); @@ -374,7 +400,7 @@ mod tests { fn test_identical_temp_buf() -> std::fmt::Result { let content = "test string"; let multiline_content = "test\nmultiline\nmultiple"; - let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH); + let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); // create identical temp buf let mut temp = buf.create_temp_buf(); @@ -432,11 +458,40 @@ mod tests { ]; for content in &contents { - let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH); + let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); write!(buf, "{content}")?; assert_eq!(&buf.w, content); } Ok(()) } + + #[test] + fn test_indent_char() -> std::fmt::Result { + assert_eq!( + FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space).indent_char(), + ' ' + ); + assert_eq!( + FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Tab).indent_char(), + '\t' + ); + Ok(()) + } + + #[test] + fn test_indent_len() -> std::fmt::Result { + // Space should use level * TAB_WIDTH + let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Space); + assert_eq!(buf.current_indent_len(), 0); + buf.indent(2); + assert_eq!(buf.current_indent_len(), 2 * TAB_WIDTH); + + // Tab should use level + buf = FormatBuffer::new(String::new(), TAB_WIDTH, IndentStyle::Tab); + assert_eq!(buf.current_indent_len(), 0); + buf.indent(2); + assert_eq!(buf.current_indent_len(), 2); + Ok(()) + } } diff --git a/crates/fmt/src/formatter.rs b/crates/fmt/src/formatter.rs index 25626a5b0341a..c0fcbdfc65fb8 100644 --- a/crates/fmt/src/formatter.rs +++ b/crates/fmt/src/formatter.rs @@ -112,7 +112,7 @@ impl<'a, W: Write> Formatter<'a, W> { config: FormatterConfig, ) -> Self { Self { - buf: FormatBuffer::new(w, config.tab_width), + buf: FormatBuffer::new(w, config.tab_width, config.style), source, config, temp_bufs: Vec::new(), @@ -158,6 +158,7 @@ impl<'a, W: Write> Formatter<'a, W> { buf_fn! { fn last_indent_group_skipped(&self) -> bool } buf_fn! { fn set_last_indent_group_skipped(&mut self, skip: bool) } buf_fn! { fn write_raw(&mut self, s: impl AsRef) -> std::fmt::Result } + buf_fn! { fn indent_char(&self) -> char } /// Do the callback within the context of a temp buffer fn with_temp_buf( @@ -570,7 +571,12 @@ impl<'a, W: Write> Formatter<'a, W> { .char_indices() .take_while(|(idx, ch)| ch.is_whitespace() && *idx <= self.buf.current_indent_len()) .count(); - let to_skip = indent_whitespace_count - indent_whitespace_count % self.config.tab_width; + let to_skip = if indent_whitespace_count < self.buf.current_indent_len() { + 0 + } else { + self.buf.current_indent_len() + }; + write!(self.buf(), " *")?; let content = &line[to_skip..]; if !content.trim().is_empty() { @@ -599,7 +605,8 @@ impl<'a, W: Write> Formatter<'a, W> { .char_indices() .skip_while(|(idx, ch)| ch.is_whitespace() && *idx < indent) .map(|(_, ch)| ch); - let padded = format!("{}{}", " ".repeat(indent), chars.join("")); + let padded = + format!("{}{}", self.indent_char().to_string().repeat(indent), chars.join("")); self.write_raw(padded)?; return Ok(false); } @@ -722,7 +729,7 @@ impl<'a, W: Write> Formatter<'a, W> { let mut chunk = chunk.content.trim_start().to_string(); chunk.insert(0, '\n'); chunk - } else if chunk.content.starts_with(' ') { + } else if chunk.content.starts_with(self.indent_char()) { let mut chunk = chunk.content.trim_start().to_string(); chunk.insert(0, ' '); chunk diff --git a/crates/fmt/src/inline_config.rs b/crates/fmt/src/inline_config.rs index 9a770597726f2..e3177efcb5034 100644 --- a/crates/fmt/src/inline_config.rs +++ b/crates/fmt/src/inline_config.rs @@ -99,7 +99,19 @@ impl InlineConfig { InlineConfigItem::DisableLine => { let mut prev_newline = src[..loc.start()].char_indices().rev().skip_while(|(_, ch)| *ch != '\n'); - let start = prev_newline.next().map(|(idx, _)| idx).unwrap_or_default(); + let start = prev_newline + .next() + .map(|(idx, _)| { + if let Some((idx, ch)) = prev_newline.next() { + match ch { + '\r' => idx, + _ => idx + 1, + } + } else { + idx + } + }) + .unwrap_or_default(); let end_offset = loc.end(); let mut next_newline = diff --git a/crates/fmt/testdata/BlockComments/tab.fmt.sol b/crates/fmt/testdata/BlockComments/tab.fmt.sol new file mode 100644 index 0000000000000..3b12bee813749 --- /dev/null +++ b/crates/fmt/testdata/BlockComments/tab.fmt.sol @@ -0,0 +1,26 @@ +// config: style = "tab" +contract CounterTest is Test { + /** + * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev See {IERC721-balanceOf}. + */ + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } +} diff --git a/crates/fmt/testdata/BlockCommentsFunction/tab.fmt.sol b/crates/fmt/testdata/BlockCommentsFunction/tab.fmt.sol new file mode 100644 index 0000000000000..04a0986cb73db --- /dev/null +++ b/crates/fmt/testdata/BlockCommentsFunction/tab.fmt.sol @@ -0,0 +1,21 @@ +// config: style = "tab" +contract A { + Counter public counter; + /** + * TODO: this fuzz use too much time to execute + * function testGetFuzz(bytes[2][] memory kvs) public { + * for (uint256 i = 0; i < kvs.length; i++) { + * bytes32 root = trie.update(kvs[i][0], kvs[i][1]); + * console.logBytes32(root); + * } + * + * for (uint256 i = 0; i < kvs.length; i++) { + * (bool exist, bytes memory value) = trie.get(kvs[i][0]); + * console.logBool(exist); + * console.logBytes(value); + * require(exist); + * require(BytesSlice.equal(value, trie.getRaw(kvs[i][0]))); + * } + * } + */ +} diff --git a/crates/fmt/testdata/DocComments/tab.fmt.sol b/crates/fmt/testdata/DocComments/tab.fmt.sol new file mode 100644 index 0000000000000..0a2ca7a309431 --- /dev/null +++ b/crates/fmt/testdata/DocComments/tab.fmt.sol @@ -0,0 +1,101 @@ +// config: style = "tab" +pragma solidity ^0.8.13; + +/// @title A Hello world example +contract HelloWorld { + /// Some example struct + struct Person { + uint256 age; + address wallet; + } + + /** + * Here's a more double asterix comment + */ + Person public theDude; + + /// Constructs the dude + /// @param age The dude's age + constructor(uint256 age) { + theDude = Person({age: age, wallet: msg.sender}); + } + + /** + * @dev does nothing + */ + function example() public { + /** + * Does this add a whitespace error? + * + * Let's find out. + */ + } + + /** + * @dev Calculates a rectangle's surface and perimeter. + * @param w Width of the rectangle. + * @param h Height of the rectangle. + * @return s The calculated surface. + * @return p The calculated perimeter. + */ + function rectangle(uint256 w, uint256 h) + public + pure + returns (uint256 s, uint256 p) + { + s = w * h; + p = 2 * (w + h); + } + + /// A long doc line comment that will be wrapped + function docLineOverflow() external {} + + function docLinePostfixOverflow() external {} + + /// A long doc line comment that will be wrapped + + /** + * @notice Here is my comment + * - item 1 + * - item 2 + * Some equations: + * y = mx + b + */ + function anotherExample() external {} + + /** + * contract A { + * function foo() public { + * // does nothing. + * } + * } + */ + function multilineIndent() external {} + + /** + * contract A { + * function foo() public { + * // does nothing. + * } + * } + */ + function multilineMalformedIndent() external {} + + /** + * contract A { + * function withALongNameThatWillCauseCommentWrap() public { + * // does nothing. + * } + * } + */ + function malformedIndentOverflow() external {} +} + +/** + * contract A { + * function foo() public { + * // does nothing. + * } + * } + */ +function freeFloatingMultilineIndent() {} diff --git a/crates/fmt/testdata/Repros/tab.fmt.sol b/crates/fmt/testdata/Repros/tab.fmt.sol new file mode 100644 index 0000000000000..565768269e1ea --- /dev/null +++ b/crates/fmt/testdata/Repros/tab.fmt.sol @@ -0,0 +1,162 @@ +// config: style = "tab" +// Repros of fmt issues + +// https://github.com/foundry-rs/foundry/issues/4403 +function errorIdentifier() { + bytes memory error = bytes(""); + if (error.length > 0) {} +} + +// https://github.com/foundry-rs/foundry/issues/7549 +function one() external { + this.other({ + data: abi.encodeCall( + this.other, + ( + "bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla" + ) + ) + }); +} + +// https://github.com/foundry-rs/foundry/issues/3979 +contract Format { + bool public test; + + function testing(uint256 amount) public payable { + if ( + // This is a comment + msg.value == amount + ) { + test = true; + } else { + test = false; + } + + if ( + // Another one + block.timestamp >= amount + ) {} + } +} + +// https://github.com/foundry-rs/foundry/issues/3830 +contract TestContract { + function test(uint256 a) public { + if (a > 1) { + a = 2; + } // forgefmt: disable-line + } + + function test1() public { + assembly{ sstore( 1, 1) /* inline comment*/ // forgefmt: disable-line + sstore(2, 2) + } + } + + function test2() public { + assembly{ sstore( 1, 1) // forgefmt: disable-line + sstore(2, 2) + sstore(3, 3)// forgefmt: disable-line + sstore(4, 4) + } + } + + function test3() public { + // forgefmt: disable-next-line + assembly{ sstore( 1, 1) + sstore(2, 2) + sstore(3, 3)// forgefmt: disable-line + sstore(4, 4) + }// forgefmt: disable-line + } + + function test4() public { + // forgefmt: disable-next-line + assembly{ + sstore(1, 1) + sstore(2, 2) + sstore(3, 3)// forgefmt: disable-line + sstore(4, 4) + }// forgefmt: disable-line + if (condition) execute(); // comment7 + } + + function test5() public { + assembly { sstore(0, 0) }// forgefmt: disable-line + } + + function test6() returns (bool) { // forgefmt: disable-line + if ( true ) { // forgefmt: disable-line + } + return true ; } // forgefmt: disable-line + + function test7() returns (bool) { // forgefmt: disable-line + if (true) { // forgefmt: disable-line + uint256 a = 1; // forgefmt: disable-line + } + return true; + } + + function test8() returns (bool) { // forgefmt: disable-line + if ( true ) { // forgefmt: disable-line + uint256 a = 1; + } else { + uint256 b = 1; // forgefmt: disable-line + } + return true; + } +} + +// https://github.com/foundry-rs/foundry/issues/5825 +library MyLib { + bytes32 private constant TYPE_HASH = keccak256( + // forgefmt: disable-start + "MyStruct(" + "uint8 myEnum," + "address myAddress" + ")" + // forgefmt: disable-end + ); + + bytes32 private constant TYPE_HASH_1 = keccak256( + "MyStruct(" "uint8 myEnum," "address myAddress" ")" // forgefmt: disable-line + ); + + // forgefmt: disable-start + bytes32 private constant TYPE_HASH_2 = keccak256( + "MyStruct(" + "uint8 myEnum," + "address myAddress" + ")" + ); + // forgefmt: disable-end +} + +contract IfElseTest { + function setNumber(uint256 newNumber) public { + number = newNumber; + if (newNumber = 1) { + number = 1; + } else if (newNumber = 2) { + // number = 2; + } else { + newNumber = 3; + } + } +} + +contract DbgFmtTest is Test { + function test_argsList() public { + uint256 result1 = internalNoArgs({}); + result2 = add({a: 1, b: 2}); + } + + function add(uint256 a, uint256 b) internal pure returns (uint256) { + return a + b; + } + + function internalNoArgs() internal pure returns (uint256) { + return 0; + } +} diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index bcfaca725678e..a15a4ea3b7876 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -1061,6 +1061,7 @@ path = "out" [fmt] line_length = 120 tab_width = 4 +style = "space" bracket_spacing = false int_types = "long" multiline_func_header = "attributes_first" @@ -1277,6 +1278,7 @@ exclude = [] "fmt": { "line_length": 120, "tab_width": 4, + "style": "space", "bracket_spacing": false, "int_types": "long", "multiline_func_header": "attributes_first",