Skip to content

Commit 9d38af9

Browse files
0xrusowskyDaniPopesgrandizzy
authored
feat(fmt): rewrite formatter using Solar and a structured algorithm (#10907)
* init * wip * wip * add dbg from prettyplease * wip * fixes, pragma&imports * feat: using, types, literals * feat: contract * rm duplicate testdata * wip: finish items; exprs, stmts * wips * feat: line_length, tab_width * feat: contract_new_lines * wip: single_line_statement_blocks * tweaks * chore: bump solar to latest main * fix: test dir * bump * fix docs * fix: adjust '()' for modifier calls * test: typed yul does not exist anymore argotorg/solidity#15329 * test: function parameters cannot be empty * fix: adjust '()' for modifier calls for real * fix: forge fmt hates '*' imports * test: fix some invalid syntax * test: disable-stop does not exist * test: fix all parse errors * fix: literal touchups * bump * test: add snapshotting * test: update NumberLiteralUnderscore * fixes * struct space * test: fix StructDefinition; empty structs are not allowed anyway * test: update EventDefinition; matches Solidity style guide * test: update EnumDefinition; same as StructDefinition * test: update StructDefinition 2 * fix: comments in structs/enums * test: update ErrorDefinition; matches Solidity style guide * feat: print docs with other comments; update EnumVariants * chore: update EnumDefinition, StructDefinition * chore: readd post_break * chore: rename is_hardbreak_tok * feat: cleanups, more impls * test: fix some compile errors * feat: add FormatterResult with more variants * stuff * refactor: move print_item arms into their own functions * chore: consolidate item hardbreaks * fix: inline config parsing for block comments * wip: rm FunctionLike, wip functions * fix: clamp margin to max as well * megawip * feat: most of yul * wip: try-catch * wip: try-catch * feat: print compact tuple * wip: inline comments * wip: try-cactch * bump solar to have try-catch spans (#10832) * wip comment fmt * wip: array expr * finish arrays * block comments * doc block comments * ternary operators * wip: fn header * wip: fn header * fix: doc block comments + block braces * refactor state to organize helpers * fix commasep with initial trailing cmnt * fix: improve contract fmt * fix: block comments + contract definition * fix: wrap trailing comments * fix fn alingment * fix: rmv unecessary check * working fn headers!!! * block with comments at the beginning * bump solar * inline if statements based on user config * operator expr * finish binary operators + housekeeping * housekeeping * feat: binary expressions * fix: string literals * refactor comments + finish mappings * named functions * item spacing * more flexible comments + return stmts * var definition and flexible comments (#11093) * comment wrapping * sorted imports * middle cmnts for arrays and literals with subdenominations * revert: solar won't have spanned dataloc + subdenom * refactor inline config + almost finished impl * finish inline disable * finish inline disable * wip inline disable for repros * passing repros * almost working yul * chore: remove unrelated changes / merge artifacts * chore: remove unrelated changes / merge artifacts 2 * chore: remove unrelated changes / merge artifacts 3 * update fmt files to reflect current status * enable both passes * undo repros changes * config: style = tabs * test: inline config * style: drop "lint" references in favor of "ids" * function header config * finish fn header config! * re-enable 2nd pass * finish fn header style * feat: yul * test: update tracking cmnts * fix: yul repros * chore: small comment * chore: random + typos * chore: rm dead code * chore: rm unused vars * chore: clippy --fix * chore: some manual clippying * chore: final clippy --fix * refactor: tidy up * yul: inline blocks * yul: inline fn params * ensure all tests are successful * chore(fmt): merge new compiler setup (#11487) * patch/impl/test pending repros * style: typos * docs: update readme * docs: readme feedback * style: clippy * fix: merge conflicts * fix: disable legacy fmt tests * fix: config test * fix(win): normalize breaks * style: clippy * fix(win): normalize escaped quotes * fix(win): normalize multiline strings * fix(win): only normalize line breaks for expected data * chore: solar-powered fmt rollout (#11570) * fix: comment spans for asm + try blocks * fix: don't fmt yul addresses * fix: empty buffer due to really long comment * add repro * Revert "fix: empty buffer due to really long comment" This reverts commit f6768b4. * fix: advance cursor correctly in print_comment * fix: bin op indentation in complex exprs * docs Co-authored-by: DaniPopes <[email protected]> * chore: clean up * refactor: inline config * chore: share inline config * feat: remove HIR inline config visitor * test: bless * style: clippy * feat(fmt): call chain awareness (#11611) * wip: better call chains + return stmts + more tests * fix: more yul cmnts * fix: modifier cmnts + more yul cmnts * fix: returns with bin ops * fix breaks, still pending indentation * wip: call cahins and nested... getting closer * call stack to fmt complex calls * fix: more yul cmnts * Fix config test, typos * wip * fix: new call alignement cases * Fix tests * fix: return + bin ops + calls * style: clippy * feat: wrap long comments and merge with next line * fix: stale test * style: rmv comments * fmt nits (#11750) * nits * Try no format * chore: simplify call logic * fix: reenable 2nd pass * chore: cleanup * docs: solar cmnt * format testdata with new style * fix: extra space in function type * Readd relevant todo * style: use span builder methods * Review changes * fix: remove outdated cmnt * fix: test spacing * Revert "fix: test spacing" This reverts commit 541f4c8. --------- Co-authored-by: 0xrusowsky <[email protected]> --------- Co-authored-by: DaniPopes <[email protected]> Co-authored-by: grandizzy <[email protected]> Co-authored-by: grandizzy <[email protected]>
1 parent f653eda commit 9d38af9

File tree

156 files changed

+9215
-6562
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

156 files changed

+9215
-6562
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.DS_STORE
22
/target*
3+
/*.sol
34
out/
45
snapshots/
56
out.json

Cargo.lock

Lines changed: 6 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ serde.opt-level = 3
109109
foundry-solang-parser.opt-level = 3
110110
lalrpop-util.opt-level = 3
111111

112+
solar.opt-level = 3
112113
solar-ast.opt-level = 3
113114
solar-data-structures.opt-level = 3
114115
solar-interface.opt-level = 3
@@ -272,7 +273,6 @@ op-alloy-flz = "0.13.1"
272273
revm = { version = "29.0.0", default-features = false }
273274
revm-inspectors = { version = "0.29.0", features = ["serde"] }
274275
op-revm = { version = "10.0.0", default-features = false }
275-
276276
## alloy-evm
277277
alloy-evm = "0.20.1"
278278
alloy-op-evm = "0.20.1"
@@ -360,10 +360,12 @@ jiff = "0.2"
360360
heck = "0.5"
361361
uuid = "1.17.0"
362362
flate2 = "1.1"
363-
snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] }
364363

365364
## Pinned dependencies. Enabled for the workspace in crates/test-utils.
366365

366+
# testing
367+
snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] }
368+
367369
# Use unicode-rs which has a smaller binary size than the default ICU4X as the IDNA backend, used
368370
# by the `url` crate.
369371
# See the `idna_adapter` README.md for more details: https://docs.rs/crate/idna_adapter/latest

crates/chisel/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ path = "bin/main.rs"
1919

2020
[dependencies]
2121
# forge
22+
forge-doc.workspace = true
2223
forge-fmt.workspace = true
2324
foundry-cli.workspace = true
2425
foundry-common.workspace = true

crates/chisel/src/dispatcher.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,8 @@ pub struct EtherscanABIResponse {
6969

7070
/// Helper function that formats solidity source with the given [FormatterConfig]
7171
pub fn format_source(source: &str, config: FormatterConfig) -> eyre::Result<String> {
72-
match forge_fmt::parse(source) {
73-
Ok(parsed) => {
74-
let mut formatted_source = String::default();
75-
76-
if forge_fmt::format_to(&mut formatted_source, parsed, config).is_err() {
77-
eyre::bail!("Could not format source!");
78-
}
79-
80-
Ok(formatted_source)
81-
}
82-
Err(_) => eyre::bail!("Formatter could not parse source!"),
83-
}
72+
let formatted = forge_fmt::format(source, config).into_result()?;
73+
Ok(formatted)
8474
}
8575

8676
impl ChiselDispatcher {

crates/chisel/src/source.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! execution helpers.
66
77
use eyre::Result;
8-
use forge_fmt::solang_ext::{CodeLocationExt, SafeUnwrap};
8+
use forge_doc::solang_ext::{CodeLocationExt, SafeUnwrap};
99
use foundry_common::fs;
1010
use foundry_compilers::{
1111
Artifact, ProjectCompileOutput,

crates/cli/src/utils/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use alloy_json_abi::JsonAbi;
22
use alloy_primitives::{Address, U256, map::HashMap};
33
use alloy_provider::{Provider, network::AnyNetwork};
44
use eyre::{ContextCompat, Result};
5+
use forge_fmt::FormatterConfig;
56
use foundry_common::{
67
provider::{ProviderBuilder, RetryProvider},
78
shell,
@@ -100,7 +101,7 @@ fn env_filter() -> tracing_subscriber::EnvFilter {
100101

101102
pub fn abi_to_solidity(abi: &JsonAbi, name: &str) -> Result<String> {
102103
let s = abi.to_sol(name, None);
103-
let s = forge_fmt::format(&s)?;
104+
let s = forge_fmt::format(&s, FormatterConfig::default()).into_result()?;
104105
Ok(s)
105106
}
106107

crates/common/src/comments/mod.rs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub const DISABLE_START: &str = "forgefmt: disable-start";
1515
pub const DISABLE_END: &str = "forgefmt: disable-end";
1616

1717
pub struct Comments {
18-
comments: std::vec::IntoIter<Comment>,
18+
comments: std::collections::VecDeque<Comment>,
1919
}
2020

2121
impl fmt::Debug for Comments {
@@ -36,25 +36,32 @@ impl Comments {
3636
let gatherer = CommentGatherer::new(sf, sm, normalize_cmnts, tab_width).gather();
3737

3838
Self {
39-
comments: if group_cmnts {
40-
gatherer.group().into_iter()
41-
} else {
42-
gatherer.comments.into_iter()
43-
},
39+
comments: if group_cmnts { gatherer.group().into() } else { gatherer.comments.into() },
4440
}
4541
}
4642

4743
pub fn peek(&self) -> Option<&Comment> {
48-
self.comments.as_slice().first()
44+
self.comments.front()
4945
}
5046

5147
#[allow(clippy::should_implement_trait)]
5248
pub fn next(&mut self) -> Option<Comment> {
53-
self.comments.next()
49+
self.comments.pop_front()
5450
}
5551

5652
pub fn iter(&self) -> impl Iterator<Item = &Comment> {
57-
self.comments.as_slice().iter()
53+
self.comments.iter()
54+
}
55+
56+
/// Adds a new comment at the beginning of the list.
57+
///
58+
/// Should only be used when comments are gathered scattered, and must be manually sorted.
59+
///
60+
/// **WARNING:** This struct works under the assumption that comments are always sorted by
61+
/// ascending span position. It is the caller's responsibility to ensure that this premise
62+
/// always holds true.
63+
pub fn push_front(&mut self, cmnt: Comment) {
64+
self.comments.push_front(cmnt)
5865
}
5966

6067
/// Finds the first trailing comment on the same line as `span_pos`, allowing for `Mixed`
@@ -270,7 +277,7 @@ impl<'ast> CommentGatherer<'ast> {
270277
if let Some(line) = lines.next() {
271278
let line = line.trim_end();
272279
// Ensure first line of a doc comment only has the `/**` decorator
273-
if let Some((_, second)) = line.split_once("/**") {
280+
if is_doc && let Some((_, second)) = line.split_once("/**") {
274281
res.push("/**".to_string());
275282
if !second.trim().is_empty() {
276283
let line = normalize_block_comment_ws(second, col).trim_end();
@@ -298,10 +305,11 @@ impl<'ast> CommentGatherer<'ast> {
298305
if !pos.is_last {
299306
res.push(format_doc_block_comment(&line, self.tab_width));
300307
} else {
308+
// Ensure last line of a doc comment only has the `*/` decorator
301309
if let Some((first, _)) = line.split_once("*/")
302310
&& !first.trim().is_empty()
303311
{
304-
res.push(format_doc_block_comment(first, self.tab_width));
312+
res.push(format_doc_block_comment(first.trim_end(), self.tab_width));
305313
}
306314
res.push(" */".to_string());
307315
}
@@ -428,6 +436,11 @@ pub fn line_with_tabs(
428436
output.push_str(rest_of_line);
429437
}
430438

439+
/// Estimates the display width of a string, accounting for tabs.
440+
pub fn estimate_line_width(line: &str, tab_width: usize) -> usize {
441+
line.chars().fold(0, |width, c| width + if c == '\t' { tab_width } else { 1 })
442+
}
443+
431444
/// Returns the `BytePos` of the beginning of the current line.
432445
fn line_begin_pos(sf: &SourceFile, pos: BytePos) -> BytePos {
433446
let pos = sf.relative_position(pos);

0 commit comments

Comments
 (0)