Skip to content

feat(forge fmt): Adds tab support as indent char in fmt #10979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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: 13 additions & 1 deletion crates/config/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2589,6 +2589,7 @@ mod tests {
cache::{CachedChains, CachedEndpoints},
endpoints::RpcEndpointType,
etherscan::ResolvedEtherscanConfigs,
fmt::IndentStyle,
};
use NamedChain::Moonbeam;
use endpoints::{RpcAuth, RpcEndpointConfig};
Expand Down Expand Up @@ -4502,12 +4503,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!(
Expand All @@ -4516,6 +4518,7 @@ mod tests {
line_length: 100,
tab_width: 2,
bracket_spacing: true,
style: IndentStyle::Space,
..Default::default()
}
);
Expand Down
1 change: 1 addition & 0 deletions crates/fmt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
75 changes: 65 additions & 10 deletions crates/fmt/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,17 +45,19 @@ pub struct FormatBuffer<W> {
indents: Vec<IndentGroup>,
base_indent_len: usize,
tab_width: usize,
style: IndentStyle,
last_char: Option<char>,
current_line_len: usize,
restrict_to_single_line: bool,
state: WriteState,
}

impl<W> FormatBuffer<W> {
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,
Expand All @@ -67,7 +70,7 @@ impl<W> FormatBuffer<W> {
/// 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<String> {
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;
Expand Down Expand Up @@ -114,9 +117,28 @@ impl<W> FormatBuffer<W> {
}
}

/// 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
Expand Down Expand Up @@ -209,7 +231,7 @@ impl<W: Write> Write for FormatBuffer<W> {
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 {
Expand All @@ -232,11 +254,14 @@ impl<W: Write> Write for FormatBuffer<W> {
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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(())
}
}
15 changes: 11 additions & 4 deletions crates/fmt/src/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<str>) -> std::fmt::Result }
buf_fn! { fn indent_char(&self) -> char }

/// Do the callback within the context of a temp buffer
fn with_temp_buf(
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions crates/fmt/testdata/Annotation/tab.fmt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// config: style = "tab"
// Support for Solana/Substrate annotations
contract A {
@selector([1, 2, 3, 4])
function foo() public {}

@selector("another one")
function bar() public {}

@first("")
@second("")
function foobar() public {}
}

@topselector(2)
contract B {}
68 changes: 68 additions & 0 deletions crates/fmt/testdata/ArrayExpressions/tab.fmt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// config: style = "tab"
contract ArrayExpressions {
function test() external {
/* ARRAY SUBSCRIPT */
uint256[10] memory sample;

uint256 length = 10;
uint256[] memory sample2 = new uint256[](length);

uint256[] /* comment1 */ memory /* comment2 */ sample3; // comment3

/* ARRAY SLICE */
msg.data[4:];
msg.data[:msg.data.length];
msg.data[4:msg.data.length];

msg.data[
// comment1
4:
];
msg.data[
: /* comment2 */ msg.data.length // comment3
];
msg.data[
// comment4
4: // comment5
msg.data.length /* comment6 */
];

uint256
someVeryVeryVeryLongVariableNameThatDenotesTheStartOfTheMessageDataSlice = 4;
uint256 someVeryVeryVeryLongVariableNameThatDenotesTheEndOfTheMessageDataSlice =
msg.data.length;
msg.data[
someVeryVeryVeryLongVariableNameThatDenotesTheStartOfTheMessageDataSlice:
];
msg.data[
:someVeryVeryVeryLongVariableNameThatDenotesTheEndOfTheMessageDataSlice
];
msg.data[
someVeryVeryVeryLongVariableNameThatDenotesTheStartOfTheMessageDataSlice:
someVeryVeryVeryLongVariableNameThatDenotesTheEndOfTheMessageDataSlice
];

/* ARRAY LITERAL */
[1, 2, 3];

uint256 someVeryVeryLongVariableName = 0;
[
someVeryVeryLongVariableName,
someVeryVeryLongVariableName,
someVeryVeryLongVariableName
];
uint256[3] memory literal = [
someVeryVeryLongVariableName,
someVeryVeryLongVariableName,
someVeryVeryLongVariableName
];

uint8[3] memory literal2 = /* comment7 */ [ // comment8
1,
2, /* comment9 */
3 // comment10
];
uint256[1] memory literal3 =
[ /* comment11 */ someVeryVeryLongVariableName /* comment13 */ ];
}
}
26 changes: 26 additions & 0 deletions crates/fmt/testdata/BlockComments/tab.fmt.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading