Skip to content
Merged
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
16 changes: 16 additions & 0 deletions crates/config/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub struct FormatterConfig {
pub override_spacing: bool,
/// Wrap comments on `line_length` reached
pub wrap_comments: bool,
/// Style of doc comments
pub docs_style: DocCommentStyle,
/// Globs to ignore
pub ignore: Vec<String>,
/// Add new line at start and end of contract declarations
Expand Down Expand Up @@ -101,6 +103,19 @@ pub enum HexUnderscore {
Bytes,
}

/// Style of doc comments
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DocCommentStyle {
/// Preserve the source code style
#[default]
Preserve,
/// Use single-line style (`///`)
Line,
/// Use block style (`/** .. */`)
Block,
}

/// Style of string quotes
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -200,6 +215,7 @@ impl Default for FormatterConfig {
sort_imports: false,
pow_no_space: false,
call_compact_args: true,
docs_style: DocCommentStyle::default(),
}
}
}
1 change: 1 addition & 0 deletions crates/fmt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ The formatter supports multiple configuration options defined in `foundry.toml`.
| `single_line_statement_blocks` | `preserve` | The style of single-line blocks in statements. Options: `preserve`, `single`, `multi`. |
| `override_spacing` | `false` | Print a space in the `override` attribute. |
| `wrap_comments` | `false` | Wrap comments when `line_length` is reached. |
| `docs_style` | `preserve` | Enforces the style of doc (natspec) comments. Options: `preserve`, `line`, `block`. |
| `ignore` | `[]` | Globs to ignore. |
| `contract_new_lines` | `false` | Add a new line at the start and end of contract declarations. |
| `sort_imports` | `false` | Sort import statements alphabetically in groups. A group is a set of imports separated by a newline. |
Expand Down
74 changes: 72 additions & 2 deletions crates/fmt/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use foundry_common::{
comments::{Comment, CommentStyle, Comments, estimate_line_width, line_with_tabs},
iter::IterDelimited,
};
use foundry_config::fmt::IndentStyle;
use foundry_config::fmt::{DocCommentStyle, IndentStyle};
use solar::parse::{
ast::{self, Span},
interface::{BytePos, SourceMap},
Expand Down Expand Up @@ -480,9 +480,30 @@ impl<'sess> State<'sess, '_> {
let config_cache = config;
let mut buffered_blank = None;
while self.peek_comment().is_some_and(|c| c.pos() < pos) {
let cmnt = self.next_comment().unwrap();
let mut cmnt = self.next_comment().unwrap();
let style_cache = cmnt.style;

// Merge consecutive line doc comments when converting to block style
if self.config.docs_style == foundry_config::fmt::DocCommentStyle::Block
&& cmnt.is_doc
&& cmnt.kind == ast::CommentKind::Line
{
let mut ref_line = self.sm.lookup_char_pos(cmnt.span.hi()).line;
while let Some(next_cmnt) = self.peek_comment() {
if !next_cmnt.is_doc
|| next_cmnt.kind != ast::CommentKind::Line
|| ref_line + 1 != self.sm.lookup_char_pos(next_cmnt.span.lo()).line
{
break;
}

let next_to_merge = self.next_comment().unwrap();
cmnt.lines.extend(next_to_merge.lines);
cmnt.span = cmnt.span.to(next_to_merge.span);
ref_line += 1;
}
}

// Ensure breaks are never skipped when there are multiple comments
if self.peek_comment_before(pos).is_some() {
config.iso_no_break = false;
Expand Down Expand Up @@ -662,6 +683,11 @@ impl<'sess> State<'sess, '_> {

fn print_comment(&mut self, mut cmnt: Comment, mut config: CommentConfig) {
self.cursor.advance_to(cmnt.span.hi(), true);

if cmnt.is_doc {
cmnt = style_doc_comment(self.config.docs_style, cmnt);
}

match cmnt.style {
CommentStyle::Mixed => {
let Some(prefix) = cmnt.prefix() else { return };
Expand Down Expand Up @@ -1056,3 +1082,47 @@ fn snippet_with_tabs(s: String, tab_width: usize) -> String {

formatted
}

/// Formats a doc comment with the requested style.
///
/// NOTE: assumes comments have already been normalized.
fn style_doc_comment(style: DocCommentStyle, mut cmnt: Comment) -> Comment {
match style {
DocCommentStyle::Line if cmnt.kind == ast::CommentKind::Block => {
let mut new_lines = Vec::new();
for (pos, line) in cmnt.lines.iter().delimited() {
if pos.is_first || pos.is_last {
// Skip the opening '/**' and closing '*/' lines
continue;
}

// Convert ' * {content}' to '/// {content}'
let trimmed = line.trim_start();
if let Some(content) = trimmed.strip_prefix('*') {
new_lines.push(format!("///{content}"));
} else if !trimmed.is_empty() {
new_lines.push(format!("/// {trimmed}"));
}
}

cmnt.lines = new_lines;
cmnt.kind = ast::CommentKind::Line;
cmnt
}
DocCommentStyle::Block if cmnt.kind == ast::CommentKind::Line => {
let mut new_lines = vec!["/**".to_string()];

for line in &cmnt.lines {
// Convert '/// {content}' to ' * {content}'
new_lines.push(format!(" *{content}", content = &line[3..]))
}

new_lines.push(" */".to_string());
cmnt.lines = new_lines;
cmnt.kind = ast::CommentKind::Block;
cmnt
}
// Otherwise, no conversion needed.
_ => cmnt,
}
}
117 changes: 117 additions & 0 deletions crates/fmt/testdata/DocComments/block.fmt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// config: docs_style = "block"
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;

/**
* Will this long comment be wrapped leaving
* orphan words?
*/
Person public anotherDude;

/**
* 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() {}
87 changes: 87 additions & 0 deletions crates/fmt/testdata/DocComments/line.fmt.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// config: docs_style = "line"
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;

/// Will this long comment be wrapped leaving
/// orphan words?
Person public anotherDude;

/// 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() {}
2 changes: 2 additions & 0 deletions crates/forge/tests/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ hex_underscore = "remove"
single_line_statement_blocks = "preserve"
override_spacing = false
wrap_comments = false
docs_style = "preserve"
ignore = []
contract_new_lines = false
sort_imports = false
Expand Down Expand Up @@ -1306,6 +1307,7 @@ forgetest_init!(test_default_config, |prj, cmd| {
"single_line_statement_blocks": "preserve",
"override_spacing": false,
"wrap_comments": false,
"docs_style": "preserve",
"ignore": [],
"contract_new_lines": false,
"sort_imports": false,
Expand Down
Loading