diff --git a/.gitignore b/.gitignore index 64af508ada43c..2b54c7c9bf18a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_STORE /target* +/*.sol out/ snapshots/ out.json @@ -11,4 +12,4 @@ CLAUDE.md node_modules dist bin -_ \ No newline at end of file +_ diff --git a/Cargo.lock b/Cargo.lock index 87f5b19f4a726..502d67f237dbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4121,6 +4121,22 @@ dependencies = [ "tracing-subscriber 0.3.19", ] +[[package]] +name = "forge-fmt-2" +version = "1.3.2" +dependencies = [ + "foundry-common", + "foundry-config", + "itertools 0.14.0", + "similar", + "similar-asserts", + "snapbox", + "solar-parse", + "toml 0.9.5", + "tracing", + "tracing-subscriber 0.3.19", +] + [[package]] name = "forge-lint" version = "1.3.2" @@ -4450,6 +4466,7 @@ dependencies = [ "semver 1.0.26", "serde", "serde_json", + "solar-interface", "solar-parse", "solar-sema", "terminal_size", diff --git a/Cargo.toml b/Cargo.toml index 65a3a93fd4723..788c8a4f5239a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/evm/fuzz/", "crates/evm/traces/", "crates/fmt/", + "crates/fmt-2/", "crates/forge/", "crates/script-sequence/", "crates/macros/", @@ -297,10 +298,7 @@ bytes = "1.10" walkdir = "2" prettyplease = "0.2" base64 = "0.22" -chrono = { version = "0.4", default-features = false, features = [ - "clock", - "std", -] } +chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } axum = "0.8" ciborium = "0.2" color-eyre = "0.6" @@ -356,6 +354,9 @@ flate2 = "1.1" ## Pinned dependencies. Enabled for the workspace in crates/test-utils. +# testing +snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] } + # Use unicode-rs which has a smaller binary size than the default ICU4X as the IDNA backend, used # by the `url` crate. # See the `idna_adapter` README.md for more details: https://docs.rs/crate/idna_adapter/latest diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 16e858b2ccc54..ed8b40735fd92 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -44,6 +44,7 @@ alloy-transport.workspace = true alloy-consensus = { workspace = true, features = ["k256"] } alloy-network.workspace = true +solar-interface.workspace = true solar-parse.workspace = true solar-sema.workspace = true diff --git a/crates/common/src/comments/comment.rs b/crates/common/src/comments/comment.rs index b05a7bbdd1251..2d3a9a456ccaf 100644 --- a/crates/common/src/comments/comment.rs +++ b/crates/common/src/comments/comment.rs @@ -17,6 +17,21 @@ pub enum CommentStyle { BlankLine, } +impl CommentStyle { + pub fn is_mixed(&self) -> bool { + matches!(self, Self::Mixed) + } + pub fn is_trailing(&self) -> bool { + matches!(self, Self::Trailing) + } + pub fn is_isolated(&self) -> bool { + matches!(self, Self::Isolated) + } + pub fn is_blank(&self) -> bool { + matches!(self, Self::BlankLine) + } +} + #[derive(Clone, Debug)] pub struct Comment { pub lines: Vec, diff --git a/crates/common/src/comments/comments.rs b/crates/common/src/comments/comments.rs deleted file mode 100644 index fb13654f811fc..0000000000000 --- a/crates/common/src/comments/comments.rs +++ /dev/null @@ -1,193 +0,0 @@ -use super::comment::{Comment, CommentStyle}; -use solar_parse::{ - ast::{CommentKind, Span}, - interface::{source_map::SourceFile, BytePos, CharPos, SourceMap}, - lexer::token::RawTokenKind as TokenKind, -}; -use std::fmt; - -pub struct Comments { - comments: std::vec::IntoIter, -} - -impl fmt::Debug for Comments { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Comments")?; - f.debug_list().entries(self.iter()).finish() - } -} - -/// Returns `None` if the first `col` chars of `s` contain a non-whitespace char. -/// Otherwise returns `Some(k)` where `k` is first char offset after that leading -/// whitespace. Note that `k` may be outside bounds of `s`. -fn all_whitespace(s: &str, col: CharPos) -> Option { - let mut idx = 0; - for (i, ch) in s.char_indices().take(col.to_usize()) { - if !ch.is_whitespace() { - return None; - } - idx = i + ch.len_utf8(); - } - Some(idx) -} - -fn trim_whitespace_prefix(s: &str, col: CharPos) -> &str { - let len = s.len(); - match all_whitespace(s, col) { - Some(col) => { - if col < len { - &s[col..] - } else { - "" - } - } - None => s, - } -} - -fn split_block_comment_into_lines(text: &str, col: CharPos) -> Vec { - let mut res: Vec = vec![]; - let mut lines = text.lines(); - // just push the first line - res.extend(lines.next().map(|it| it.to_string())); - // for other lines, strip common whitespace prefix - for line in lines { - res.push(trim_whitespace_prefix(line, col).to_string()) - } - res -} - -/// Returns the `BytePos` of the beginning of the current line. -fn line_begin_pos(sf: &SourceFile, pos: BytePos) -> BytePos { - let pos = sf.relative_position(pos); - let line_index = sf.lookup_line(pos).unwrap(); - let line_start_pos = sf.lines()[line_index]; - sf.absolute_position(line_start_pos) -} - -fn gather_comments(sf: &SourceFile) -> Vec { - let text = sf.src.as_str(); - let start_bpos = sf.start_pos; - let mut pos = 0; - let mut comments: Vec = Vec::new(); - let mut code_to_the_left = false; - - let make_span = |range: std::ops::Range| { - Span::new(start_bpos + range.start as u32, start_bpos + range.end as u32) - }; - - /* - if let Some(shebang_len) = strip_shebang(text) { - comments.push(Comment { - style: CommentStyle::Isolated, - lines: vec![text[..shebang_len].to_string()], - pos: start_bpos, - }); - pos += shebang_len; - } - */ - - for token in solar_parse::Cursor::new(&text[pos..]) { - let token_range = pos..pos + token.len as usize; - let span = make_span(token_range.clone()); - let token_text = &text[token_range]; - match token.kind { - TokenKind::Whitespace => { - if let Some(mut idx) = token_text.find('\n') { - code_to_the_left = false; - - // NOTE(dani): this used to be `while`, but we want only a single blank line. - if let Some(next_newline) = token_text[idx + 1..].find('\n') { - idx += 1 + next_newline; - let pos = pos + idx; - comments.push(Comment { - is_doc: false, - kind: CommentKind::Line, - style: CommentStyle::BlankLine, - lines: vec![], - span: make_span(pos..pos), - }); - } - } - } - TokenKind::BlockComment { is_doc, .. } => { - let code_to_the_right = - !matches!(text[pos + token.len as usize..].chars().next(), Some('\r' | '\n')); - let style = match (code_to_the_left, code_to_the_right) { - (_, true) => CommentStyle::Mixed, - (false, false) => CommentStyle::Isolated, - (true, false) => CommentStyle::Trailing, - }; - let kind = CommentKind::Block; - - // Count the number of chars since the start of the line by rescanning. - let pos_in_file = start_bpos + BytePos(pos as u32); - let line_begin_in_file = line_begin_pos(sf, pos_in_file); - let line_begin_pos = (line_begin_in_file - start_bpos).to_usize(); - let col = CharPos(text[line_begin_pos..pos].chars().count()); - - let lines = split_block_comment_into_lines(token_text, col); - comments.push(Comment { is_doc, kind, style, lines, span }) - } - TokenKind::LineComment { is_doc } => { - comments.push(Comment { - is_doc, - kind: CommentKind::Line, - style: if code_to_the_left { - CommentStyle::Trailing - } else { - CommentStyle::Isolated - }, - lines: vec![token_text.to_string()], - span, - }); - } - _ => { - code_to_the_left = true; - } - } - pos += token.len as usize; - } - - comments -} - -impl Comments { - pub fn new(sf: &SourceFile) -> Self { - Self { comments: gather_comments(sf).into_iter() } - } - - pub fn peek(&self) -> Option<&Comment> { - self.comments.as_slice().first() - } - - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option { - self.comments.next() - } - - pub fn iter(&self) -> impl Iterator { - self.comments.as_slice().iter() - } - - pub fn trailing_comment( - &mut self, - sm: &SourceMap, - span: Span, - next_pos: Option, - ) -> Option { - if let Some(cmnt) = self.peek() { - if cmnt.style != CommentStyle::Trailing { - return None; - } - let span_line = sm.lookup_char_pos(span.hi()); - let comment_line = sm.lookup_char_pos(cmnt.pos()); - let next = next_pos.unwrap_or_else(|| cmnt.pos() + BytePos(1)); - if span.hi() < cmnt.pos() && cmnt.pos() < next && span_line.line == comment_line.line { - return Some(self.next().unwrap()); - } - } - - None - } -} diff --git a/crates/common/src/comments/inline_config.rs b/crates/common/src/comments/inline_config.rs new file mode 100644 index 0000000000000..385dd67acc61a --- /dev/null +++ b/crates/common/src/comments/inline_config.rs @@ -0,0 +1,477 @@ +use solar_interface::{SourceMap, Span}; +use solar_parse::ast::{Item, SourceUnit, visit::Visit as VisitAst}; +use solar_sema::hir::{self, Visit as VisitHir}; +use std::{collections::HashMap, hash::Hash, marker::PhantomData, ops::ControlFlow}; + +/// A disabled formatting range. `loose` designates that the range includes any loc which +/// may start in between start and end, whereas the strict version requires that +/// `range.start >= loc.start <=> loc.end <= range.end` +#[derive(Debug, Clone, Copy)] +struct DisabledRange { + start: usize, + end: usize, + loose: bool, +} + +impl DisabledRange { + fn includes(&self, range: std::ops::Range) -> bool { + range.start >= self.start && (if self.loose { range.start } else { range.end } <= self.end) + } +} + +/// An inline config item +#[derive(Clone, Debug)] +pub enum InlineConfigItem { + /// Disables the next code (AST) item regardless of newlines + DisableNextItem(I), + /// Disables formatting on the current line + DisableLine(I), + /// Disables formatting between the next newline and the newline after + DisableNextLine(I), + /// Disables formatting for any code that follows this and before the next "disable-end" + DisableStart(I), + /// Disables formatting for any code that precedes this and after the previous "disable-start" + DisableEnd(I), +} + +impl InlineConfigItem> { + /// Parse an inline config item from a string. Validates IDs against available IDs. + pub fn parse(s: &str, available_ids: &[&str]) -> Result { + let (disable, relevant) = s.split_once('(').unwrap_or((s, "")); + let ids = if relevant.is_empty() || relevant == "all)" { + vec!["all".to_string()] + } else { + match relevant.split_once(')') { + Some((id_str, _)) => id_str.split(",").map(|s| s.trim().to_string()).collect(), + None => return Err(InvalidInlineConfigItem::Syntax(s.into())), + } + }; + + // Validate IDs + let mut invalid_ids = Vec::new(); + 'ids: for id in &ids { + if id == "all" { + continue; + } + for available_id in available_ids { + if *available_id == id { + continue 'ids; + } + } + invalid_ids.push(id.to_owned()); + } + + if !invalid_ids.is_empty() { + return Err(InvalidInlineConfigItem::Ids(invalid_ids)); + } + + let res = match disable { + "disable-next-item" => Self::DisableNextItem(ids), + "disable-line" => Self::DisableLine(ids), + "disable-next-line" => Self::DisableNextLine(ids), + "disable-start" => Self::DisableStart(ids), + "disable-end" => Self::DisableEnd(ids), + s => return Err(InvalidInlineConfigItem::Syntax(s.into())), + }; + + Ok(res) + } +} + +impl std::str::FromStr for InlineConfigItem<()> { + type Err = InvalidInlineConfigItem; + fn from_str(s: &str) -> Result { + Ok(match s { + "disable-next-item" => Self::DisableNextItem(()), + "disable-line" => Self::DisableLine(()), + "disable-next-line" => Self::DisableNextLine(()), + "disable-start" => Self::DisableStart(()), + "disable-end" => Self::DisableEnd(()), + s => return Err(InvalidInlineConfigItem::Syntax(s.into())), + }) + } +} + +#[derive(Debug)] +pub enum InvalidInlineConfigItem { + Syntax(String), + Ids(Vec), +} + +impl std::fmt::Display for InvalidInlineConfigItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Syntax(s) => write!(f, "invalid inline config item: {s}"), + Self::Ids(ids) => { + write!(f, "unknown id: '{}'", ids.join("', '")) + } + } + } +} + +/// A trait for `InlineConfigItem` types that can be iterated over to produce keys for storage. +pub trait ItemIdIterator { + type Item: Eq + Hash + Clone; + fn into_iter(self) -> impl IntoIterator; +} + +impl ItemIdIterator for () { + type Item = (); + fn into_iter(self) -> impl IntoIterator { + std::iter::once(()) + } +} + +impl ItemIdIterator for Vec { + type Item = String; + fn into_iter(self) -> impl IntoIterator { + self + } +} + +#[derive(Debug, Default)] +pub struct InlineConfig { + disabled_ranges: HashMap>, +} + +impl InlineConfig { + /// Build a new inline config with an iterator of inline config items and their locations in a + /// source file. + /// + /// # Panics + /// + /// Panics if `items` is not sorted in ascending order of [`Span`]s. + pub fn from_ast<'ast>( + items: impl IntoIterator)>, + ast: &'ast SourceUnit<'ast>, + source_map: &SourceMap, + ) -> Self { + Self::build(items, source_map, |offset| NextItemFinderAst::new(offset).find(ast)) + } + + /// Build a new inline config with an iterator of inline config items and their locations in a + /// source file. + /// + /// # Panics + /// + /// Panics if `items` is not sorted in ascending order of [`Span`]s. + pub fn from_hir<'hir>( + items: impl IntoIterator)>, + hir: &'hir hir::Hir<'hir>, + source_id: hir::SourceId, + source_map: &SourceMap, + ) -> Self { + Self::build(items, source_map, |offset| NextItemFinderHir::new(offset, hir).find(source_id)) + } + + fn build( + items: impl IntoIterator)>, + source_map: &SourceMap, + mut find_next_item: impl FnMut(usize) -> Option, + ) -> Self { + let mut disabled_ranges: HashMap> = HashMap::new(); + let mut disabled_blocks: HashMap = HashMap::new(); + + let mut prev_sp = Span::DUMMY; + for (sp, item) in items { + if cfg!(debug_assertions) { + assert!(sp >= prev_sp, "InlineConfig::new: unsorted items: {sp:?} < {prev_sp:?}"); + prev_sp = sp; + } + + let Ok((file, comment_range)) = source_map.span_to_source(sp) else { continue }; + let src = file.src.as_str(); + match item { + InlineConfigItem::DisableNextItem(ids) => { + if let Some(next_item) = find_next_item(sp.hi().to_usize()) { + for id in ids.into_iter() { + disabled_ranges.entry(id).or_default().push(DisabledRange { + start: next_item.lo().to_usize(), + end: next_item.hi().to_usize(), + loose: false, + }); + } + } + } + InlineConfigItem::DisableLine(ids) => { + let start = src[..comment_range.start].rfind('\n').map_or(0, |i| i); + let end = src[comment_range.end..] + .find('\n') + .map_or(src.len(), |i| comment_range.end + i); + + for id in ids.into_iter() { + disabled_ranges.entry(id).or_default().push(DisabledRange { + start: start + file.start_pos.to_usize(), + end: end + file.start_pos.to_usize(), + loose: false, + }) + } + } + InlineConfigItem::DisableNextLine(ids) => { + if let Some(offset) = src[comment_range.end..].find('\n') { + let next_line = comment_range.end + offset + 1; + if next_line < src.len() { + let end = + src[next_line..].find('\n').map_or(src.len(), |i| next_line + i); + for id in ids.into_iter() { + disabled_ranges.entry(id).or_default().push(DisabledRange { + start: comment_range.start + file.start_pos.to_usize(), + end: end + file.start_pos.to_usize(), + loose: false, + }) + } + } + } + } + InlineConfigItem::DisableStart(ids) => { + for id in ids.into_iter() { + disabled_blocks + .entry(id) + .and_modify(|(_, depth, _)| *depth += 1) + .or_insert(( + sp.lo().to_usize(), + 1, + // Use file end as fallback for unclosed blocks + file.start_pos.to_usize() + src.len(), + )); + } + } + InlineConfigItem::DisableEnd(ids) => { + for id in ids.into_iter() { + if let Some((start, depth, _)) = disabled_blocks.get_mut(&id) { + *depth = depth.saturating_sub(1); + + if *depth == 0 { + let start = *start; + _ = disabled_blocks.remove(&id); + + disabled_ranges.entry(id).or_default().push(DisabledRange { + start, + end: sp.hi().to_usize(), + loose: false, + }) + } + } + } + } + } + } + + for (id, (start, _, file_end)) in disabled_blocks { + disabled_ranges.entry(id).or_default().push(DisabledRange { + start, + end: file_end, + loose: false, + }); + } + + Self { disabled_ranges } + } +} + +impl InlineConfig +where + I: ItemIdIterator, + I::Item: Clone + Eq + Hash, +{ + /// Checks if a span is disabled (only applicable when inline config doesn't require an id). + pub fn is_disabled(&self, span: Span) -> bool + where + I: ItemIdIterator, + { + if let Some(ranges) = self.disabled_ranges.get(&()) { + return ranges.iter().any(|range| range.includes(span.to_range())); + } + false + } + + /// Checks if a span is disabled for a specific id. Also checks against "all", which disables + /// all rules. + pub fn is_disabled_with_id(&self, span: Span, id: &str) -> bool + where + I::Item: std::borrow::Borrow, + { + if let Some(ranges) = self.disabled_ranges.get(id) + && ranges.iter().any(|range| range.includes(span.to_range())) + { + return true; + } + + if let Some(ranges) = self.disabled_ranges.get("all") + && ranges.iter().any(|range| range.includes(span.to_range())) + { + return true; + } + + false + } +} + +/// An AST visitor that finds the first `Item` that starts after a given offset. +#[derive(Debug, Default)] +struct NextItemFinderAst<'ast> { + /// The offset to search after. + offset: usize, + _pd: PhantomData<&'ast ()>, +} + +impl<'ast> NextItemFinderAst<'ast> { + fn new(offset: usize) -> Self { + Self { offset, _pd: PhantomData } + } + + /// Finds the next AST item or statement which a span that begins after the `offset`. + fn find(&mut self, ast: &'ast SourceUnit<'ast>) -> Option { + match self.visit_source_unit(ast) { + ControlFlow::Break(span) => Some(span), + ControlFlow::Continue(()) => None, + } + } +} + +impl<'ast> VisitAst<'ast> for NextItemFinderAst<'ast> { + type BreakValue = Span; + + fn visit_item(&mut self, item: &'ast Item<'ast>) -> ControlFlow { + // Check if this item starts after the offset. + if item.span.lo().to_usize() > self.offset { + return ControlFlow::Break(item.span); + } + + // Otherwise, continue traversing inside this item. + self.walk_item(item) + } + + fn visit_stmt( + &mut self, + stmt: &'ast solar_sema::ast::Stmt<'ast>, + ) -> ControlFlow { + // Check if this stmt starts after the offset. + if stmt.span.lo().to_usize() > self.offset { + return ControlFlow::Break(stmt.span); + } + + // Otherwise, continue traversing inside this stmt. + self.walk_stmt(stmt) + } +} + +/// A HIR visitor that finds the first `Item` that starts after a given offset. +#[derive(Debug)] +struct NextItemFinderHir<'hir> { + hir: &'hir hir::Hir<'hir>, + /// The offset to search after. + offset: usize, +} + +impl<'hir> NextItemFinderHir<'hir> { + fn new(offset: usize, hir: &'hir hir::Hir<'hir>) -> Self { + Self { offset, hir } + } + + /// Finds the next HIR item which a span that begins after the `offset`. + fn find(&mut self, id: hir::SourceId) -> Option { + match self.visit_nested_source(id) { + ControlFlow::Break(span) => Some(span), + ControlFlow::Continue(()) => None, + } + } +} + +impl<'hir> VisitHir<'hir> for NextItemFinderHir<'hir> { + type BreakValue = Span; + + fn hir(&self) -> &'hir hir::Hir<'hir> { + self.hir + } + + fn visit_item(&mut self, item: hir::Item<'hir, 'hir>) -> ControlFlow { + // Check if this item starts after the offset. + if item.span().lo().to_usize() > self.offset { + return ControlFlow::Break(item.span()); + } + + // If the item is before the offset, skip traverse. + if item.span().hi().to_usize() < self.offset { + return ControlFlow::Continue(()); + } + + // Otherwise, continue traversing inside this item. + self.walk_item(item) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_disabled_range_includes() { + // Strict mode - requires full containment + let strict = DisabledRange { start: 10, end: 20, loose: false }; + assert!(strict.includes(10..20)); + assert!(strict.includes(12..18)); + assert!(!strict.includes(5..15)); // Partial overlap fails + + // Loose mode - only checks start position + let loose = DisabledRange { start: 10, end: 20, loose: true }; + assert!(loose.includes(10..25)); // Start in range + assert!(!loose.includes(5..15)); // Start before range + } + + #[test] + fn test_inline_config_item_from_str() { + assert!(matches!( + "disable-next-item".parse::>().unwrap(), + InlineConfigItem::DisableNextItem(()) + )); + assert!(matches!( + "disable-line".parse::>().unwrap(), + InlineConfigItem::DisableLine(()) + )); + assert!(matches!( + "disable-start".parse::>().unwrap(), + InlineConfigItem::DisableStart(()) + )); + assert!(matches!( + "disable-end".parse::>().unwrap(), + InlineConfigItem::DisableEnd(()) + )); + assert!("invalid".parse::>().is_err()); + } + + #[test] + fn test_inline_config_item_parse_with_lints() { + let lint_ids = vec!["lint1", "lint2"]; + + // No lints = "all" + match InlineConfigItem::parse("disable-line", &lint_ids).unwrap() { + InlineConfigItem::DisableLine(lints) => assert_eq!(lints, vec!["all"]), + _ => panic!("Wrong type"), + } + + // Valid single lint + match InlineConfigItem::parse("disable-start(lint1)", &lint_ids).unwrap() { + InlineConfigItem::DisableStart(lints) => assert_eq!(lints, vec!["lint1"]), + _ => panic!("Wrong type"), + } + + // Multiple lints with spaces + match InlineConfigItem::parse("disable-end(lint1, lint2)", &lint_ids).unwrap() { + InlineConfigItem::DisableEnd(lints) => assert_eq!(lints, vec!["lint1", "lint2"]), + _ => panic!("Wrong type"), + } + + // Invalid lint ID + assert!(matches!( + InlineConfigItem::parse("disable-line(unknown)", &lint_ids), + Err(InvalidInlineConfigItem::Ids(_)) + )); + + // Malformed syntax + assert!(matches!( + InlineConfigItem::parse("disable-line(lint1", &lint_ids), + Err(InvalidInlineConfigItem::Syntax(_)) + )); + } +} diff --git a/crates/common/src/comments/mod.rs b/crates/common/src/comments/mod.rs index 6ef9767e13883..f86637b357139 100644 --- a/crates/common/src/comments/mod.rs +++ b/crates/common/src/comments/mod.rs @@ -1,6 +1,4 @@ -mod comment; - -use comment::{Comment, CommentStyle}; +use crate::iter::IterDelimited; use solar_parse::{ ast::{CommentKind, Span}, interface::{BytePos, CharPos, SourceMap, source_map::SourceFile}, @@ -8,6 +6,14 @@ use solar_parse::{ }; use std::fmt; +mod comment; +pub use comment::{Comment, CommentStyle}; + +pub mod inline_config; + +pub const DISABLE_START: &str = "forgefmt: disable-start"; +pub const DISABLE_END: &str = "forgefmt: disable-end"; + pub struct Comments { comments: std::vec::IntoIter, } @@ -19,103 +25,197 @@ impl fmt::Debug for Comments { } } -/// Returns `None` if the first `col` chars of `s` contain a non-whitespace char. -/// Otherwise returns `Some(k)` where `k` is first char offset after that leading -/// whitespace. Note that `k` may be outside bounds of `s`. -fn all_whitespace(s: &str, col: CharPos) -> Option { - let mut idx = 0; - for (i, ch) in s.char_indices().take(col.to_usize()) { - if !ch.is_whitespace() { - return None; +impl Comments { + pub fn new( + sf: &SourceFile, + sm: &SourceMap, + normalize_cmnts: bool, + group_cmnts: bool, + tab_width: Option, + ) -> Self { + let gatherer = CommentGatherer::new(sf, sm, normalize_cmnts, tab_width).gather(); + + Self { + comments: if group_cmnts { + gatherer.group().into_iter() + } else { + gatherer.comments.into_iter() + }, } - idx = i + ch.len_utf8(); } - Some(idx) -} -fn trim_whitespace_prefix(s: &str, col: CharPos) -> &str { - let len = s.len(); - match all_whitespace(s, col) { - Some(col) => { - if col < len { - &s[col..] - } else { - "" + pub fn peek(&self) -> Option<&Comment> { + self.comments.as_slice().first() + } + + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option { + self.comments.next() + } + + pub fn iter(&self) -> impl Iterator { + self.comments.as_slice().iter() + } + + /// Finds the first trailing comment on the same line as `span_pos`, allowing for `Mixed` + /// style comments to appear before it. + /// + /// Returns the comment and its index in the buffer. + pub fn peek_trailing( + &self, + sm: &SourceMap, + span_pos: BytePos, + next_pos: Option, + ) -> Option<(&Comment, usize)> { + let span_line = sm.lookup_char_pos(span_pos).line; + for (i, cmnt) in self.iter().enumerate() { + // If we have moved to the next line, we can stop. + let comment_line = sm.lookup_char_pos(cmnt.pos()).line; + if comment_line != span_line { + break; + } + + // The comment must start after the given span position. + if cmnt.pos() < span_pos { + continue; + } + + // The comment must be before the next element. + if cmnt.pos() >= next_pos.unwrap_or_else(|| cmnt.pos() + BytePos(1)) { + break; + } + + // Stop when we find a trailing or a non-mixed comment + match cmnt.style { + CommentStyle::Mixed => continue, + CommentStyle::Trailing => return Some((cmnt, i)), + _ => break, } } - None => s, + None } } -fn split_block_comment_into_lines(text: &str, col: CharPos) -> Vec { - let mut res: Vec = vec![]; - let mut lines = text.lines(); - // just push the first line - res.extend(lines.next().map(|it| it.to_string())); - // for other lines, strip common whitespace prefix - for line in lines { - res.push(trim_whitespace_prefix(line, col).to_string()) - } - res +struct CommentGatherer<'ast> { + sf: &'ast SourceFile, + sm: &'ast SourceMap, + text: &'ast str, + start_bpos: BytePos, + pos: usize, + comments: Vec, + code_to_the_left: bool, + disabled_block_depth: usize, + tab_width: Option, } -/// Returns the `BytePos` of the beginning of the current line. -fn line_begin_pos(sf: &SourceFile, pos: BytePos) -> BytePos { - let pos = sf.relative_position(pos); - let line_index = sf.lookup_line(pos).unwrap(); - let line_start_pos = sf.lines()[line_index]; - sf.absolute_position(line_start_pos) -} +impl<'ast> CommentGatherer<'ast> { + fn new( + sf: &'ast SourceFile, + sm: &'ast SourceMap, + normalize_cmnts: bool, + tab_width: Option, + ) -> Self { + Self { + sf, + sm, + text: sf.src.as_str(), + start_bpos: sf.start_pos, + pos: 0, + comments: Vec::new(), + code_to_the_left: false, + disabled_block_depth: if normalize_cmnts { 0 } else { 1 }, + tab_width, + } + } -fn gather_comments(sf: &SourceFile) -> Vec { - let text = sf.src.as_str(); - let start_bpos = sf.start_pos; - let mut pos = 0; - let mut comments: Vec = Vec::new(); - let mut code_to_the_left = false; + /// Consumes the gatherer and returns the collected comments. + fn gather(mut self) -> Self { + for token in solar_parse::Cursor::new(&self.text[self.pos..]) { + self.process_token(token); + } + self + } - let make_span = |range: std::ops::Range| { - Span::new(start_bpos + range.start as u32, start_bpos + range.end as u32) - }; + /// Post-processes a list of comments to group consecutive comments. + /// + /// Necessary for properly indenting multi-line trailing comments, which would + /// otherwise be parsed as a `Trailing` followed by several `Isolated`. + fn group(self) -> Vec { + let mut processed = Vec::new(); + let mut cursor = self.comments.into_iter().peekable(); + + while let Some(mut current) = cursor.next() { + if current.kind == CommentKind::Line + && (current.style.is_trailing() || current.style.is_isolated()) + { + let mut ref_line = self.sm.lookup_char_pos(current.span.hi()).line; + while let Some(next_comment) = cursor.peek() { + if !next_comment.style.is_isolated() + || next_comment.kind != CommentKind::Line + || ref_line + 1 != self.sm.lookup_char_pos(next_comment.span.lo()).line + { + break; + } + + let next_to_merge = cursor.next().unwrap(); + current.lines.extend(next_to_merge.lines); + current.span = current.span.to(next_to_merge.span); + ref_line += 1; + } + } + + processed.push(current); + } + + processed + } + + /// Creates a `Span` relative to the source file's start position. + fn make_span(&self, range: std::ops::Range) -> Span { + Span::new(self.start_bpos + range.start as u32, self.start_bpos + range.end as u32) + } + + /// Processes a single token from the source. + fn process_token(&mut self, token: solar_parse::lexer::token::RawToken) { + let token_range = self.pos..self.pos + token.len as usize; + let span = self.make_span(token_range.clone()); + let token_text = &self.text[token_range]; + + // Keep track of disabled blocks + if token_text.trim_start().contains(DISABLE_START) { + self.disabled_block_depth += 1; + } else if token_text.trim_start().contains(DISABLE_END) { + self.disabled_block_depth -= 1; + } - /* - if let Some(shebang_len) = strip_shebang(text) { - comments.push(Comment { - style: CommentStyle::Isolated, - lines: vec![text[..shebang_len].to_string()], - pos: start_bpos, - }); - pos += shebang_len; - } - */ - - for token in solar_parse::Cursor::new(&text[pos..]) { - let token_range = pos..pos + token.len as usize; - let span = make_span(token_range.clone()); - let token_text = &text[token_range]; match token.kind { TokenKind::Whitespace => { if let Some(mut idx) = token_text.find('\n') { - code_to_the_left = false; + self.code_to_the_left = false; - // NOTE(dani): this used to be `while`, but we want only a single blank line. - if let Some(next_newline) = token_text[idx + 1..].find('\n') { + while let Some(next_newline) = token_text[idx + 1..].find('\n') { idx += 1 + next_newline; - let pos = pos + idx; - comments.push(Comment { + let pos = self.pos + idx; + self.comments.push(Comment { is_doc: false, kind: CommentKind::Line, style: CommentStyle::BlankLine, lines: vec![], - span: make_span(pos..pos), + span: self.make_span(pos..pos), }); + // If not disabled, early-exit as we want only a single blank line. + if self.disabled_block_depth == 0 { + break; + } } } } TokenKind::BlockComment { is_doc, .. } => { - let code_to_the_right = - !matches!(text[pos + token.len as usize..].chars().next(), Some('\r' | '\n')); - let style = match (code_to_the_left, code_to_the_right) { + let code_to_the_right = !matches!( + self.text[self.pos + token.len as usize..].chars().next(), + Some('\r' | '\n') + ); + let style = match (self.code_to_the_left, code_to_the_right) { (_, true) => CommentStyle::Mixed, (false, false) => CommentStyle::Isolated, (true, false) => CommentStyle::Trailing, @@ -123,73 +223,215 @@ fn gather_comments(sf: &SourceFile) -> Vec { let kind = CommentKind::Block; // Count the number of chars since the start of the line by rescanning. - let pos_in_file = start_bpos + BytePos(pos as u32); - let line_begin_in_file = line_begin_pos(sf, pos_in_file); - let line_begin_pos = (line_begin_in_file - start_bpos).to_usize(); - let col = CharPos(text[line_begin_pos..pos].chars().count()); + let pos_in_file = self.start_bpos + BytePos(self.pos as u32); + let line_begin_in_file = line_begin_pos(self.sf, pos_in_file); + let line_begin_pos = (line_begin_in_file - self.start_bpos).to_usize(); + let col = CharPos(self.text[line_begin_pos..self.pos].chars().count()); - let lines = split_block_comment_into_lines(token_text, col); - comments.push(Comment { is_doc, kind, style, lines, span }) + let lines = self.split_block_comment_into_lines(token_text, is_doc, col); + self.comments.push(Comment { is_doc, kind, style, lines, span }) } TokenKind::LineComment { is_doc } => { - comments.push(Comment { + let line = + if self.disabled_block_depth != 0 { token_text } else { token_text.trim_end() }; + self.comments.push(Comment { is_doc, kind: CommentKind::Line, - style: if code_to_the_left { + style: if self.code_to_the_left { CommentStyle::Trailing } else { CommentStyle::Isolated }, - lines: vec![token_text.to_string()], + lines: vec![line.into()], span, }); } _ => { - code_to_the_left = true; + self.code_to_the_left = true; } } - pos += token.len as usize; + self.pos += token.len as usize; } - comments + /// Splits a block comment into lines, ensuring that each line is properly formatted. + fn split_block_comment_into_lines( + &self, + text: &str, + is_doc: bool, + col: CharPos, + ) -> Vec { + // if formatting is disabled, return as is + if self.disabled_block_depth != 0 { + return vec![text.into()]; + } + + let mut res: Vec = vec![]; + let mut lines = text.lines(); + if let Some(line) = lines.next() { + let line = line.trim_end(); + // Ensure first line of a doc comment only has the `/**` decorator + if let Some((_, second)) = line.split_once("/**") { + res.push("/**".to_string()); + if !second.trim().is_empty() { + let line = normalize_block_comment_ws(second, col).trim_end(); + // Ensure last line of a doc comment only has the `*/` decorator + if let Some((first, _)) = line.split_once("*/") { + if !first.trim().is_empty() { + res.push(format_doc_block_comment(first.trim_end(), self.tab_width)); + } + res.push(" */".to_string()); + } else { + res.push(format_doc_block_comment(line.trim_end(), self.tab_width)); + } + } + } else { + res.push(line.to_string()); + } + } + + for (pos, line) in lines.delimited() { + let line = normalize_block_comment_ws(line, col).trim_end().to_string(); + if !is_doc { + res.push(line); + continue; + } + if !pos.is_last { + res.push(format_doc_block_comment(&line, self.tab_width)); + } else { + if let Some((first, _)) = line.split_once("*/") + && !first.trim().is_empty() + { + res.push(format_doc_block_comment(first, self.tab_width)); + } + res.push(" */".to_string()); + } + } + res + } } -impl Comments { - pub fn new(sf: &SourceFile) -> Self { - Self { comments: gather_comments(sf).into_iter() } +/// Returns `None` if the first `col` chars of `s` contain a non-whitespace char. +/// Otherwise returns `Some(k)` where `k` is first char offset after that leading +/// whitespace. Note that `k` may be outside bounds of `s`. +fn all_whitespace(s: &str, col: CharPos) -> Option { + let mut idx = 0; + for (i, ch) in s.char_indices().take(col.to_usize()) { + if !ch.is_whitespace() { + return None; + } + idx = i + ch.len_utf8(); } + Some(idx) +} - pub fn peek(&self) -> Option<&Comment> { - self.comments.as_slice().first() +/// Returns `Some(k)` where `k` is the byte offset of the first non-whitespace char. Returns `k = 0` +/// if `s` starts with a non-whitespace char. If `s` only contains whitespaces, returns `None`. +fn first_non_whitespace(s: &str) -> Option { + let mut len = 0; + for (i, ch) in s.char_indices() { + if ch.is_whitespace() { + len = ch.len_utf8() + } else { + return if i == 0 { Some(0) } else { Some(i + 1 - len) }; + } } + None +} - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option { - self.comments.next() +/// Returns a slice of `s` with a whitespace prefix removed based on `col`. If the first `col` chars +/// of `s` are all whitespace, returns a slice starting after that prefix. +fn normalize_block_comment_ws(s: &str, col: CharPos) -> &str { + let len = s.len(); + if let Some(col) = all_whitespace(s, col) { + return if col < len { &s[col..] } else { "" }; } + if let Some(col) = first_non_whitespace(s) { + return &s[col..]; + } + s +} - pub fn iter(&self) -> impl Iterator { - self.comments.as_slice().iter() +/// Formats a doc block comment line so that they have the ` *` decorator. +fn format_doc_block_comment(line: &str, tab_width: Option) -> String { + if line.is_empty() { + return (" *").to_string(); } - pub fn trailing_comment( - &mut self, - sm: &SourceMap, - span: Span, - next_pos: Option, - ) -> Option { - if let Some(cmnt) = self.peek() { - if cmnt.style != CommentStyle::Trailing { - return None; + if let Some((_, rest_of_line)) = line.split_once("*") { + if rest_of_line.is_empty() { + (" *").to_string() + } else if let Some(tab_width) = tab_width { + let mut normalized = String::from(" *"); + line_with_tabs( + &mut normalized, + rest_of_line, + tab_width, + Some(Consolidation::MinOneTab), + ); + normalized + } else { + format!(" *{rest_of_line}",) + } + } else if let Some(tab_width) = tab_width { + let mut normalized = String::from(" *\t"); + line_with_tabs(&mut normalized, line, tab_width, Some(Consolidation::WithoutSpaces)); + normalized + } else { + format!(" * {line}") + } +} + +pub enum Consolidation { + MinOneTab, + WithoutSpaces, +} + +/// Normalizes the leading whitespace of a string slice according to a given tab width. +/// +/// It aggregates and converts leading whitespace (spaces and tabs) into a representation that +/// maximizes the amount of tabs. +pub fn line_with_tabs( + output: &mut String, + line: &str, + tab_width: usize, + strategy: Option, +) { + // Find the end of the leading whitespace (any sequence of spaces and tabs) + let first_non_ws = line.find(|c| c != ' ' && c != '\t').unwrap_or(line.len()); + let (leading_ws, rest_of_line) = line.split_at(first_non_ws); + + // Compute its equivalent length and derive the required amount of tabs and spaces + let total_width = + leading_ws.chars().fold(0, |width, c| width + if c == ' ' { 1 } else { tab_width }); + let (mut num_tabs, mut num_spaces) = (total_width / tab_width, total_width % tab_width); + + // Adjust based on the desired config + match strategy { + Some(Consolidation::MinOneTab) => { + if num_tabs == 0 && num_spaces != 0 { + (num_tabs, num_spaces) = (1, 0); + } else if num_spaces != 0 { + (num_tabs, num_spaces) = (num_tabs + 1, 0); } - let span_line = sm.lookup_char_pos(span.hi()); - let comment_line = sm.lookup_char_pos(cmnt.pos()); - let next = next_pos.unwrap_or_else(|| cmnt.pos() + BytePos(1)); - if span.hi() < cmnt.pos() && cmnt.pos() < next && span_line.line == comment_line.line { - return Some(self.next().unwrap()); + } + Some(Consolidation::WithoutSpaces) => { + if num_spaces != 0 { + (num_tabs, num_spaces) = (num_tabs + 1, 0); } } + None => (), + }; - None - } + // Append the normalized indentation and the rest of the line to the output + output.extend(std::iter::repeat_n('\t', num_tabs)); + output.extend(std::iter::repeat_n(' ', num_spaces)); + output.push_str(rest_of_line); +} + +/// Returns the `BytePos` of the beginning of the current line. +fn line_begin_pos(sf: &SourceFile, pos: BytePos) -> BytePos { + let pos = sf.relative_position(pos); + let line_index = sf.lookup_line(pos).unwrap(); + let line_start_pos = sf.lines()[line_index]; + sf.absolute_position(line_start_pos) } diff --git a/crates/common/src/iter.rs b/crates/common/src/iter.rs new file mode 100644 index 0000000000000..09d16c5f30ae4 --- /dev/null +++ b/crates/common/src/iter.rs @@ -0,0 +1,31 @@ +use std::iter::Peekable; + +pub struct Delimited { + is_first: bool, + iter: Peekable, +} + +pub trait IterDelimited: Iterator + Sized { + fn delimited(self) -> Delimited { + Delimited { is_first: true, iter: self.peekable() } + } +} + +impl IterDelimited for I {} + +pub struct IteratorPosition { + pub is_first: bool, + pub is_last: bool, +} + +impl Iterator for Delimited { + type Item = (IteratorPosition, I::Item); + + fn next(&mut self) -> Option { + let item = self.iter.next()?; + let position = + IteratorPosition { is_first: self.is_first, is_last: self.iter.peek().is_none() }; + self.is_first = false; + Some((position, item)) + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index fc3b4884c2d5e..a0b0c413f6c05 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -26,6 +26,7 @@ pub mod contracts; pub mod errors; pub mod evm; pub mod fs; +pub mod iter; mod preprocessor; pub mod provider; pub mod reports; diff --git a/crates/config/src/fmt.rs b/crates/config/src/fmt.rs index 223b46d2ffd6b..50e46e05af1e5 100644 --- a/crates/config/src/fmt.rs +++ b/crates/config/src/fmt.rs @@ -37,16 +37,17 @@ pub struct FormatterConfig { pub sort_imports: bool, } -/// Style of uint/int256 types -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +/// Style of integer types. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum IntTypes { - /// Print the explicit uint256 or int256 + /// Use the type defined in the source code. + Preserve, + /// Print the full length `uint256` or `int256`. + #[default] Long, - /// Print the implicit uint or int + /// Print the alias `uint` or `int`. Short, - /// Use the type defined in the source code - Preserve, } /// Style of underscores in number literals @@ -54,9 +55,9 @@ pub enum IntTypes { #[serde(rename_all = "snake_case")] pub enum NumberUnderscore { /// Use the underscores defined in the source code + #[default] Preserve, /// Remove all underscores - #[default] Remove, /// Add an underscore every thousand, if greater than 9999 /// e.g. 1000 -> 1000 and 10000 -> 10_000 @@ -96,63 +97,45 @@ pub enum HexUnderscore { Bytes, } -impl HexUnderscore { - /// Returns true if the option is `Preserve` - #[inline] - pub fn is_preserve(self) -> bool { - matches!(self, Self::Preserve) - } - - /// Returns true if the option is `Remove` - #[inline] - pub fn is_remove(self) -> bool { - matches!(self, Self::Remove) - } - - /// Returns true if the option is `Remove` - #[inline] - pub fn is_bytes(self) -> bool { - matches!(self, Self::Bytes) - } -} - /// Style of string quotes -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum QuoteStyle { - /// Use double quotes where possible + /// Use quotation mark defined in the source code. + Preserve, + /// Use double quotes where possible. + #[default] Double, - /// Use single quotes where possible + /// Use single quotes where possible. Single, - /// Use quotation mark defined in the source code - Preserve, } impl QuoteStyle { - /// Get associated quotation mark with option - pub fn quote(self) -> Option { + /// Returns the associated quotation mark character. + pub const fn quote(self) -> Option { match self { + Self::Preserve => None, Self::Double => Some('"'), Self::Single => Some('\''), - Self::Preserve => None, } } } /// Style of single line blocks in statements -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SingleLineBlockStyle { + /// Preserve the original style + #[default] + Preserve, /// Prefer single line block when possible Single, /// Always use multiline block Multi, - /// Preserve the original style - Preserve, } /// Style of function header in case it doesn't fit -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum MultilineFuncHeaderStyle { /// Write function parameters multiline first. @@ -160,6 +143,7 @@ pub enum MultilineFuncHeaderStyle { /// Write function parameters multiline first when there is more than one param. ParamsFirstMulti, /// Write function attributes multiline first. + #[default] AttributesFirst, /// If function params or attrs are multiline. /// split the rest @@ -168,6 +152,20 @@ pub enum MultilineFuncHeaderStyle { AllParams, } +impl MultilineFuncHeaderStyle { + pub fn all(&self) -> bool { + matches!(self, Self::All | Self::AllParams) + } + + pub fn params_first(&self) -> bool { + matches!(self, Self::ParamsFirst | Self::ParamsFirstMulti) + } + + pub fn attrib_first(&self) -> bool { + matches!(self, Self::AttributesFirst) + } +} + /// Style of indent #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -184,12 +182,12 @@ impl Default for FormatterConfig { tab_width: 4, style: IndentStyle::Space, bracket_spacing: false, - int_types: IntTypes::Long, - multiline_func_header: MultilineFuncHeaderStyle::AttributesFirst, - quote_style: QuoteStyle::Double, - number_underscore: NumberUnderscore::Preserve, - hex_underscore: HexUnderscore::Remove, - single_line_statement_blocks: SingleLineBlockStyle::Preserve, + int_types: IntTypes::default(), + multiline_func_header: MultilineFuncHeaderStyle::default(), + quote_style: QuoteStyle::default(), + number_underscore: NumberUnderscore::default(), + hex_underscore: HexUnderscore::default(), + single_line_statement_blocks: SingleLineBlockStyle::default(), override_spacing: false, wrap_comments: false, ignore: vec![], diff --git a/crates/fmt-2/Cargo.toml b/crates/fmt-2/Cargo.toml new file mode 100644 index 0000000000000..5a06f34baefe3 --- /dev/null +++ b/crates/fmt-2/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "forge-fmt-2" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +foundry-config.workspace = true +foundry-common.workspace = true + +solar-parse.workspace = true + +# alloy-primitives.workspace = true +itertools.workspace = true +similar = { version = "2", features = ["inline"] } +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } + +[dev-dependencies] +itertools.workspace = true +similar-asserts.workspace = true +toml.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter"] } +snapbox.workspace = true diff --git a/crates/fmt-2/README.md b/crates/fmt-2/README.md new file mode 100644 index 0000000000000..1fc2712ad6e0e --- /dev/null +++ b/crates/fmt-2/README.md @@ -0,0 +1,222 @@ +# Formatter (`fmt`) + +Solidity formatter that respects (some parts of) +the [Style Guide](https://docs.soliditylang.org/en/latest/style-guide.html) and +is tested on the [Prettier Solidity Plugin](https://github.com/prettier-solidity/prettier-plugin-solidity) cases. + +## Architecture + +The formatter works in two steps: + +1. Parse Solidity source code with [solang](https://github.com/hyperledger-labs/solang) into the PT (Parse Tree) + (not the same as Abstract Syntax Tree, [see difference](https://stackoverflow.com/a/9864571)). +2. Walk the PT and output new source code that's compliant with provided config and rule set. + +The technique for walking the tree is based on [Visitor Pattern](https://en.wikipedia.org/wiki/Visitor_pattern) +and works as following: + +1. Implement `Formatter` callback functions for each PT node type. + Every callback function should write formatted output for the current node + and call `Visitable::visit` function for child nodes delegating the output writing. +1. Implement `Visitable` trait and its `visit` function for each PT node type. Every `visit` function should call + corresponding `Formatter`'s callback function. + +### Output + +The formatted output is written into the output buffer in _chunks_. The `Chunk` struct holds the content to be written & +metadata for it. This includes the comments surrounding the content as well as the `needs_space` flag specifying whether +this _chunk_ needs a space. The flag overrides the default behavior of `Formatter::next_char_needs_space` method. + +The content gets written into the `FormatBuffer` which contains the information about the current indentation level, +indentation length, current state as well as the other data determining the rules for writing the content. +`FormatBuffer` implements the `std::fmt::Write` trait where it evaluates the current information and decides how the +content should be written to the destination. + +### Comments + +The solang parser does not output comments as a type of parse tree node, but rather +in a list alongside the parse tree with location information. It is therefore necessary +to infer where to insert the comments and how to format them while traversing the parse tree. + +To handle this, the formatter pre-parses the comments and puts them into two categories: +Prefix and Postfix comments. Prefix comments refer to the node directly after them, and +postfix comments refer to the node before them. As an illustration: + +```solidity +// This is a prefix comment +/* This is also a prefix comment */ +uint variable = 1 + 2; /* this is postfix */ // this is postfix too + // and this is a postfix comment on the next line +``` + +To insert the comments into the appropriate areas, strings get converted to chunks +before being written to the buffer. A chunk is any string that cannot be split by +whitespace. A chunk also carries with it the surrounding comment information. Thereby +when writing the chunk the comments can be added before and after the chunk as well +as any any whitespace surrounding. + +To construct a chunk, the string and the location of the string is given to the +Formatter and the pre-parsed comments before the start and end of the string are +associated with that string. The source code can then further be chunked before the +chunks are written to the buffer. + +To write the chunk, first the comments associated with the start of the chunk get +written to the buffer. Then the Formatter checks if any whitespace is needed between +what's been written to the buffer and what's in the chunk and inserts it where appropriate. +If the chunk content fits on the same line, it will be written directly to the buffer, +otherwise it will be written on the next line. Finally, any associated postfix +comments also get written. + +### Example + +Source code + +```solidity +pragma solidity ^0.8.10 ; +contract HelloWorld { + string public message; + constructor( string memory initMessage) { message = initMessage;} +} + + +event Greet( string indexed name) ; +``` + +Parse Tree (simplified) + +```text +SourceUnit + | PragmaDirective("solidity", "^0.8.10") + | ContractDefinition("HelloWorld") + | VariableDefinition("string", "message", null, ["public"]) + | FunctionDefinition("constructor") + | Parameter("string", "initMessage", ["memory"]) + | EventDefinition("string", "Greet", ["indexed"], ["name"]) +``` + +Formatted source code that was reconstructed from the Parse Tree + +```solidity +pragma solidity ^0.8.10; + +contract HelloWorld { + string public message; + + constructor(string memory initMessage) { + message = initMessage; + } +} + +event Greet(string indexed name); +``` + +### Configuration + +The formatter supports multiple configuration options defined in `FormatterConfig`. + +| Option | Default | Description | +|------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| line_length | 120 | Maximum line length where formatter will try to wrap the line | +| tab_width | 4 | Number of spaces per indentation level | +| bracket_spacing | false | Print spaces between brackets | +| int_types | long | Style of uint/int256 types. Available options: `long`, `short`, `preserve` | +| multiline_func_header | attributes_first | Style of multiline function header in case it doesn't fit. Available options: `params_first`, `params_first_multi`, `attributes_first`, `all`, `all_params` | +| quote_style | double | Style of quotation marks. Available options: `double`, `single`, `preserve` | +| number_underscore | preserve | Style of underscores in number literals. Available options: `preserve`, `remove`, `thousands` | +| hex_underscore | remove | Style of underscores in hex literals. Available options: `preserve`, `remove`, `bytes` | +| single_line_statement_blocks | preserve | Style of single line blocks in statements. Available options: `single`, `multi`, `preserve` | +| override_spacing | false | Print space in state variable, function and modifier `override` attribute | +| wrap_comments | false | Wrap comments on `line_length` reached | +| 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 | + +### Disable Line + +The formatter can be disabled on specific lines by adding a comment `// forgefmt: disable-next-line`, like this: + +```solidity +// forgefmt: disable-next-line +uint x = 100; +``` + +Alternatively, the comment can also be placed at the end of the line. In this case, you'd have to use `disable-line` +instead: + +```solidity +uint x = 100; // forgefmt: disable-line +``` + +### Disable Block + +The formatter can be disabled for a section of code by adding a comment `// forgefmt: disable-start` before and a +comment `// forgefmt: disable-end` after, like this: + +```solidity +// forgefmt: disable-start +uint x = 100; +uint y = 101; +// forgefmt: disable-end +``` + +### Testing + +Tests reside under the `fmt/testdata` folder and specify the malformatted & expected Solidity code. The source code file +is named `original.sol` and expected file(s) are named in a format `({prefix}.)?fmt.sol`. Multiple expected files are +needed for tests covering available configuration options. + +The default configuration values can be overridden from within the expected file by adding a comment in the format +`// config: {config_entry} = {config_value}`. For example: + +```solidity +// config: line_length = 160 +``` + +The `test_directory` macro is used to specify a new folder with source files for the test suite. Each test suite has the +following process: + +1. Preparse comments with config values +2. Parse and compare the AST for source & expected files. + - The `AstEq` trait defines the comparison rules for the AST nodes +3. Format the source file and assert the equality of the output with the expected file. +4. Format the expected files and assert the idempotency of the formatting operation. + +## Contributing + +Check out the [foundry contribution guide](https://github.com/foundry-rs/foundry/blob/master/CONTRIBUTING.md). + +Guidelines for contributing to `forge fmt`: + +### Opening an issue + +1. Create a short concise title describing an issue. + - Bad Title Examples + ```text + Forge fmt does not work + Forge fmt breaks + Forge fmt unexpected behavior + ``` + - Good Title Examples + ```text + Forge fmt postfix comment misplaced + Forge fmt does not inline short yul blocks + ``` +2. Fill in the issue template fields that include foundry version, platform & component info. +3. Provide the code snippets showing the current & expected behaviors. +4. If it's a feature request, specify why this feature is needed. +5. Besides the default label (`T-Bug` for bugs or `T-feature` for features), add `C-forge` and `Cmd-forge-fmt` labels. + +### Fixing A Bug + +1. Specify an issue that is being addressed in the PR description. +2. Add a note on the solution in the PR description. +3. Make sure the PR includes the acceptance test(s). + +### Developing A Feature + +1. Specify an issue that is being addressed in the PR description. +2. Add a note on the solution in the PR description. +3. Provide the test coverage for the new feature. These should include: + - Adding malformatted & expected solidity code under `fmt/testdata/$dir/` + - Testing the behavior of pre and postfix comments + - If it's a new config value, tests covering **all** available options \ No newline at end of file diff --git a/crates/fmt-2/src/lib.rs b/crates/fmt-2/src/lib.rs new file mode 100644 index 0000000000000..6d734835a887c --- /dev/null +++ b/crates/fmt-2/src/lib.rs @@ -0,0 +1,247 @@ +#![doc = include_str!("../README.md")] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![allow(dead_code)] // TODO(dani) + +const DEBUG: bool = false || option_env!("FMT_DEBUG").is_some(); +const DEBUG_INDENT: bool = false; + +use foundry_common::comments::{ + Comment, Comments, + inline_config::{InlineConfig, InlineConfigItem}, +}; + +// TODO(dani) +// #[macro_use] +// extern crate tracing; +use tracing as _; +use tracing_subscriber as _; + +mod state; + +mod pp; + +use solar_parse::{ + ast::{SourceUnit, Span}, + interface::{Session, diagnostics::EmittedDiagnostics, source_map::SourceFile}, +}; +use std::{path::Path, sync::Arc}; + +pub use foundry_config::fmt::*; + +/// The result of the formatter. +pub type FormatterResult = DiagnosticsResult; + +/// The result of the formatter. +#[derive(Debug)] +pub enum DiagnosticsResult { + /// Everything went well. + Ok(T), + /// No errors encountered, but warnings or other non-error diagnostics were emitted. + OkWithDiagnostics(T, E), + /// Errors encountered, but a result was produced anyway. + ErrRecovered(T, E), + /// Fatal errors encountered. + Err(E), +} + +impl DiagnosticsResult { + /// Converts the formatter result into a standard result. + /// + /// This ignores any non-error diagnostics if `Ok`, and any valid result if `Err`. + pub fn into_result(self) -> Result { + match self { + Self::Ok(s) | Self::OkWithDiagnostics(s, _) => Ok(s), + Self::ErrRecovered(_, d) | Self::Err(d) => Err(d), + } + } + + /// Returns the result, even if it was produced with errors. + pub fn into_ok(self) -> Result { + match self { + Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Ok(s), + Self::Err(e) => Err(e), + } + } + + /// Returns any result produced. + pub fn ok_ref(&self) -> Option<&T> { + match self { + Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Some(s), + Self::Err(_) => None, + } + } + + /// Returns any diagnostics emitted. + pub fn err_ref(&self) -> Option<&E> { + match self { + Self::Ok(_) => None, + Self::OkWithDiagnostics(_, d) | Self::ErrRecovered(_, d) | Self::Err(d) => Some(d), + } + } + + /// Returns `true` if the result is `Ok`. + pub fn is_ok(&self) -> bool { + matches!(self, Self::Ok(_) | Self::OkWithDiagnostics(_, _)) + } + + /// Returns `true` if the result is `Err`. + pub fn is_err(&self) -> bool { + !self.is_ok() + } +} + +pub fn format_file(path: &Path, config: FormatterConfig) -> FormatterResult { + format_inner(config, &|sess| { + sess.source_map().load_file(path).map_err(|e| sess.dcx.err(e.to_string()).emit()) + }) +} + +pub fn format_source( + source: &str, + path: Option<&Path>, + config: FormatterConfig, +) -> FormatterResult { + format_inner(config, &|sess| { + let name = match path { + Some(path) => solar_parse::interface::source_map::FileName::Real(path.to_path_buf()), + None => solar_parse::interface::source_map::FileName::Stdin, + }; + sess.source_map() + .new_source_file(name, source) + .map_err(|e| sess.dcx.err(e.to_string()).emit()) + }) +} + +fn format_inner( + config: FormatterConfig, + mk_file: &dyn Fn(&Session) -> solar_parse::interface::Result>, +) -> FormatterResult { + // First pass formatting + let first_result = format_once(config.clone(), mk_file); + + // If first pass was not successful, return the result + if first_result.is_err() { + return first_result; + } + let Some(first_formatted) = first_result.ok_ref() else { return first_result }; + + // Second pass formatting + let second_result = format_once(config, &|sess| { + sess.source_map() + .new_source_file( + solar_parse::interface::source_map::FileName::Custom("format-again".to_string()), + first_formatted, + ) + .map_err(|e| sess.dcx.err(e.to_string()).emit()) + }); + + // Check if the two passes produce the same output (idempotency) + match (first_result.ok_ref(), second_result.ok_ref()) { + (Some(first), Some(second)) if first != second => { + panic!("formatter is not idempotent:\n{}", diff(first, second)); + } + _ => {} + } + + if first_result.is_ok() && second_result.is_err() && !DEBUG { + panic!( + "failed to format a second time:\nfirst_result={first_result:#?}\nsecond_result={second_result:#?}" + ); + // second_result + } else { + first_result + } +} + +fn diff(first: &str, second: &str) -> impl std::fmt::Display { + use std::fmt::Write; + let diff = similar::TextDiff::from_lines(first, second); + let mut s = String::new(); + for change in diff.iter_all_changes() { + let tag = match change.tag() { + similar::ChangeTag::Delete => "-", + similar::ChangeTag::Insert => "+", + similar::ChangeTag::Equal => " ", + }; + write!(s, "{tag}{change}").unwrap(); + } + s +} + +fn format_once( + config: FormatterConfig, + mk_file: &dyn Fn(&Session) -> solar_parse::interface::Result>, +) -> FormatterResult { + let sess = + solar_parse::interface::Session::builder().with_buffer_emitter(Default::default()).build(); + let res = sess.enter(|| -> solar_parse::interface::Result<_> { + let file = mk_file(&sess)?; + let arena = solar_parse::ast::Arena::new(); + let mut parser = solar_parse::Parser::from_source_file(&sess, &arena, &file); + let comments = Comments::new( + &file, + sess.source_map(), + true, + config.wrap_comments, + if matches!(config.style, IndentStyle::Tab) { Some(config.tab_width) } else { None }, + ); + let ast = parser.parse_file().map_err(|e| e.emit())?; + let inline_config = parse_inline_config(&sess, &comments, &ast); + + let mut state = state::State::new(sess.source_map(), config, inline_config, comments); + state.print_source_unit(&ast); + Ok(state.s.eof()) + }); + let diagnostics = sess.emitted_diagnostics().unwrap(); + match (res, sess.dcx.has_errors()) { + (Ok(s), Ok(())) if diagnostics.is_empty() => FormatterResult::Ok(s), + (Ok(s), Ok(())) => FormatterResult::OkWithDiagnostics(s, diagnostics), + (Ok(s), Err(_)) => FormatterResult::ErrRecovered(s, diagnostics), + (Err(_), Ok(_)) => unreachable!(), + (Err(_), Err(_)) => FormatterResult::Err(diagnostics), + } +} + +fn parse_inline_config<'ast>( + sess: &Session, + comments: &Comments, + ast: &'ast SourceUnit<'ast>, +) -> InlineConfig<()> { + let parse_item = |mut item: &str, cmnt: &Comment| -> Option<(Span, InlineConfigItem<()>)> { + if let Some(prefix) = cmnt.prefix() { + item = item.strip_prefix(prefix).unwrap_or(item); + } + if let Some(suffix) = cmnt.suffix() { + item = item.strip_suffix(suffix).unwrap_or(item); + } + let item = item.trim_start().strip_prefix("forgefmt:")?.trim(); + match item.parse::>() { + Ok(item) => Some((cmnt.span, item)), + Err(e) => { + sess.dcx.warn(e.to_string()).span(cmnt.span).emit(); + None + } + } + }; + + let items = comments.iter().flat_map(|cmnt| { + let mut found_items = Vec::with_capacity(2); + // Always process the first line. + if let Some(line) = cmnt.lines.first() + && let Some(item) = parse_item(line, cmnt) + { + found_items.push(item); + } + // If the comment has more than one line, process the last line. + if cmnt.lines.len() > 1 + && let Some(line) = cmnt.lines.last() + && let Some(item) = parse_item(line, cmnt) + { + found_items.push(item); + } + found_items + }); + + InlineConfig::from_ast(items, ast, sess.source_map()) +} diff --git a/crates/fmt-2/src/main.rs b/crates/fmt-2/src/main.rs new file mode 100644 index 0000000000000..e476b4fb6a939 --- /dev/null +++ b/crates/fmt-2/src/main.rs @@ -0,0 +1,34 @@ +// TODO(dani): tmp for testing + +#![allow(dead_code, clippy::disallowed_macros)] + +use std::{io::Read, path::PathBuf}; + +fn main() { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let args = std::env::args().collect::>(); + let (src, path) = if args.len() < 2 || args[1] == "-" { + let mut s = String::new(); + std::io::stdin().read_to_string(&mut s).unwrap(); + (s, None) + } else { + let path = PathBuf::from(&args[1]); + (std::fs::read_to_string(&path).unwrap(), Some(path)) + }; + let config = foundry_config::Config::load().unwrap(); + let result = forge_fmt_2::format_source(&src, path.as_deref(), config.fmt); + if let Some(formatted) = result.ok_ref() { + print!("{formatted}"); + } + if let Some(diagnostics) = result.err_ref() { + if result.is_err() { + eprintln!("failed formatting:\n{diagnostics}"); + std::process::exit(1); + } else { + eprintln!("formatted with output:\n{diagnostics}"); + } + } +} diff --git a/crates/fmt-2/src/pp/convenience.rs b/crates/fmt-2/src/pp/convenience.rs new file mode 100644 index 0000000000000..b3cca7c78522e --- /dev/null +++ b/crates/fmt-2/src/pp/convenience.rs @@ -0,0 +1,207 @@ +use super::{BeginToken, BreakToken, Breaks, IndentStyle, Printer, SIZE_INFINITY, Token}; +use std::borrow::Cow; + +impl Printer { + /// "raw box" + pub fn rbox(&mut self, indent: isize, breaks: Breaks) { + self.scan_begin(BeginToken { indent: IndentStyle::Block { offset: indent }, breaks }); + } + + /// Inconsistent breaking box + pub fn ibox(&mut self, indent: isize) { + self.rbox(indent, Breaks::Inconsistent); + } + + /// Consistent breaking box + pub fn cbox(&mut self, indent: isize) { + self.rbox(indent, Breaks::Consistent); + } + + pub fn visual_align(&mut self) { + self.scan_begin(BeginToken { indent: IndentStyle::Visual, breaks: Breaks::Consistent }); + } + + pub fn break_offset(&mut self, n: usize, off: isize) { + self.scan_break(BreakToken { offset: off, blank_space: n, ..BreakToken::default() }); + } + + pub fn end(&mut self) { + self.scan_end(); + } + + pub fn eof(mut self) -> String { + self.scan_eof(); + self.out + } + + pub fn word(&mut self, w: impl Into>) { + self.scan_string(w.into()); + } + + fn spaces(&mut self, n: usize) { + self.break_offset(n, 0); + } + + pub fn zerobreak(&mut self) { + self.spaces(0); + } + + pub fn space(&mut self) { + self.spaces(1); + } + + pub fn hardbreak(&mut self) { + self.spaces(SIZE_INFINITY as usize); + } + + pub fn last_token_is_neverbreak(&self) -> bool { + if let Some(token) = self.last_token() { + return token.is_neverbreak(); + } + + false + } + + pub fn last_token_is_break(&self) -> bool { + if let Some(token) = self.last_token() { + return matches!(token, Token::Break(_)); + } + false + } + + pub fn last_token_is_hardbreak(&self) -> bool { + if let Some(token) = self.last_token() { + return token.is_hardbreak(); + } + false + } + + pub fn last_token_is_space(&self) -> bool { + if let Some(token) = self.last_token() + && token.is_space() + { + return true; + } + + self.out.ends_with(" ") + } + + pub fn is_beginning_of_line(&self) -> bool { + match self.last_token() { + Some(last_token) => last_token.is_hardbreak(), + None => self.out.is_empty() || self.out.ends_with('\n'), + } + } + + /// Attempts to identify whether the current position is: + /// 1. the beginning of a line (empty) + /// 2. a line with only indentation (just whitespaces) + /// + /// NOTE: this is still an educated guess, based on a heuristic. + pub fn is_bol_or_only_ind(&self) -> bool { + for i in self.buf.index_range().rev() { + let token = &self.buf[i].token; + if token.is_hardbreak() { + return true; + } + if Self::token_has_non_whitespace_content(token) { + return false; + } + } + + let last_line = + if let Some(pos) = self.out.rfind('\n') { &self.out[pos + 1..] } else { &self.out[..] }; + + last_line.trim().is_empty() + } + + fn token_has_non_whitespace_content(token: &Token) -> bool { + match token { + Token::String(s) => !s.trim().is_empty(), + Token::Break(BreakToken { pre_break: Some(s), .. }) => !s.trim().is_empty(), + _ => false, + } + } + + pub(crate) fn hardbreak_tok_offset(offset: isize) -> Token { + Token::Break(BreakToken { + offset, + blank_space: SIZE_INFINITY as usize, + ..BreakToken::default() + }) + } + + pub fn space_if_nonempty(&mut self) { + self.scan_break(BreakToken { blank_space: 1, if_nonempty: true, ..BreakToken::default() }); + } + + pub fn hardbreak_if_nonempty(&mut self) { + self.scan_break(BreakToken { + blank_space: SIZE_INFINITY as usize, + if_nonempty: true, + ..BreakToken::default() + }); + } + + // Doesn't actually print trailing comma since it's not allowed in Solidity. + pub fn trailing_comma(&mut self, is_last: bool) { + if is_last { + // self.scan_break(BreakToken { pre_break: Some(','), ..BreakToken::default() }); + self.zerobreak(); + } else { + self.word(","); + self.space(); + } + } + + pub fn trailing_comma_or_space(&mut self, is_last: bool) { + if is_last { + self.scan_break(BreakToken { + blank_space: 1, + pre_break: Some(","), + ..BreakToken::default() + }); + } else { + self.word(","); + self.space(); + } + } + + pub fn neverbreak(&mut self) { + self.scan_break(BreakToken { never_break: true, ..BreakToken::default() }); + } + + pub fn last_brace_is_closed(&self, kw: &str) -> bool { + self.out.rsplit_once(kw).is_none_or(|(_, relevant)| { + let open = relevant.chars().filter(|c| *c == '{').count(); + let close = relevant.chars().filter(|c| *c == '}').count(); + open == close + }) + } +} + +impl Token { + pub(crate) fn is_neverbreak(&self) -> bool { + if let Self::Break(BreakToken { never_break, .. }) = *self { + return never_break; + } + false + } + + pub(crate) fn is_hardbreak(&self) -> bool { + if let Self::Break(BreakToken { blank_space, never_break, .. }) = *self { + return blank_space == SIZE_INFINITY as usize && !never_break; + } + false + } + + pub(crate) fn is_space(&self) -> bool { + match self { + Self::Break(BreakToken { offset, blank_space, .. }) => { + *offset == 0 && *blank_space == 1 + } + Self::String(s) => s.ends_with(' '), + _ => false, + } + } +} diff --git a/crates/fmt-2/src/pp/helpers.rs b/crates/fmt-2/src/pp/helpers.rs new file mode 100644 index 0000000000000..a847f852b5505 --- /dev/null +++ b/crates/fmt-2/src/pp/helpers.rs @@ -0,0 +1,56 @@ +use super::{Printer, Token}; +use std::borrow::Cow; + +impl Printer { + pub fn word_space(&mut self, w: impl Into>) { + self.word(w); + self.space(); + } + + /// Adds a new hardbreak if not at the beginning of the line. + /// If there was a buffered break token, replaces it (ensures hardbreak) keeping the offset. + pub fn hardbreak_if_not_bol(&mut self) { + if !self.is_bol_or_only_ind() { + if let Some(Token::Break(last)) = self.last_token_still_buffered() + && last.offset != 0 + { + self.replace_last_token_still_buffered(Self::hardbreak_tok_offset(last.offset)); + return; + } + self.hardbreak(); + } + } + + pub fn space_if_not_bol(&mut self) { + if !self.is_bol_or_only_ind() { + self.space(); + } + } + + pub fn nbsp(&mut self) { + self.word(" "); + } + + pub fn space_or_nbsp(&mut self, breaks: bool) { + if breaks { + self.space(); + } else { + self.nbsp(); + } + } + + pub fn word_nbsp(&mut self, w: impl Into>) { + self.word(w); + self.nbsp(); + } + + /// Synthesizes a comment that was not textually present in the original + /// source file. + pub fn synth_comment(&mut self, text: impl Into>) { + self.word("/*"); + self.space(); + self.word(text); + self.space(); + self.word("*/"); + } +} diff --git a/crates/fmt-2/src/pp/mod.rs b/crates/fmt-2/src/pp/mod.rs new file mode 100644 index 0000000000000..431b1bacaad29 --- /dev/null +++ b/crates/fmt-2/src/pp/mod.rs @@ -0,0 +1,456 @@ +//! Adapted from [`rustc_ast_pretty`](https://github.com/rust-lang/rust/blob/07d3fd1d9b9c1f07475b96a9d168564bf528db68/compiler/rustc_ast_pretty/src/pp.rs) +//! and [`prettyplease`](https://github.com/dtolnay/prettyplease/blob/8eb8c14649aea32e810732bd4d64fe519e6b752a/src/algorithm.rs). + +use crate::{DEBUG, DEBUG_INDENT}; +use ring::RingBuffer; +use std::{borrow::Cow, cmp, collections::VecDeque, iter}; + +mod convenience; +mod helpers; +mod ring; + +// Every line is allowed at least this much space, even if highly indented. +const MIN_SPACE: isize = 40; + +/// How to break. Described in more detail in the module docs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Breaks { + Consistent, + Inconsistent, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum IndentStyle { + /// Vertically aligned under whatever column this block begins at. + /// ```ignore + /// fn demo(arg1: usize, + /// arg2: usize) {} + /// ``` + Visual, + /// Indented relative to the indentation level of the previous line. + /// ```ignore + /// fn demo( + /// arg1: usize, + /// arg2: usize, + /// ) {} + /// ``` + Block { offset: isize }, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) struct BreakToken { + pub(crate) offset: isize, + pub(crate) blank_space: usize, + pub(crate) pre_break: Option<&'static str>, + pub(crate) post_break: Option<&'static str>, + pub(crate) if_nonempty: bool, + pub(crate) never_break: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct BeginToken { + indent: IndentStyle, + breaks: Breaks, +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Token { + // In practice a string token contains either a `&'static str` or a + // `String`. `Cow` is overkill for this because we never modify the data, + // but it's more convenient than rolling our own more specialized type. + String(Cow<'static, str>), + Break(BreakToken), + Begin(BeginToken), + End, +} + +#[derive(Copy, Clone, Debug)] +enum PrintFrame { + Fits(Breaks), + Broken(usize, Breaks), +} + +pub(crate) const SIZE_INFINITY: isize = 0xffff; + +#[derive(Debug)] +pub struct Printer { + out: String, + /// Number of spaces left on line. + space: isize, + /// Ring-buffer of tokens and calculated sizes. + buf: RingBuffer, + /// Running size of stream "...left". + left_total: isize, + /// Running size of stream "...right". + right_total: isize, + /// Pseudo-stack, really a ring too. Holds the + /// primary-ring-buffers index of the Begin that started the + /// current block, possibly with the most recent Break after that + /// Begin (if there is any) on top of it. Stuff is flushed off the + /// bottom as it becomes irrelevant due to the primary ring-buffer + /// advancing. + scan_stack: VecDeque, + /// Stack of blocks-in-progress being flushed by print. + print_stack: Vec, + /// Level of indentation of current line. + indent: usize, + /// Buffered indentation to avoid writing trailing whitespace. + pending_indentation: usize, + /// The token most recently popped from the left boundary of the + /// ring-buffer for printing. + last_printed: Option, + + /// Target line width. + margin: isize, + /// If `Some(tab_width)` the printer will use tabs for indentation. + indent_config: Option, +} + +#[derive(Debug)] +pub struct BufEntry { + token: Token, + size: isize, +} + +impl Printer { + pub fn new(margin: usize, use_tab_with_size: Option) -> Self { + let margin = (margin as isize).clamp(MIN_SPACE, SIZE_INFINITY - 1); + Self { + out: String::new(), + space: margin, + buf: RingBuffer::new(), + left_total: 0, + right_total: 0, + scan_stack: VecDeque::new(), + print_stack: Vec::new(), + indent: 0, + pending_indentation: 0, + last_printed: None, + + margin, + indent_config: use_tab_with_size, + } + } + + pub(crate) fn space_left(&self) -> usize { + if self.space > 0 { + self.space as usize + } else if self.margin > 0 { + self.margin as usize + } else { + 0 + } + } + + pub(crate) fn last_token(&self) -> Option<&Token> { + self.last_token_still_buffered().or(self.last_printed.as_ref()) + } + + pub(crate) fn last_token_still_buffered(&self) -> Option<&Token> { + if self.buf.is_empty() { + return None; + } + Some(&self.buf.last().token) + } + + /// Be very careful with this! + pub(crate) fn replace_last_token_still_buffered(&mut self, token: Token) { + self.buf.last_mut().token = token; + } + + /// WARNING: Be very careful with this! + /// + /// Searches backwards through the buffer to find and replace the last token + /// that satisfies a predicate. This is a specialized and sensitive operation. + /// + /// This function's traversal logic is specifically designed to handle cases + /// where formatting boxes have been closed (e.g., after a multi-line + /// comment). It will automatically skip over any trailing `Token::End` + /// tokens to find the substantive token before them. + /// + /// The search stops as soon as it encounters any token other than `End` + /// (i.e., a `String`, `Break`, or `Begin`). The provided predicate is then + /// called on that token. If the predicate returns `true`, the token is + /// replaced. + /// + /// This function will only ever evaluate the predicate on **one** token. + pub(crate) fn find_and_replace_last_token_still_buffered( + &mut self, + new_token: Token, + predicate: F, + ) where + F: FnOnce(&Token) -> bool, + { + for i in self.buf.index_range().rev() { + let token = &self.buf[i].token; + if let Token::End = token { + // It's safe to skip the end of a box. + continue; + } + + // Apply the predicate and return after the first non-end token. + if predicate(token) { + self.buf[i].token = new_token; + } + break; + } + } + + fn scan_eof(&mut self) { + if !self.scan_stack.is_empty() { + self.check_stack(0); + self.advance_left(); + } + } + + fn scan_begin(&mut self, token: BeginToken) { + if self.scan_stack.is_empty() { + self.left_total = 1; + self.right_total = 1; + self.buf.clear(); + } + let right = self.buf.push(BufEntry { token: Token::Begin(token), size: -self.right_total }); + self.scan_stack.push_back(right); + } + + fn scan_end(&mut self) { + if self.scan_stack.is_empty() { + self.print_end(); + } else { + if !self.buf.is_empty() + && let Token::Break(break_token) = self.buf.last().token + { + if self.buf.len() >= 2 + && let Token::Begin(_) = self.buf.second_last().token + { + self.buf.pop_last(); + self.buf.pop_last(); + self.scan_stack.pop_back(); + self.scan_stack.pop_back(); + self.right_total -= break_token.blank_space as isize; + return; + } + if break_token.if_nonempty { + self.buf.pop_last(); + self.scan_stack.pop_back(); + self.right_total -= break_token.blank_space as isize; + } + } + let right = self.buf.push(BufEntry { token: Token::End, size: -1 }); + self.scan_stack.push_back(right); + } + } + + pub(crate) fn scan_break(&mut self, token: BreakToken) { + if self.scan_stack.is_empty() { + self.left_total = 1; + self.right_total = 1; + self.buf.clear(); + } else { + self.check_stack(0); + } + let right = self.buf.push(BufEntry { token: Token::Break(token), size: -self.right_total }); + self.scan_stack.push_back(right); + self.right_total += token.blank_space as isize; + } + + fn scan_string(&mut self, string: Cow<'static, str>) { + if self.scan_stack.is_empty() { + self.print_string(&string); + } else { + let len = string.len() as isize; + self.buf.push(BufEntry { token: Token::String(string), size: len }); + self.right_total += len; + self.check_stream(); + } + } + + #[track_caller] + pub(crate) fn offset(&mut self, offset: isize) { + match &mut self.buf.last_mut().token { + Token::Break(token) => token.offset += offset, + Token::Begin(_) => {} + Token::String(_) | Token::End => unreachable!(), + } + } + + pub(crate) fn ends_with(&self, ch: char) -> bool { + for i in self.buf.index_range().rev() { + if let Token::String(token) = &self.buf[i].token { + return token.ends_with(ch); + } + } + self.out.ends_with(ch) + } + + fn check_stream(&mut self) { + while self.right_total - self.left_total > self.space { + if *self.scan_stack.front().unwrap() == self.buf.index_range().start { + self.scan_stack.pop_front().unwrap(); + self.buf.first_mut().size = SIZE_INFINITY; + } + + self.advance_left(); + + if self.buf.is_empty() { + break; + } + } + } + + fn advance_left(&mut self) { + while self.buf.first().size >= 0 { + let left = self.buf.pop_first(); + + match &left.token { + Token::String(string) => { + self.left_total += left.size; + self.print_string(string); + } + Token::Break(token) => { + self.left_total += token.blank_space as isize; + self.print_break(*token, left.size); + } + Token::Begin(token) => self.print_begin(*token, left.size), + Token::End => self.print_end(), + } + + self.last_printed = Some(left.token); + + if self.buf.is_empty() { + break; + } + } + } + + fn check_stack(&mut self, mut depth: usize) { + while let Some(&index) = self.scan_stack.back() { + let entry = &mut self.buf[index]; + match entry.token { + Token::Begin(_) => { + if depth == 0 { + break; + } + self.scan_stack.pop_back().unwrap(); + entry.size += self.right_total; + depth -= 1; + } + Token::End => { + // paper says + not =, but that makes no sense. + self.scan_stack.pop_back().unwrap(); + entry.size = 1; + depth += 1; + } + _ => { + self.scan_stack.pop_back().unwrap(); + entry.size += self.right_total; + if depth == 0 { + break; + } + } + } + } + } + + fn get_top(&self) -> PrintFrame { + self.print_stack.last().copied().unwrap_or(PrintFrame::Broken(0, Breaks::Inconsistent)) + } + + fn print_begin(&mut self, token: BeginToken, size: isize) { + if DEBUG { + self.out.push(match token.breaks { + Breaks::Consistent => '«', + Breaks::Inconsistent => '‹', + }); + if DEBUG_INDENT && let IndentStyle::Block { offset } = token.indent { + self.out.extend(offset.to_string().chars().map(|ch| match ch { + '0'..='9' => ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'] + [(ch as u8 - b'0') as usize], + '-' => '₋', + _ => unreachable!(), + })); + } + } + + if size > self.space { + self.print_stack.push(PrintFrame::Broken(self.indent, token.breaks)); + self.indent = match token.indent { + IndentStyle::Block { offset } => { + usize::try_from(self.indent as isize + offset).unwrap() + } + IndentStyle::Visual => (self.margin - self.space) as usize, + }; + } else { + self.print_stack.push(PrintFrame::Fits(token.breaks)); + } + } + + fn print_end(&mut self) { + let breaks = match self.print_stack.pop().unwrap() { + PrintFrame::Broken(indent, breaks) => { + self.indent = indent; + breaks + } + PrintFrame::Fits(breaks) => breaks, + }; + if DEBUG { + self.out.push(match breaks { + Breaks::Consistent => '»', + Breaks::Inconsistent => '›', + }); + } + } + + fn print_break(&mut self, token: BreakToken, size: isize) { + let fits = token.never_break + || match self.get_top() { + PrintFrame::Fits(..) => true, + PrintFrame::Broken(.., Breaks::Consistent) => false, + PrintFrame::Broken(.., Breaks::Inconsistent) => size <= self.space, + }; + if fits { + self.pending_indentation += token.blank_space; + self.space -= token.blank_space as isize; + if DEBUG { + self.out.push('·'); + } + } else { + if let Some(pre_break) = token.pre_break { + self.print_indent(); + self.out.push_str(pre_break); + } + if DEBUG { + self.out.push('·'); + } + self.out.push('\n'); + let indent = self.indent as isize + token.offset; + self.pending_indentation = usize::try_from(indent).expect("negative indentation"); + self.space = cmp::max(self.margin - indent, MIN_SPACE); + if let Some(post_break) = token.post_break { + self.print_indent(); + self.out.push_str(post_break); + self.space -= post_break.len() as isize; + } + } + } + + fn print_string(&mut self, string: &str) { + self.print_indent(); + self.out.push_str(string); + self.space -= string.len() as isize; + } + + fn print_indent(&mut self) { + self.out.reserve(self.pending_indentation); + if let Some(tab_width) = self.indent_config { + let num_tabs = self.pending_indentation / tab_width; + self.out.extend(iter::repeat_n('\t', num_tabs)); + + let remainder = self.pending_indentation % tab_width; + self.out.extend(iter::repeat_n(' ', remainder)); + } else { + self.out.extend(iter::repeat_n(' ', self.pending_indentation)); + } + self.pending_indentation = 0; + } +} diff --git a/crates/fmt-2/src/pp/ring.rs b/crates/fmt-2/src/pp/ring.rs new file mode 100644 index 0000000000000..f958a0dd2fefc --- /dev/null +++ b/crates/fmt-2/src/pp/ring.rs @@ -0,0 +1,95 @@ +use std::{ + collections::VecDeque, + ops::{Index, IndexMut, Range}, +}; + +#[derive(Debug)] +pub(crate) struct RingBuffer { + data: VecDeque, + // Abstract index of data[0] in the infinitely sized queue. + offset: usize, +} + +impl RingBuffer { + pub(crate) fn new() -> Self { + Self { data: VecDeque::new(), offset: 0 } + } + + pub(crate) fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub(crate) fn len(&self) -> usize { + self.data.len() + } + + pub(crate) fn push(&mut self, value: T) -> usize { + let index = self.offset + self.data.len(); + self.data.push_back(value); + index + } + + pub(crate) fn clear(&mut self) { + self.data.clear(); + } + + pub(crate) fn index_range(&self) -> Range { + self.offset..self.offset + self.data.len() + } + + #[inline] + #[track_caller] + pub(crate) fn first(&self) -> &T { + &self.data[0] + } + + #[inline] + #[track_caller] + pub(crate) fn first_mut(&mut self) -> &mut T { + &mut self.data[0] + } + + #[inline] + #[track_caller] + pub(crate) fn pop_first(&mut self) -> T { + self.offset += 1; + self.data.pop_front().unwrap() + } + + #[inline] + #[track_caller] + pub(crate) fn last(&self) -> &T { + self.data.back().unwrap() + } + + #[inline] + #[track_caller] + pub(crate) fn last_mut(&mut self) -> &mut T { + self.data.back_mut().unwrap() + } + + #[inline] + #[track_caller] + pub(crate) fn second_last(&self) -> &T { + &self.data[self.data.len() - 2] + } + + #[inline] + #[track_caller] + pub(crate) fn pop_last(&mut self) { + self.data.pop_back().unwrap(); + } +} + +impl Index for RingBuffer { + type Output = T; + fn index(&self, index: usize) -> &Self::Output { + &self.data[index.checked_sub(self.offset).unwrap()] + } +} + +impl IndexMut for RingBuffer { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.data[index.checked_sub(self.offset).unwrap()] + } +} diff --git a/crates/fmt-2/src/state.rs b/crates/fmt-2/src/state.rs new file mode 100644 index 0000000000000..66764cf32c46c --- /dev/null +++ b/crates/fmt-2/src/state.rs @@ -0,0 +1,3892 @@ +#![allow(clippy::too_many_arguments)] + +use crate::{ + FormatterConfig, InlineConfig, + pp::{self, BreakToken, SIZE_INFINITY, Token}, +}; +use foundry_common::{ + comments::{Comment, CommentStyle, Comments, line_with_tabs}, + iter::IterDelimited, +}; +use foundry_config::fmt::{self as config, IndentStyle, MultilineFuncHeaderStyle}; +use itertools::{Either, Itertools}; +use solar_parse::{ + Cursor, + ast::{self, Span, token, yul}, + interface::{BytePos, SourceMap}, +}; +use std::{borrow::Cow, collections::HashMap, fmt::Debug}; + +struct SourcePos { + pos: BytePos, + enabled: bool, +} + +impl SourcePos { + fn advance(&mut self, bytes: u32) { + self.pos += BytePos(bytes); + } + + fn advance_to(&mut self, pos: BytePos, enabled: bool) { + self.pos = std::cmp::max(pos, self.pos); + self.enabled = enabled; + } + + fn span(&self, to: BytePos) -> Span { + Span::new(self.pos, to) + } +} + +pub(super) struct State<'sess, 'ast> { + pub(crate) s: pp::Printer, + ind: isize, + + sm: &'sess SourceMap, + comments: Comments, + config: FormatterConfig, + inline_config: InlineConfig<()>, + cursor: SourcePos, + + contract: Option<&'ast ast::ItemContract<'ast>>, + single_line_stmt: Option, + call_expr_named: bool, + binary_expr: bool, + member_expr: bool, + var_init: bool, +} + +impl std::ops::Deref for State<'_, '_> { + type Target = pp::Printer; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.s + } +} + +impl std::ops::DerefMut for State<'_, '_> { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.s + } +} + +/// Generic methods. +impl<'sess> State<'sess, '_> { + pub(super) fn new( + sm: &'sess SourceMap, + config: FormatterConfig, + inline_config: InlineConfig<()>, + comments: Comments, + ) -> Self { + Self { + s: pp::Printer::new( + config.line_length, + if matches!(config.style, IndentStyle::Tab) { + Some(config.tab_width) + } else { + None + }, + ), + ind: config.tab_width as isize, + sm, + comments, + config, + inline_config, + cursor: SourcePos { pos: BytePos::from_u32(0), enabled: true }, + contract: None, + single_line_stmt: None, + call_expr_named: false, + binary_expr: false, + member_expr: false, + var_init: false, + } + } + + fn cmnt_config(&self) -> CommentConfig { + CommentConfig { current_ind: self.ind, ..Default::default() } + } + + fn cmnt_config_skip_ws(&self) -> CommentConfig { + CommentConfig { current_ind: self.ind, skip_blanks: Some(Skip::All), ..Default::default() } + } + + /// Prints comments that are before the given position. + /// + /// Returns `Some` with the style of the last comment printed, or `None` if no comment was + /// printed. + fn print_comments(&mut self, pos: BytePos, mut config: CommentConfig) -> Option { + let mut last_style: Option = None; + let mut is_leading = true; + 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 style_cache = cmnt.style; + let Some(cmnt) = self.handle_comment(cmnt) else { + last_style = Some(style_cache); + continue; + }; + + if cmnt.style.is_blank() { + match config.skip_blanks { + Some(Skip::All) => continue, + Some(Skip::Leading) if is_leading => continue, + Some(Skip::Trailing) => { + buffered_blank = Some(cmnt); + continue; + } + _ => (), + } + // Never print blank lines after docs comments + } else if !cmnt.is_doc { + is_leading = false; + } + + if let Some(blank) = buffered_blank.take() { + self.print_comment(blank, config); + } + + // Handle mixed with follow-up comment + if cmnt.style.is_mixed() { + if let Some(cmnt) = self.peek_comment_before(pos) { + config.mixed_no_break = true; + config.mixed_post_nbsp = cmnt.style.is_mixed(); + } + + // Ensure consecutive mixed comments don't have a double-space + if last_style.is_some_and(|s| s.is_mixed()) { + config.mixed_no_break = true; + config.mixed_prev_space = false; + } + } else if config.offset != 0 + && cmnt.style.is_isolated() + && last_style.is_some_and(|s| s.is_isolated()) + { + self.offset(config.offset); + } + + last_style = Some(cmnt.style); + self.print_comment(cmnt, config); + config = config_cache; + } + last_style + } + + /// Prints a line, wrapping it if it starts with the given prefix. + fn print_wrapped_line( + &mut self, + line: &str, + prefix: &'static str, + break_offset: isize, + is_doc: bool, + ) { + if !line.starts_with(prefix) { + self.word(line.to_owned()); + return; + } + + let post_break_prefix = |prefix: &'static str, line_len: usize| -> &'static str { + match prefix { + "///" if line_len > 3 => "/// ", + "//" if line_len > 2 => "// ", + "/*" if line_len > 2 => "/* ", + " *" if line_len > 2 => " * ", + _ => prefix, + } + }; + + self.ibox(0); + let (prefix, content) = if is_doc { + // Doc comments preserve leading whitespaces (right after the prefix). + self.word(prefix); + let content = &line[prefix.len()..]; + let (leading_ws, rest) = + content.split_at(content.chars().take_while(|&c| c.is_whitespace()).count()); + if !leading_ws.is_empty() { + self.word(leading_ws.to_owned()); + } + let prefix = post_break_prefix(prefix, rest.len()); + (prefix, rest) + } else { + let content = line[prefix.len()..].trim(); + let prefix = post_break_prefix(prefix, content.len()); + self.word(prefix); + (prefix, content) + }; + + // Split the rest of the content into words. + let mut words = content.split_whitespace().peekable(); + while let Some(word) = words.next() { + self.word(word.to_owned()); + if let Some(next_word) = words.peek() { + if *next_word == "*/" { + self.nbsp(); + } else { + self.s.scan_break(BreakToken { + offset: break_offset, + blank_space: 1, + post_break: if matches!(prefix, "/* ") { None } else { Some(prefix) }, + ..Default::default() + }); + } + } + } + self.end(); + } + + fn print_comment(&mut self, mut cmnt: Comment, mut config: CommentConfig) { + self.cursor.advance_to(cmnt.span.hi(), true); + match cmnt.style { + CommentStyle::Mixed => { + let Some(prefix) = cmnt.prefix() else { return }; + let never_break = self.last_token_is_neverbreak(); + if !self.is_bol_or_only_ind() { + match (never_break || config.mixed_no_break, config.mixed_prev_space) { + (false, true) => config.space(&mut self.s), + (false, false) => config.zerobreak(&mut self.s), + (true, true) => self.nbsp(), + (true, false) => (), + }; + } + for (pos, line) in cmnt.lines.into_iter().delimited() { + if self.config.wrap_comments { + self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc); + } else { + self.word(line); + } + if !pos.is_last { + self.hardbreak(); + } + } + if config.mixed_post_nbsp { + config.nbsp_or_space(self.config.wrap_comments, &mut self.s); + self.cursor.advance(1); + } else if !config.mixed_no_break { + config.space(&mut self.s); + self.cursor.advance(1); + } + } + CommentStyle::Isolated => { + let Some(mut prefix) = cmnt.prefix() else { return }; + config.hardbreak_if_not_bol(self.is_bol_or_only_ind(), &mut self.s); + for (pos, line) in cmnt.lines.into_iter().delimited() { + if line.is_empty() { + self.hardbreak(); + continue; + } + if pos.is_first { + self.ibox(config.offset); + if self.config.wrap_comments && cmnt.is_doc && matches!(prefix, "/**") { + self.word(prefix); + self.hardbreak(); + prefix = " * "; + continue; + } + } + + if self.config.wrap_comments { + self.print_wrapped_line(&line, prefix, 0, cmnt.is_doc); + } else { + self.word(line); + } + if pos.is_last { + self.end(); + } + self.print_sep(Separator::Hardbreak); + } + } + CommentStyle::Trailing => { + let Some(prefix) = cmnt.prefix() else { return }; + self.neverbreak(); + if !self.is_bol_or_only_ind() { + self.nbsp(); + } + + if !self.config.wrap_comments && cmnt.lines.len() == 1 { + self.word(cmnt.lines.pop().unwrap()); + } else if self.config.wrap_comments { + config.offset = self.ind; + for (lpos, line) in cmnt.lines.into_iter().delimited() { + if !line.is_empty() { + self.print_wrapped_line( + &line, + prefix, + if cmnt.is_doc { 0 } else { config.offset }, + cmnt.is_doc, + ); + } + if !lpos.is_last { + config.hardbreak(&mut self.s); + } + } + } else { + self.visual_align(); + for (pos, line) in cmnt.lines.into_iter().delimited() { + if !line.is_empty() { + self.word(line); + if !pos.is_last { + self.hardbreak(); + } + } + } + self.end(); + } + + if !config.trailing_no_break { + self.print_sep(Separator::Hardbreak); + } + } + + CommentStyle::BlankLine => { + // We need to do at least one, possibly two hardbreaks. + let twice = match self.last_token() { + Some(Token::String(s)) => ";" == s, + Some(Token::Begin(_)) => true, + Some(Token::End) => true, + _ => false, + }; + if twice { + config.hardbreak(&mut self.s); + self.cursor.advance(1); + } + config.hardbreak(&mut self.s); + self.cursor.advance(1); + } + } + } + + fn peek_comment<'b>(&'b self) -> Option<&'b Comment> + where + 'sess: 'b, + { + self.comments.peek() + } + + fn peek_comment_before<'b>(&'b self, pos: BytePos) -> Option<&'b Comment> + where + 'sess: 'b, + { + self.comments.iter().take_while(|c| c.pos() < pos).find(|c| !c.style.is_blank()) + } + + fn peek_comment_between<'b>(&'b self, pos_lo: BytePos, pos_hi: BytePos) -> Option<&'b Comment> + where + 'sess: 'b, + { + self.comments + .iter() + .take_while(|c| pos_lo < c.pos() && c.pos() < pos_hi) + .find(|c| !c.style.is_blank()) + } + + fn has_comment_between(&self, start_pos: BytePos, end_pos: BytePos) -> bool { + self.comments.iter().filter(|c| c.pos() > start_pos && c.pos() < end_pos).any(|_| true) + } + + fn next_comment(&mut self) -> Option { + self.comments.next() + } + + fn peek_trailing_comment<'b>( + &'b self, + span_pos: BytePos, + next_pos: Option, + ) -> Option<&'b Comment> + where + 'sess: 'b, + { + self.comments.peek_trailing(self.sm, span_pos, next_pos).map(|(cmnt, _)| cmnt) + } + + fn print_trailing_comment_inner( + &mut self, + span_pos: BytePos, + next_pos: Option, + config: Option, + ) -> bool { + let mut printed = 0; + if let Some((_, n)) = self.comments.peek_trailing(self.sm, span_pos, next_pos) { + let config = + config.unwrap_or(CommentConfig::skip_ws().mixed_no_break().mixed_prev_space()); + while printed <= n { + let cmnt = self.comments.next().unwrap(); + if let Some(cmnt) = self.handle_comment(cmnt) { + self.print_comment(cmnt, config); + }; + printed += 1; + } + } + printed != 0 + } + + fn print_trailing_comment(&mut self, span_pos: BytePos, next_pos: Option) -> bool { + self.print_trailing_comment_inner(span_pos, next_pos, None) + } + + fn print_trailing_comment_no_break(&mut self, span_pos: BytePos, next_pos: Option) { + self.print_trailing_comment_inner( + span_pos, + next_pos, + Some(CommentConfig::skip_ws().trailing_no_break().mixed_no_break().mixed_prev_space()), + ); + } + + fn print_remaining_comments(&mut self) { + // If there aren't any remaining comments, then we need to manually + // make sure there is a line break at the end. + if self.peek_comment().is_none() && !self.is_bol_or_only_ind() { + self.hardbreak(); + } + + while let Some(cmnt) = self.next_comment() { + if let Some(cmnt) = self.handle_comment(cmnt) { + self.print_comment(cmnt, CommentConfig::default()); + } else if self.peek_comment().is_none() { + self.hardbreak(); + } + } + } + + fn break_offset_if_not_bol(&mut self, n: usize, off: isize, search: bool) { + // When searching, the break token is expected to be inside a closed box. Thus, we will + // traverse the buffer and evaluate the first non-end token. + if search { + // We do something pretty sketchy here: tuck the nonzero offset-adjustment we + // were going to deposit along with the break into the previous hardbreak. + self.find_and_replace_last_token_still_buffered( + pp::Printer::hardbreak_tok_offset(off), + |token| token.is_hardbreak(), + ); + return; + } + + // When not explicitly searching, the break token is expected to be the last token. + if !self.is_beginning_of_line() { + self.break_offset(n, off) + } else if off != 0 + && let Some(last_token) = self.last_token_still_buffered() + && last_token.is_hardbreak() + { + // We do something pretty sketchy here: tuck the nonzero offset-adjustment we + // were going to deposit along with the break into the previous hardbreak. + self.replace_last_token_still_buffered(pp::Printer::hardbreak_tok_offset(off)); + } + } + + fn braces_break(&mut self) { + if self.config.bracket_spacing { + self.space(); + } else { + self.zerobreak(); + } + } + + fn print_tuple<'a, T, P, S>( + &mut self, + values: &'a [T], + pos_lo: BytePos, + pos_hi: BytePos, + mut print: P, + mut get_span: S, + format: ListFormat, + break_single_no_cmnts: bool, + ) where + P: FnMut(&mut Self, &'a T), + S: FnMut(&T) -> Option, + { + if self.handle_span(Span::new(pos_lo, pos_hi), true) { + return; + } + + if values.is_empty() { + self.print_word("("); + self.s.cbox(self.ind); + if let Some(cmnt) = + self.print_comments(pos_hi, CommentConfig::skip_ws().mixed_prev_space()) + { + if cmnt.is_mixed() { + self.s.offset(-self.ind); + } else { + self.break_offset_if_not_bol(0, -self.ind, false); + } + } + self.end(); + self.print_word(")"); + return; + } + + // Format single-item inline lists directly without boxes + if values.len() == 1 && matches!(format, ListFormat::Inline) { + self.print_word("("); + if let Some(span) = get_span(&values[0]) { + self.s.cbox(self.ind); + let mut skip_break = true; + if self.peek_comment_before(span.hi()).is_some() { + self.hardbreak(); + skip_break = false; + } + self.print_comments(span.lo(), CommentConfig::skip_ws().mixed_prev_space()); + print(self, &values[0]); + if !self.print_trailing_comment(span.hi(), None) && skip_break { + self.neverbreak(); + } else { + self.break_offset_if_not_bol(0, -self.ind, false); + } + self.end(); + } else { + print(self, &values[0]); + } + + self.print_word(")"); + return; + } + + // Otherwise, use commasep + self.print_word("("); + self.commasep(values, pos_lo, pos_hi, print, get_span, format, break_single_no_cmnts); + self.print_word(")"); + } + + fn print_array<'a, T, P, S>(&mut self, values: &'a [T], span: Span, print: P, get_span: S) + where + P: FnMut(&mut Self, &'a T), + S: FnMut(&T) -> Option, + { + if self.handle_span(span, false) { + return; + } + + self.print_word("["); + self.commasep( + values, + span.lo(), + span.hi(), + print, + get_span, + ListFormat::Compact { cmnts_break: false, with_space: false }, + false, + ); + self.print_word("]"); + } + + fn commasep<'a, T, P, S>( + &mut self, + values: &'a [T], + _pos_lo: BytePos, + pos_hi: BytePos, + mut print: P, + mut get_span: S, + format: ListFormat, + break_single_no_cmnts: bool, + ) where + P: FnMut(&mut Self, &'a T), + S: FnMut(&T) -> Option, + { + if values.is_empty() { + return; + } + + let is_single_without_cmnts = values.len() == 1 + && !break_single_no_cmnts + && self.peek_comment_before(pos_hi).is_none(); + + self.s.cbox(self.ind); + let mut skip_first_break = is_single_without_cmnts; + if let Some(first_pos) = get_span(&values[0]).map(Span::lo) { + if let Some((span, style)) = + self.peek_comment_before(first_pos).map(|cmnt| (cmnt.span, cmnt.style)) + { + if self.cursor.enabled + && self.inline_config.is_disabled(span) + && style.is_isolated() + { + self.hardbreak(); + } + let last_style = self.print_comments( + first_pos, + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ); + // If mixed comment before the 1st item, manually handle breaks. + match (style.is_mixed(), last_style.unwrap().is_mixed(), format.breaks_comments()) { + (true, true, true) => self.hardbreak(), + (true, true, false) => self.space(), + (false, true, _) => self.nbsp(), + _ => {} + }; + skip_first_break = true; + } + // Update cursor if previously enabled + if self.cursor.enabled { + self.cursor.advance_to(first_pos, true); + } + } + + if let Some(sym) = format.prev_symbol() { + self.word_space(sym); + } else if !skip_first_break { + format.add_break(true, values.len(), &mut self.s); + } else if is_single_without_cmnts && format.with_space() { + self.nbsp(); + } + if format.is_compact() { + self.s.cbox(0); + } + + let mut skip_last_break = is_single_without_cmnts; + for (i, value) in values.iter().enumerate() { + let is_last = i == values.len() - 1; + let span = get_span(value); + if let Some(span) = span + && self + .print_comments(span.lo(), CommentConfig::skip_ws().mixed_prev_space()) + .is_some_and(|cmnt| cmnt.is_mixed()) + && format.breaks_comments() + { + self.hardbreak(); // trailing and isolated comments already hardbreak + } + + print(self, value); + if !is_last { + self.word(","); + } + let next_pos = if is_last { None } else { get_span(&values[i + 1]).map(Span::lo) } + .unwrap_or(pos_hi); + if !is_last + && format.breaks_comments() + && self.peek_comment_before(next_pos).is_some_and(|cmnt| cmnt.style.is_mixed()) + { + self.hardbreak(); // trailing and isolated comments already hardbreak + } + self.print_comments( + next_pos, + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ); + + if is_last && self.is_bol_or_only_ind() { + // if a trailing comment is printed at the very end, we have to manually adjust + // the offset to avoid having a double break. + self.break_offset_if_not_bol(0, -self.ind, false); + skip_last_break = true; + } + if !is_last && !self.is_bol_or_only_ind() { + format.add_break(false, values.len(), &mut self.s); + } + } + + if format.is_compact() { + self.end(); + } + if !skip_last_break { + if let Some(sym) = format.post_symbol() { + format.add_break(false, values.len(), &mut self.s); + self.s.offset(-self.ind); + self.word(sym); + } else { + format.add_break(true, values.len(), &mut self.s); + self.s.offset(-self.ind); + } + } else if is_single_without_cmnts && format.with_space() { + self.nbsp(); + } else if let Some(sym) = format.post_symbol() { + self.nbsp(); + self.word(sym); + } + self.end(); + self.cursor.advance_to(pos_hi, true); + } +} + +/// Span to source. +impl State<'_, '_> { + fn char_at(&self, pos: BytePos) -> char { + let res = self.sm.lookup_byte_offset(pos); + res.sf.src[res.pos.to_usize()..].chars().next().unwrap() + } + + /// Returns `true` if the span is disabled and has been printed as-is. + #[must_use] + fn handle_span(&mut self, span: Span, skip_prev_cmnts: bool) -> bool { + if !skip_prev_cmnts { + self.print_comments(span.lo(), CommentConfig::default()); + } + self.print_span_if_disabled(span) + } + + /// Returns `None` if the span is disabled and has been printed as-is. + #[must_use] + fn handle_comment(&mut self, cmnt: Comment) -> Option { + if self.cursor.enabled { + if self.inline_config.is_disabled(cmnt.span) { + if cmnt.style.is_trailing() { + self.nbsp(); + } + self.print_span_cold(cmnt.span); + if cmnt.style.is_isolated() || cmnt.style.is_trailing() { + self.print_sep(Separator::Hardbreak); + } + return None; + } + } else if self.print_span_if_disabled(cmnt.span) { + if cmnt.style.is_isolated() || cmnt.style.is_trailing() { + self.print_sep(Separator::Hardbreak); + } + return None; + } + Some(cmnt) + } + + /// Returns `true` if the span is disabled and has been printed as-is. + #[inline] + #[must_use] + fn print_span_if_disabled(&mut self, span: Span) -> bool { + let cursor_span = self.cursor.span(span.hi()); + if self.inline_config.is_disabled(cursor_span) { + // println!("------------"); + // println!("> DISABLED: true"); + // println!("> CURSOR SPAN: {cursor_span:?}"); + // println!("> SNIPPET: '{}'", + // self.sm.span_to_snippet(cursor_span).unwrap_or_default()); + + self.print_span_cold(cursor_span); + return true; + } + if self.inline_config.is_disabled(span) { + // println!("------------"); + // println!("> DISABLED: true"); + // println!("> SPAN: {span:?}"); + // println!("> SNIPPET: {}", self.sm.span_to_snippet(span).unwrap_or_default()); + self.print_span_cold(span); + return true; + } + // println!("------------"); + // println!("> DISABLED: false"); + // println!("> SPAN: {span:?}"); + // println!("> SNIPPET: {}", self.sm.span_to_snippet(span).unwrap_or_default()); + false + } + + #[cold] + fn print_span_cold(&mut self, span: Span) { + self.print_span(span); + } + + fn print_span(&mut self, span: Span) { + match self.sm.span_to_snippet(span) { + Ok(s) => self.s.word(if matches!(self.config.style, IndentStyle::Tab) { + snippet_with_tabs(s, self.config.tab_width) + } else { + s + }), + Err(e) => panic!("failed to print {span:?}: {e:#?}"), + } + // Drop comments that are included in the span. + while let Some(cmnt) = self.peek_comment() { + if cmnt.pos() >= span.hi() { + break; + } + let _ = self.next_comment().unwrap(); + } + // Update cursor + self.cursor.advance_to(span.hi(), false); + } +} + +#[rustfmt::skip] +macro_rules! get_span { + () => { |value| Some(value.span) }; + (()) => { |value| Some(value.span()) }; +} + +/// Language-specific pretty printing. +impl<'ast> State<'_, 'ast> { + pub fn print_source_unit(&mut self, source_unit: &'ast ast::SourceUnit<'ast>) { + let mut items = source_unit.items.iter().peekable(); + let mut is_first = true; + while let Some(item) = items.next() { + // If imports shouldn't be sorted, or if the item is not an import, print it directly. + if !self.config.sort_imports || !matches!(item.kind, ast::ItemKind::Import(_)) { + self.print_item(item, is_first); + is_first = false; + if let Some(next_item) = items.peek() { + self.separate_items(next_item, false); + } + continue; + } + + // Otherwise, collect a group of consecutive imports and sort them before printing. + let mut import_group = vec![item]; + while let Some(next_item) = items.peek() { + // Groups end when the next item is not an import or when there is a blank line. + if !matches!(next_item.kind, ast::ItemKind::Import(_)) + || self.has_comment_between(item.span.hi(), next_item.span.lo()) + { + break; + } + import_group.push(items.next().unwrap()); + } + + import_group.sort_by_key(|item| { + if let ast::ItemKind::Import(import) = &item.kind { + import.path.value.as_str() + } else { + unreachable!("Expected an import item") + } + }); + + for (pos, group_item) in import_group.iter().delimited() { + self.print_item(group_item, is_first); + is_first = false; + + if !pos.is_last { + self.hardbreak_if_not_bol(); + } + } + if let Some(next_item) = items.peek() { + self.separate_items(next_item, false); + } + } + + self.print_remaining_comments(); + } + + /// Prints a hardbreak if the item needs an isolated line break. + fn separate_items(&mut self, next_item: &'ast ast::Item<'ast>, advance: bool) { + if !item_needs_iso(&next_item.kind) { + return; + } + let span = next_item.span; + + let cmnts = self + .comments + .iter() + .filter_map(|c| if c.pos() < span.lo() { Some(c.style) } else { None }) + .collect::>(); + + if let Some(first) = cmnts.first() + && let Some(last) = cmnts.last() + { + if !(first.is_blank() || last.is_blank()) { + self.hardbreak(); + return; + } + if advance { + if self.peek_comment_before(span.lo()).is_some() { + self.print_comments(span.lo(), CommentConfig::default()); + } else if self + .inline_config + .is_disabled(Span::new(span.lo(), span.lo() + BytePos(1))) + { + self.hardbreak(); + self.cursor.advance_to(span.lo(), true); + } + } + } else { + self.hardbreak(); + } + } + + fn print_item(&mut self, item: &'ast ast::Item<'ast>, skip_ws: bool) { + let ast::Item { ref docs, span, ref kind } = *item; + self.print_docs(docs); + + if self.handle_span(item.span, skip_ws) { + if !self.print_trailing_comment(span.hi(), None) { + self.print_sep(Separator::Hardbreak); + } + return; + } + + let add_zero_break = if skip_ws { + self.print_comments(span.lo(), CommentConfig::skip_leading_ws()) + } else { + self.print_comments(span.lo(), CommentConfig::default()) + } + .is_some_and(|cmnt| cmnt.is_mixed()); + + if add_zero_break { + self.zerobreak(); + } + + match kind { + ast::ItemKind::Pragma(pragma) => self.print_pragma(pragma), + ast::ItemKind::Import(import) => self.print_import(import), + ast::ItemKind::Using(using) => self.print_using(using), + ast::ItemKind::Contract(contract) => self.print_contract(contract, span), + ast::ItemKind::Function(func) => self.print_function(func), + ast::ItemKind::Variable(var) => self.print_var_def(var), + ast::ItemKind::Struct(strukt) => self.print_struct(strukt, span), + ast::ItemKind::Enum(enm) => self.print_enum(enm, span), + ast::ItemKind::Udvt(udvt) => self.print_udvt(udvt), + ast::ItemKind::Error(err) => self.print_error(err), + ast::ItemKind::Event(event) => self.print_event(event), + } + + self.cursor.advance_to(span.hi(), true); + self.print_comments(span.hi(), CommentConfig::default()); + self.print_trailing_comment(span.hi(), None); + self.hardbreak_if_not_bol(); + self.cursor.advance(1); + } + + fn print_pragma(&mut self, pragma: &'ast ast::PragmaDirective<'ast>) { + self.word("pragma "); + match &pragma.tokens { + ast::PragmaTokens::Version(ident, semver_req) => { + self.print_ident(ident); + self.nbsp(); + self.word(semver_req.to_string()); + } + ast::PragmaTokens::Custom(a, b) => { + self.print_ident_or_strlit(a); + if let Some(b) = b { + self.nbsp(); + self.print_ident_or_strlit(b); + } + } + ast::PragmaTokens::Verbatim(tokens) => { + self.print_tokens(tokens); + } + } + self.word(";"); + } + + fn print_commasep_aliases<'a, I>(&mut self, aliases: I) + where + I: Iterator)>, + 'ast: 'a, + { + for (pos, (ident, alias)) in aliases.delimited() { + self.print_ident(ident); + if let Some(alias) = alias { + self.word(" as "); + self.print_ident(alias); + } + if !pos.is_last { + self.word(","); + self.space(); + } + } + } + + fn print_import(&mut self, import: &'ast ast::ImportDirective<'ast>) { + let ast::ImportDirective { path, items } = import; + self.word("import "); + match items { + ast::ImportItems::Plain(_) | ast::ImportItems::Glob(_) => { + self.print_ast_str_lit(path); + if let Some(ident) = items.source_alias() { + self.word(" as "); + self.print_ident(&ident); + } + } + + ast::ImportItems::Aliases(aliases) => { + self.s.cbox(self.ind); + self.word("{"); + self.braces_break(); + + if self.config.sort_imports { + let mut sorted: Vec<_> = aliases.iter().collect(); + sorted.sort_by_key(|(ident, _alias)| ident.name.as_str()); + self.print_commasep_aliases(sorted.into_iter()); + } else { + self.print_commasep_aliases(aliases.iter()); + }; + + self.braces_break(); + self.s.offset(-self.ind); + self.word("}"); + self.end(); + self.word(" from "); + self.print_ast_str_lit(path); + } + } + self.word(";"); + } + + fn print_using(&mut self, using: &'ast ast::UsingDirective<'ast>) { + let ast::UsingDirective { list, ty, global } = using; + self.word("using "); + match list { + ast::UsingList::Single(path) => self.print_path(path, true), + ast::UsingList::Multiple(items) => { + self.s.cbox(self.ind); + self.word("{"); + self.braces_break(); + for (pos, (path, op)) in items.iter().delimited() { + self.print_path(path, true); + if let Some(op) = op { + self.word(" as "); + self.word(op.to_str()); + } + if !pos.is_last { + self.word(","); + self.space(); + } + } + self.braces_break(); + self.s.offset(-self.ind); + self.word("}"); + self.end(); + } + } + self.word(" for "); + if let Some(ty) = ty { + self.print_ty(ty); + } else { + self.word("*"); + } + if *global { + self.word(" global"); + } + self.word(";"); + } + + fn print_contract(&mut self, c: &'ast ast::ItemContract<'ast>, span: Span) { + let ast::ItemContract { kind, name, layout, bases, body } = c; + self.contract = Some(c); + self.cursor.advance_to(span.lo(), true); + + self.s.cbox(self.ind); + self.ibox(0); + self.cbox(0); + self.word_nbsp(kind.to_str()); + self.print_ident(name); + self.nbsp(); + + // TODO(rusowsky): move into helper fn to deal with disabled lists of items + if let Some(first) = bases.first().map(|base| base.span()) + && let Some(last) = bases.last().map(|base| base.span()) + && self.inline_config.is_disabled(Span::new(first.lo(), last.hi())) + { + _ = self.handle_span(Span::new(first.lo(), last.hi()), false); + } else if !bases.is_empty() { + self.word("is"); + self.space(); + for (pos, base) in bases.iter().delimited() { + if !self.handle_span(base.span(), false) { + self.print_modifier_call(base, false); + if !pos.is_last { + self.word(","); + self.space(); + } + } + } + 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(); + if !body.is_empty() { + self.print_sep(Separator::Hardbreak); + if self.config.contract_new_lines { + self.hardbreak(); + } + let body_lo = body[0].span.lo(); + if self.peek_comment_before(body_lo).is_some() { + self.print_comments(body_lo, CommentConfig::skip_leading_ws()); + } + + let mut is_first = true; + let mut items = body.iter().peekable(); + while let Some(item) = items.next() { + self.print_item(item, is_first); + is_first = false; + if let Some(next_item) = items.peek() { + if self.inline_config.is_disabled(next_item.span) { + _ = self.handle_span(next_item.span, false); + } else { + self.separate_items(next_item, true); + } + } + } + + if let Some(cmnt) = self.print_comments(span.hi(), CommentConfig::skip_ws()) + && self.config.contract_new_lines + && !cmnt.is_blank() + { + self.print_sep(Separator::Hardbreak); + } + self.s.offset(-self.ind); + self.end(); + if self.config.contract_new_lines { + self.hardbreak_if_nonempty(); + } + } else { + if self.print_comments(span.hi(), CommentConfig::skip_ws()).is_some() { + self.zerobreak(); + } else if self.config.bracket_spacing { + self.nbsp(); + }; + self.end(); + } + self.print_word("}"); + + self.cursor.advance_to(span.hi(), true); + self.contract = None; + } + + fn print_struct(&mut self, strukt: &'ast ast::ItemStruct<'ast>, span: Span) { + let ast::ItemStruct { name, fields } = strukt; + let ind = if self.estimate_size(name.span) + 8 >= self.space_left() { self.ind } else { 0 }; + self.s.ibox(self.ind); + self.word("struct"); + self.space(); + self.print_ident(name); + self.word(" {"); + if !fields.is_empty() { + self.break_offset(SIZE_INFINITY as usize, ind); + } + self.s.ibox(0); + for var in fields.iter() { + self.print_var_def(var); + if !self.print_trailing_comment(var.span.hi(), None) { + self.hardbreak(); + } + } + self.print_comments(span.hi(), CommentConfig::skip_ws()); + if ind == 0 { + self.s.offset(-self.ind); + } + self.end(); + self.end(); + self.word("}"); + } + + fn print_enum(&mut self, enm: &'ast ast::ItemEnum<'ast>, span: Span) { + let ast::ItemEnum { name, variants } = enm; + self.s.cbox(self.ind); + self.word("enum "); + self.print_ident(name); + self.word(" {"); + self.hardbreak_if_nonempty(); + for (pos, ident) in variants.iter().delimited() { + self.print_comments(ident.span.lo(), CommentConfig::default()); + self.print_ident(ident); + if !pos.is_last { + self.word(","); + } + if !self.print_trailing_comment(ident.span.hi(), None) { + self.hardbreak(); + } + } + self.print_comments(span.hi(), CommentConfig::skip_ws()); + self.s.offset(-self.ind); + self.end(); + self.word("}"); + } + + fn print_udvt(&mut self, udvt: &'ast ast::ItemUdvt<'ast>) { + let ast::ItemUdvt { name, ty } = udvt; + self.word("type "); + self.print_ident(name); + self.word(" is "); + self.print_ty(ty); + self.word(";"); + } + + // NOTE(rusowsky): Functions are the only source unit item that handle inline (disabled) format + fn print_function(&mut self, func: &'ast ast::ItemFunction<'ast>) { + let ast::ItemFunction { kind, ref header, ref body, body_span } = *func; + let ast::FunctionHeader { + name, + ref parameters, + visibility, + state_mutability: sm, + virtual_, + ref override_, + ref returns, + .. + } = *header; + + self.s.cbox(self.ind); + + // Print fn name and params + _ = self.handle_span(self.cursor.span(header.span.lo()), false); + self.print_word(kind.to_str()); + if let Some(name) = name { + self.print_sep(Separator::Nbsp); + self.print_ident(&name); + self.cursor.advance_to(name.span.hi(), true); + } + self.s.cbox(-self.ind); + let header_style = self.config.multiline_func_header; + let params_format = match header_style { + MultilineFuncHeaderStyle::ParamsFirst => { + ListFormat::AlwaysBreak { break_single: true, with_space: false } + } + MultilineFuncHeaderStyle::AllParams + if !header.parameters.is_empty() && !self.can_header_fit_in_one_line(header) => + { + ListFormat::AlwaysBreak { break_single: true, with_space: false } + } + _ => ListFormat::Consistent { cmnts_break: true, with_space: false }, + }; + self.print_parameter_list(parameters, parameters.span, params_format); + self.end(); + + // Map attributes to their corresponding comments + let (mut map, attributes, first_attrib_pos) = + AttributeCommentMapper::new(returns.as_ref(), body_span.lo()).build(self, header); + + let mut handle_pre_cmnts = |this: &mut Self, span: Span| -> bool { + if this.inline_config.is_disabled(span) + // Note: `map` is still captured from the outer scope, which is fine. + && let Some((pre_cmnts, _)) = map.remove(&span.lo()) + { + for (pos, cmnt) in pre_cmnts.into_iter().delimited() { + if pos.is_first && cmnt.style.is_isolated() && !this.is_bol_or_only_ind() { + this.print_sep(Separator::Hardbreak); + } + if let Some(cmnt) = this.handle_comment(cmnt) { + this.print_comment(cmnt, CommentConfig::skip_ws().mixed_post_nbsp()); + } + if pos.is_last { + return true; + } + } + } + false + }; + + let skip_attribs = returns.as_ref().is_some_and(|ret| { + let attrib_span = Span::new(first_attrib_pos, ret.span.lo()); + handle_pre_cmnts(self, attrib_span); + self.handle_span(attrib_span, false) + }); + let skip_returns = { + let pos = if skip_attribs { self.cursor.pos } else { first_attrib_pos }; + let ret_span = Span::new(pos, body_span.lo()); + handle_pre_cmnts(self, ret_span); + self.handle_span(ret_span, false) + }; + + let attrib_box = self.config.multiline_func_header.params_first() + || (self.config.multiline_func_header.attrib_first() + && !self.can_header_params_fit_in_one_line(header)); + if attrib_box { + self.s.cbox(0); + } + if !(skip_attribs || skip_returns) { + // Print fn attributes in correct order + if let Some(v) = visibility { + self.print_fn_attribute(v.span, &mut map, &mut |s| s.word(v.to_str())); + } + if let Some(sm) = sm + && !matches!(*sm, ast::StateMutability::NonPayable) + { + self.print_fn_attribute(sm.span, &mut map, &mut |s| s.word(sm.to_str())); + } + if let Some(v) = virtual_ { + self.print_fn_attribute(v, &mut map, &mut |s| s.word("virtual")); + } + if let Some(o) = override_ { + self.print_fn_attribute(o.span, &mut map, &mut |s| s.print_override(o)); + } + for m in attributes.iter().filter(|a| matches!(a.kind, AttributeKind::Modifier(_))) { + if let AttributeKind::Modifier(modifier) = m.kind { + let is_base = self.is_modifier_a_base_contract(kind, modifier); + self.print_fn_attribute(m.span, &mut map, &mut |s| { + s.print_modifier_call(modifier, is_base) + }); + } + } + } + if !skip_returns + && let Some(ret) = returns + && !ret.is_empty() + && let Some(ret) = returns + { + if !self.handle_span(self.cursor.span(ret.span.lo()), false) { + if !self.is_bol_or_only_ind() && !self.last_token_is_space() { + self.print_sep(Separator::Space); + } + self.cursor.advance_to(ret.span.lo(), true); + self.print_word("returns "); + } + self.print_parameter_list( + ret, + ret.span, + ListFormat::Consistent { cmnts_break: false, with_space: false }, + ); + } + + // Print fn body + if let Some(body) = body { + if self.handle_span(self.cursor.span(body_span.lo()), false) { + // Print spacing if necessary. Updates cursor. + } else { + if let Some(cmnt) = self.peek_comment_before(body_span.lo()) { + if cmnt.style.is_mixed() { + // These shouldn't update the cursor, as we've already dealt with it above + self.space(); + self.s.offset(-self.ind); + self.print_comments(body_span.lo(), CommentConfig::skip_ws()); + } else { + self.zerobreak(); + self.s.offset(-self.ind); + self.print_comments(body_span.lo(), CommentConfig::skip_ws()); + self.s.offset(-self.ind); + } + } else { + // If there are no modifiers, overrides, nor returns never break + if header.modifiers.is_empty() + && header.override_.is_none() + && returns.as_ref().is_none_or(|r| r.is_empty()) + { + self.nbsp(); + } else { + self.space(); + self.s.offset(-self.ind); + } + } + self.cursor.advance_to(body_span.lo(), true); + } + self.print_word("{"); + self.end(); + if attrib_box { + self.end(); + } + + self.print_block_without_braces(body, body_span.hi(), Some(self.ind)); + if self.cursor.enabled || self.cursor.pos < body_span.hi() { + self.print_word("}"); + self.cursor.advance_to(body_span.hi(), true); + } + } else { + self.print_comments(body_span.lo(), CommentConfig::skip_ws().mixed_prev_space()); + self.end(); + if attrib_box { + self.end(); + } + self.neverbreak(); + self.print_word(";"); + } + + if let Some(cmnt) = self.peek_trailing_comment(body_span.hi(), None) { + if cmnt.is_doc { + // trailing doc comments after the fn body are isolated + // these shouldn't update the cursor, as this is our own formatting + self.hardbreak(); + self.hardbreak(); + } + self.print_trailing_comment(body_span.hi(), None); + } + } + + fn is_modifier_a_base_contract( + &self, + kind: ast::FunctionKind, + modifier: &'ast ast::Modifier<'ast>, + ) -> bool { + // Add `()` in functions when the modifier is a base contract. + // HACK: heuristics: + // 1. exactly matches the name of a base contract as declared in the `contract is`; + // this does not account for inheritance; + let is_contract_base = self.contract.is_some_and(|contract| { + contract.bases.iter().any(|contract_base| contract_base.name == modifier.name) + }); + // 2. assume that title case names in constructors are bases. + // LEGACY: constructors used to also be `function NameOfContract...`; not checked. + let is_constructor = matches!(kind, ast::FunctionKind::Constructor); + // LEGACY: we are checking the beginning of the path, not the last segment. + is_contract_base + || (is_constructor + && modifier.name.first().name.as_str().starts_with(char::is_uppercase)) + } + + fn print_error(&mut self, err: &'ast ast::ItemError<'ast>) { + let ast::ItemError { name, parameters } = err; + self.word("error "); + self.print_ident(name); + self.print_parameter_list( + parameters, + parameters.span, + ListFormat::Compact { cmnts_break: false, with_space: false }, + ); + self.word(";"); + } + + fn print_event(&mut self, event: &'ast ast::ItemEvent<'ast>) { + let ast::ItemEvent { name, parameters, anonymous } = event; + self.word("event "); + self.print_ident(name); + self.print_parameter_list( + parameters, + parameters.span, + ListFormat::Compact { cmnts_break: true, with_space: false }, + ); + if *anonymous { + self.word(" anonymous"); + } + self.word(";"); + } + + fn print_var_def(&mut self, var: &'ast ast::VariableDefinition<'ast>) { + self.print_var(var, true); + self.word(";"); + } + + fn print_var(&mut self, var: &'ast ast::VariableDefinition<'ast>, is_var_def: bool) { + let ast::VariableDefinition { + span, + ty, + visibility, + mutability, + data_location, + override_, + indexed, + name, + initializer, + } = var; + + if self.handle_span(*span, false) { + return; + } + + // NOTE(rusowsky): this is hacky but necessary to properly estimate if we figure out if we + // have double breaks (which should have double indentation) or not. + // Alternatively, we could achieve the same behavior with a new box group that supports + // "continuation" which would only increase indentation if its parent box broke. + let init_space_left = self.space_left(); + let mut pre_init_size = self.estimate_size(ty.span); + + // Non-elementary types use commasep which has its own padding. + self.s.ibox(0); + if override_.is_some() { + self.s.cbox(self.ind); + } else { + self.s.ibox(self.ind); + } + self.print_ty(ty); + if let Some(visibility) = visibility { + self.print_sep(Separator::SpaceOrNbsp(is_var_def)); + self.print_word(visibility.to_str()); + pre_init_size += visibility.to_str().len() + 1; + } + if let Some(mutability) = mutability { + self.print_sep(Separator::SpaceOrNbsp(is_var_def)); + self.print_word(mutability.to_str()); + pre_init_size += mutability.to_str().len() + 1; + } + if let Some(data_location) = data_location { + self.print_sep(Separator::SpaceOrNbsp(is_var_def)); + self.print_word(data_location.to_str()); + pre_init_size += data_location.to_str().len() + 1; + } + if let Some(override_) = override_ { + if self + .print_comments(override_.span.lo(), CommentConfig::skip_ws().mixed_prev_space()) + .is_none() + { + self.print_sep(Separator::SpaceOrNbsp(is_var_def)); + } + self.ibox(0); + self.print_override(override_); + pre_init_size += self.estimate_size(override_.span) + 1; + } + if *indexed { + self.print_sep(Separator::SpaceOrNbsp(is_var_def)); + self.print_word("indexed"); + pre_init_size += 8; + } + if let Some(ident) = name { + self.print_sep(Separator::SpaceOrNbsp(is_var_def && override_.is_none())); + self.print_comments( + ident.span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(), + ); + self.print_ident(ident); + pre_init_size += self.estimate_size(ident.span) + 1; + } + if let Some(init) = initializer { + self.var_init = true; + self.print_word(" ="); + if override_.is_some() { + self.end(); + } + self.end(); + if pre_init_size + 2 <= init_space_left { + self.neverbreak(); + } + 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 is_binary_expr(&init.kind) { + if !self.is_bol_or_only_ind() { + Separator::Space.print(&mut self.s, &mut self.cursor); + } + if matches!(ty.kind, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..)) { + self.s.offset(self.ind); + } + self.print_expr(init); + } else { + self.s.ibox(if pre_init_size + 3 > init_space_left { self.ind } else { 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() { + self.print_sep(Separator::Nbsp); + } + } else { + if !self.is_bol_or_only_ind() { + Separator::Space.print(&mut self.s, &mut self.cursor); + } + if matches!(ty.kind, ast::TypeKind::Elementary(..) | ast::TypeKind::Mapping(..)) + { + self.s.offset(self.ind); + } + } + self.print_expr(init); + self.end(); + } + self.var_init = false; + } else { + self.end(); + } + self.end(); + } + + fn print_parameter_list( + &mut self, + parameters: &'ast [ast::VariableDefinition<'ast>], + span: Span, + format: ListFormat, + ) { + if self.handle_span(span, false) { + return; + } + + self.print_tuple( + parameters, + span.lo(), + span.hi(), + |fmt, var| fmt.print_var(var, false), + get_span!(), + format, + matches!(format, ListFormat::AlwaysBreak { break_single: true, .. }), + ); + } + + fn print_docs(&mut self, docs: &'ast ast::DocComments<'ast>) { + // Intetionally no-op. Handled with `self.comments`. + let _ = docs; + } + + fn print_ident_or_strlit(&mut self, value: &'ast ast::IdentOrStrLit) { + match value { + ast::IdentOrStrLit::Ident(ident) => self.print_ident(ident), + ast::IdentOrStrLit::StrLit(strlit) => self.print_ast_str_lit(strlit), + } + } + + fn print_tokens(&mut self, tokens: &[token::Token]) { + // Leave unchanged. + let span = Span::join_first_last(tokens.iter().map(|t| t.span)); + self.print_span(span); + } + + fn print_word(&mut self, w: impl Into>) { + let cow = w.into(); + self.cursor.advance(cow.len() as u32); + self.word(cow); + } + + fn print_sep(&mut self, sep: Separator) { + if self.handle_span(self.cursor.span(self.cursor.pos + BytePos(1)), true) { + return; + } + + sep.print(&mut self.s, &mut self.cursor); + } + + fn print_ident(&mut self, ident: &ast::Ident) { + if self.handle_span(ident.span, false) { + return; + } + + self.print_comments(ident.span.lo(), CommentConfig::skip_ws()); + self.word(ident.to_string()); + } + + fn print_path(&mut self, path: &'ast ast::PathSlice, consistent_break: bool) { + if consistent_break { + self.s.cbox(self.ind); + } else { + self.s.ibox(self.ind); + } + for (pos, ident) in path.segments().iter().delimited() { + self.print_ident(ident); + if !pos.is_last { + self.zerobreak(); + self.word("."); + } + } + self.end(); + } + + // TODO: Yul literals are slightly different than normal solidity ones + fn print_lit(&mut self, lit: &'ast ast::Lit) { + let ast::Lit { span, symbol, ref kind } = *lit; + if self.handle_span(span, false) { + return; + } + + match *kind { + ast::LitKind::Str(kind, ..) => { + self.cbox(0); + for (pos, (span, symbol)) in lit.literals().delimited() { + self.ibox(0); + if !self.handle_span(span, false) { + let quote_pos = span.lo() + kind.prefix().len() as u32; + self.print_str_lit(kind, quote_pos, symbol.as_str()); + } + if !pos.is_last { + if !self.print_trailing_comment(span.hi(), None) { + self.space_if_not_bol(); + } + } else { + self.neverbreak(); + } + self.end(); + } + self.end(); + } + ast::LitKind::Number(_) | ast::LitKind::Rational(_) => { + self.print_num_literal(symbol.as_str()); + } + ast::LitKind::Address(value) => self.word(value.to_string()), + ast::LitKind::Bool(value) => self.word(if value { "true" } else { "false" }), + ast::LitKind::Err(_) => self.word(symbol.to_string()), + } + } + + fn print_num_literal(&mut self, source: &str) { + fn strip_underscores_if(b: bool, s: &str) -> Cow<'_, str> { + if b && s.contains('_') { Cow::Owned(s.replace('_', "")) } else { Cow::Borrowed(s) } + } + + fn add_underscores( + out: &mut String, + config: config::NumberUnderscore, + string: &str, + reversed: bool, + ) { + if !config.is_thousands() || string.len() < 5 { + out.push_str(string); + return; + } + + let chunks = if reversed { + Either::Left(string.as_bytes().chunks(3)) + } else { + Either::Right(string.as_bytes().rchunks(3).rev()) + } + .map(|chunk| std::str::from_utf8(chunk).unwrap()); + for chunk in Itertools::intersperse(chunks, "_") { + out.push_str(chunk); + } + } + + debug_assert!(source.is_ascii(), "{source:?}"); + + let config = self.config.number_underscore; + + let (val, exp) = source.split_once(['e', 'E']).unwrap_or((source, "")); + let (val, fract) = val.split_once('.').unwrap_or((val, "")); + + let strip_underscores = !config.is_preserve(); + 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)[..]; + + // strip any padded 0's + let mut exp_sign = ""; + if !["0x", "0b", "0o"].iter().any(|prefix| source.starts_with(prefix)) { + val = val.trim_start_matches('0'); + fract = fract.trim_end_matches('0'); + (exp_sign, exp) = + if let Some(exp) = exp.strip_prefix('-') { ("-", exp) } else { ("", exp) }; + exp = exp.trim_start_matches('0'); + } + + let mut out = String::with_capacity(source.len() * 2); + if val.is_empty() { + out.push('0'); + } else { + add_underscores(&mut out, config, val, false); + } + if source.contains('.') { + out.push('.'); + if !fract.is_empty() { + add_underscores(&mut out, config, fract, true); + } else { + out.push('0'); + } + } + if !exp.is_empty() { + // TODO: preserve the `E`? + /* + out.push(if source.contains('e') { + 'e' + } else { + debug_assert!(source.contains('E')); + 'E' + }); + */ + out.push('e'); + out.push_str(exp_sign); + add_underscores(&mut out, config, exp, false); + } + + self.word(out); + } + + /// Prints a raw AST string literal, which is unescaped. + fn print_ast_str_lit(&mut self, strlit: &'ast ast::StrLit) { + self.print_str_lit(ast::StrKind::Str, strlit.span.lo(), strlit.value.as_str()); + } + + /// `s` should be the *unescaped contents of the string literal*. + fn print_str_lit(&mut self, kind: ast::StrKind, quote_pos: BytePos, s: &str) { + self.print_comments(quote_pos, CommentConfig::default()); + let s = self.str_lit_to_string(kind, quote_pos, s); + self.word(s); + } + + /// `s` should be the *unescaped contents of the string literal*. + fn str_lit_to_string(&self, kind: ast::StrKind, quote_pos: BytePos, s: &str) -> String { + let prefix = kind.prefix(); + let quote = match self.config.quote_style { + config::QuoteStyle::Double => '\"', + config::QuoteStyle::Single => '\'', + config::QuoteStyle::Preserve => self.char_at(quote_pos), + }; + debug_assert!(matches!(quote, '\"' | '\''), "{quote:?}"); + let s = solar_parse::interface::data_structures::fmt::from_fn(move |f| { + if matches!(kind, ast::StrKind::Hex) { + match self.config.hex_underscore { + config::HexUnderscore::Preserve => {} + config::HexUnderscore::Remove | config::HexUnderscore::Bytes => { + let mut clean = s.to_string().replace('_', ""); + if matches!(self.config.hex_underscore, config::HexUnderscore::Bytes) { + clean = + clean.chars().chunks(2).into_iter().map(|c| c.format("")).join("_"); + } + return f.write_str(&clean); + } + }; + } + f.write_str(s) + }); + let mut s = format!("{prefix}{quote}{s}{quote}"); + + // If the output is not a single token then revert to the original quote. + if Cursor::new(&s).exactly_one().is_err() { + let other_quote = if quote == '\"' { '\'' } else { '\"' }; + { + let s = unsafe { s.as_bytes_mut() }; + s[prefix.len()] = other_quote as u8; + s[s.len() - 1] = other_quote as u8; + } + debug_assert!(Cursor::new(&s).exactly_one().map(|_| true).unwrap()); + } + + s + } + + fn print_ty(&mut self, ty: &'ast ast::Type<'ast>) { + if self.handle_span(ty.span, false) { + return; + } + + match &ty.kind { + &ast::TypeKind::Elementary(ty) => 'b: { + match ty { + // `address payable` is normalized to `address`. + ast::ElementaryType::Address(true) => { + self.word("address payable"); + break 'b; + } + // Integers are normalized to long form. + ast::ElementaryType::Int(size) | ast::ElementaryType::UInt(size) => { + match (self.config.int_types, size.bits_raw()) { + (config::IntTypes::Short, 0 | 256) + | (config::IntTypes::Preserve, 0) => { + let short = match ty { + ast::ElementaryType::Int(_) => "int", + ast::ElementaryType::UInt(_) => "uint", + _ => unreachable!(), + }; + self.word(short); + break 'b; + } + _ => {} + } + } + _ => {} + } + self.word(ty.to_abi_str()); + } + ast::TypeKind::Array(ast::TypeArray { element, size }) => { + self.print_ty(element); + if let Some(size) = size { + self.word("["); + self.print_expr(size); + self.word("]"); + } else { + self.word("[]"); + } + } + ast::TypeKind::Function(ast::TypeFunction { + parameters, + visibility, + state_mutability, + returns, + }) => { + self.cbox(0); + self.word("function"); + self.print_parameter_list(parameters, parameters.span, ListFormat::Inline); + self.space(); + + if let Some(v) = visibility { + self.word(v.to_str()); + self.nbsp(); + } + if let Some(sm) = state_mutability + && !matches!(**sm, ast::StateMutability::NonPayable) + { + self.word(sm.to_str()); + self.nbsp(); + } + if let Some(ret) = returns + && !ret.is_empty() + { + self.word("returns"); + self.nbsp(); + self.print_parameter_list( + ret, + ret.span, + ListFormat::Consistent { cmnts_break: false, with_space: false }, + ); + } + self.end(); + } + ast::TypeKind::Mapping(ast::TypeMapping { key, key_name, value, value_name }) => { + self.word("mapping("); + self.s.cbox(0); + if let Some(cmnt) = self.peek_comment_before(key.span.lo()) { + if cmnt.style.is_mixed() { + self.print_comments( + key.span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ); + self.break_offset_if_not_bol(SIZE_INFINITY as usize, 0, false); + } else { + self.print_comments(key.span.lo(), CommentConfig::skip_ws()); + } + } + // Fitting a mapping in one line takes, at least, 16 chars (one-char var name): + // 'mapping(' + {key} + ' => ' {value} ') ' + {name} + ';' + // To be more conservative, we use 18 to decide whether to force a break or not. + else if 18 + + self.estimate_size(key.span) + + key_name.map(|k| self.estimate_size(k.span)).unwrap_or(0) + + self.estimate_size(value.span) + + value_name.map(|v| self.estimate_size(v.span)).unwrap_or(0) + >= self.space_left() + { + self.hardbreak(); + } else { + self.zerobreak(); + } + self.s.cbox(0); + self.print_ty(key); + if let Some(ident) = key_name { + if self + .print_comments( + ident.span.lo(), + CommentConfig::skip_ws() + .mixed_no_break() + .mixed_prev_space() + .mixed_post_nbsp(), + ) + .is_none() + { + self.nbsp(); + } + self.print_ident(ident); + } + // NOTE(rusowsky): unless we add more spans to solar, using `value.span.lo()` + // consumes "comment6" of which should be printed after the `=>` + self.print_comments( + value.span.lo(), + CommentConfig::skip_ws() + .trailing_no_break() + .mixed_no_break() + .mixed_prev_space(), + ); + self.space(); + self.s.offset(self.ind); + self.word("=> "); + self.s.ibox(self.ind); + self.print_ty(value); + if let Some(ident) = value_name { + self.neverbreak(); + if self + .print_comments( + ident.span.lo(), + CommentConfig::skip_ws() + .mixed_no_break() + .mixed_prev_space() + .mixed_post_nbsp(), + ) + .is_none() + { + self.nbsp(); + } + self.print_ident(ident); + if self + .peek_comment_before(ty.span.hi()) + .is_some_and(|cmnt| cmnt.style.is_mixed()) + { + self.neverbreak(); + self.print_comments( + value.span.lo(), + CommentConfig::skip_ws().mixed_no_break(), + ); + } + } + self.end(); + self.end(); + if self + .print_comments( + ty.span.hi(), + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ) + .is_some_and(|cmnt| !cmnt.is_mixed()) + { + self.break_offset_if_not_bol(0, -self.ind, false); + } else { + self.zerobreak(); + self.s.offset(-self.ind); + } + self.end(); + self.word(")"); + } + ast::TypeKind::Custom(path) => self.print_path(path, false), + } + } + + fn print_override(&mut self, override_: &'ast ast::Override<'ast>) { + let ast::Override { span, paths } = override_; + if self.handle_span(*span, false) { + return; + } + self.word("override"); + if !paths.is_empty() { + if self.config.override_spacing { + self.nbsp(); + } + self.print_tuple( + paths, + span.lo(), + span.hi(), + |this, path| this.print_path(path, false), + get_span!(()), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + false, + ); + } + } + + /* --- Expressions --- */ + + fn print_expr(&mut self, expr: &'ast ast::Expr<'ast>) { + let ast::Expr { span, ref kind } = *expr; + if self.handle_span(span, false) { + return; + } + + match kind { + ast::ExprKind::Array(exprs) => { + self.print_array(exprs, expr.span, |this, e| this.print_expr(e), get_span!()) + } + ast::ExprKind::Assign(lhs, None, rhs) => { + self.s.ibox(if has_complex_successor(&rhs.kind, false) { 0 } else { self.ind }); + self.print_expr(lhs); + self.word(" = "); + self.neverbreak(); + self.print_expr(rhs); + self.end(); + } + ast::ExprKind::Assign(lhs, Some(bin_op), rhs) + | ast::ExprKind::Binary(lhs, bin_op, rhs) => { + let is_parent = matches!(lhs.kind, ast::ExprKind::Binary(..)) + || matches!(rhs.kind, ast::ExprKind::Binary(..)); + let is_child = self.binary_expr; + if !is_child && is_parent { + // top-level expression of the chain -> set cache + self.binary_expr = true; + self.s.ibox(self.ind); + } else if !is_child || !is_parent { + self.ibox(0); + } + + self.print_expr(lhs); + if let ast::ExprKind::Assign(..) = kind { + if !self.print_trailing_comment(lhs.span.hi(), Some(rhs.span.lo())) { + self.nbsp(); + } + self.word(bin_op.kind.to_str()); + self.word("="); + } else { + if !self.print_trailing_comment(lhs.span.hi(), Some(rhs.span.lo())) { + self.space_if_not_bol(); + } + self.word(bin_op.kind.to_str()); + } + + // box expressions with complex successors to accommodate their own indentation + if !is_child && is_parent { + if has_complex_successor(&rhs.kind, true) { + self.s.ibox(-self.ind); + } else if has_complex_successor(&rhs.kind, false) { + self.s.ibox(0); + } + } + self.nbsp(); + self.print_expr(rhs); + + if (has_complex_successor(&rhs.kind, false) + || has_complex_successor(&rhs.kind, true)) + && (!is_child && is_parent) + { + self.end(); + } + + if !is_child { + // top-level expression of the chain -> clear cache + self.binary_expr = false; + self.end(); + } else if !is_parent { + self.end(); + } + } + ast::ExprKind::Call(expr, call_args) => { + self.print_expr(expr); + self.print_call_args(call_args); + } + ast::ExprKind::CallOptions(expr, named_args) => { + self.print_expr(expr); + self.print_named_args(named_args, span.hi()); + } + ast::ExprKind::Delete(expr) => { + self.word("delete "); + self.print_expr(expr); + } + ast::ExprKind::Ident(ident) => self.print_ident(ident), + ast::ExprKind::Index(expr, kind) => { + self.print_expr(expr); + self.word("["); + self.s.cbox(self.ind); + + let mut skip_break = false; + match kind { + ast::IndexKind::Index(expr) => { + if let Some(expr) = expr { + self.zerobreak(); + self.print_expr(expr); + } + } + ast::IndexKind::Range(expr0, expr1) => { + if let Some(expr0) = expr0 { + if self + .print_comments(expr0.span.lo(), CommentConfig::skip_ws()) + .is_none_or(|s| s.is_mixed()) + { + self.zerobreak(); + } + self.print_expr(expr0); + } else { + self.zerobreak(); + } + self.word(":"); + if let Some(expr1) = expr1 { + self.s.ibox(self.ind); + if expr0.is_some() { + self.zerobreak(); + } + self.print_comments( + expr1.span.lo(), + CommentConfig::skip_ws() + .mixed_prev_space() + .mixed_no_break() + .mixed_post_nbsp(), + ); + self.print_expr(expr1); + } + + let mut is_trailing = false; + if let Some(style) = self.print_comments( + span.hi(), + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space(), + ) { + skip_break = true; + is_trailing = style.is_trailing(); + } + + // Manually revert indentation if there is `expr1` and/or comments. + if skip_break && expr1.is_some() { + self.break_offset_if_not_bol(0, -2 * self.ind, false); + self.end(); + // if a trailing comment is printed at the very end, we have to manually + // adjust the offset to avoid having a double break. + if !is_trailing { + self.break_offset_if_not_bol(0, -self.ind, false); + } + } else if skip_break { + self.break_offset_if_not_bol(0, -self.ind, false); + } else if expr1.is_some() { + self.end(); + } + } + } + if !skip_break { + self.zerobreak(); + self.s.offset(-self.ind); + } + self.end(); + self.word("]"); + } + ast::ExprKind::Lit(lit, unit) => { + self.print_lit(lit); + if let Some(unit) = unit { + self.nbsp(); + self.word(unit.to_str()); + } + } + ast::ExprKind::Member(expr, ident) => { + let is_child = self.member_expr; + if !is_child { + // top-level expression of the chain -> set cache + self.member_expr = true; + self.s.ibox(self.ind); + } + + self.print_expr(expr); + self.print_trailing_comment(expr.span.hi(), Some(ident.span.lo())); + if !matches!(expr.kind, ast::ExprKind::Ident(_) | ast::ExprKind::Type(_)) { + self.zerobreak(); + } + self.word("."); + self.print_ident(ident); + + if !is_child { + // top-level expression of the chain -> clear cache + self.member_expr = false; + self.end(); + } + } + ast::ExprKind::New(ty) => { + self.word("new "); + self.print_ty(ty); + } + ast::ExprKind::Payable(args) => { + self.word("payable"); + self.print_call_args(args); + } + ast::ExprKind::Ternary(cond, then, els) => { + self.s.cbox(self.ind); + // conditional expression + self.s.ibox(0); + self.print_comments(cond.span.lo(), CommentConfig::skip_ws()); + self.print_expr(cond); + let cmnt = self.peek_comment_before(then.span.lo()); + if cmnt.is_some() { + self.space(); + } + self.print_comments(then.span.lo(), CommentConfig::skip_ws()); + self.end(); + if !self.is_bol_or_only_ind() { + self.space(); + } + // then expression + self.s.ibox(0); + self.word("? "); + self.print_expr(then); + let cmnt = self.peek_comment_before(els.span.lo()); + if cmnt.is_some() { + self.space(); + } + self.print_comments(els.span.lo(), CommentConfig::skip_ws()); + self.end(); + if !self.is_bol_or_only_ind() { + self.space(); + } + // else expression + self.s.ibox(0); + self.word(": "); + self.print_expr(els); + self.end(); + self.neverbreak(); + self.s.offset(-self.ind); + self.end(); + } + ast::ExprKind::Tuple(exprs) => self.print_tuple( + exprs, + span.lo(), + span.hi(), + |this, expr| { + if let Some(expr) = expr { + this.print_expr(expr); + } + }, + |e| e.as_deref().map(|e| e.span), + ListFormat::Compact { cmnts_break: false, with_space: false }, + is_binary_expr(&expr.kind), + ), + ast::ExprKind::TypeCall(ty) => { + self.word("type"); + self.print_tuple( + std::slice::from_ref(ty), + span.lo(), + span.hi(), + Self::print_ty, + get_span!(), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + false, + ); + } + ast::ExprKind::Type(ty) => self.print_ty(ty), + ast::ExprKind::Unary(un_op, expr) => { + let prefix = un_op.kind.is_prefix(); + let op = un_op.kind.to_str(); + if prefix { + self.word(op); + } + self.print_expr(expr); + if !prefix { + debug_assert!(un_op.kind.is_postfix()); + self.word(op); + } + } + } + self.cursor.advance_to(span.hi(), true); + } + + // If `add_parens_if_empty` is true, then add parentheses `()` even if there are no arguments. + fn print_modifier_call( + &mut self, + modifier: &'ast ast::Modifier<'ast>, + add_parens_if_empty: bool, + ) { + let ast::Modifier { name, arguments } = modifier; + self.print_path(name, false); + if !arguments.is_empty() || add_parens_if_empty { + self.print_call_args(arguments); + } + } + + fn print_call_args(&mut self, args: &'ast ast::CallArgs<'ast>) { + let ast::CallArgs { span, ref kind } = *args; + if self.handle_span(span, true) { + return; + } + + match kind { + ast::CallArgsKind::Unnamed(exprs) => { + self.print_tuple( + exprs, + span.lo(), + span.hi(), + |this, e| this.print_expr(e), + get_span!(), + ListFormat::Compact { cmnts_break: true, with_space: false }, + false, + ); + } + ast::CallArgsKind::Named(named_args) => { + self.word("("); + self.print_named_args(named_args, span.hi()); + self.word(")"); + } + } + } + + fn print_named_args(&mut self, args: &'ast [ast::NamedArg<'ast>], pos_hi: BytePos) { + let parent_call = self.call_expr_named; + if !parent_call { + self.call_expr_named = true; + } + + 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()); + let ind = if parent_call { self.ind } else { 0 }; + + self.commasep( + args, + list_lo, + pos_hi, + // Closure to print a single named argument (`name: value`) + |s, arg| { + s.cbox(ind); + 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| Some(ast::Span::new(arg.name.span.lo(), arg.value.span.hi())), + ListFormat::Consistent { cmnts_break: true, with_space: self.config.bracket_spacing }, + true, + ); + self.word("}"); + + if parent_call { + self.call_expr_named = false; + } + } + + /* --- Statements --- */ + + fn print_stmt(&mut self, stmt: &'ast ast::Stmt<'ast>) { + let ast::Stmt { ref docs, span, ref kind } = *stmt; + self.print_docs(docs); + + // Handle disabled statements. + if self.handle_span(span, false) { + self.print_trailing_comment_no_break(stmt.span.hi(), None); + return; + } + + // return statements can't have a preceding comment in the same line. + let force_break = matches!(kind, ast::StmtKind::Return(..)) + && self.peek_comment_before(span.lo()).is_some_and(|cmnt| cmnt.style.is_mixed()); + + match kind { + ast::StmtKind::Assembly(ast::StmtAssembly { dialect, flags, block }) => { + _ = self.handle_span(self.cursor.span(span.lo()), false); + if !self.handle_span(Span::new(span.lo(), block.span.lo()), false) { + self.cursor.advance_to(span.lo(), true); + self.print_word("assembly "); + if let Some(dialect) = dialect { + self.print_ast_str_lit(dialect); + self.print_sep(Separator::Nbsp); + } + if !flags.is_empty() { + self.print_tuple( + flags, + span.lo(), + span.hi(), + Self::print_ast_str_lit, + get_span!(), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + false, + ); + self.print_sep(Separator::Nbsp); + } + } + self.print_yul_block(block, block.span, false); + } + ast::StmtKind::DeclSingle(var) => self.print_var(var, true), + ast::StmtKind::DeclMulti(vars, expr) => { + self.print_tuple( + vars, + span.lo(), + span.hi(), + |this, var| { + if let Some(var) = var { + this.print_var(var, true); + } + }, + |v| v.as_ref().map(|v| v.span), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + false, + ); + self.word(" = "); + self.neverbreak(); + self.print_expr(expr); + } + ast::StmtKind::Block(stmts) => self.print_block(stmts, span), + ast::StmtKind::Break => self.word("break"), + ast::StmtKind::Continue => self.word("continue"), + ast::StmtKind::DoWhile(stmt, cond) => { + self.word("do "); + self.print_stmt_as_block(stmt, cond.span.lo(), false); + self.nbsp(); + self.print_if_cond("while", cond, cond.span.hi()); + } + ast::StmtKind::Emit(path, args) => self.print_emit_or_revert("emit", path, args), + ast::StmtKind::Expr(expr) => self.print_expr(expr), + ast::StmtKind::For { init, cond, next, body } => { + self.cbox(0); + self.s.ibox(self.ind); + self.print_word("for ("); + self.zerobreak(); + self.s.cbox(0); + if let Some(init) = init { + self.print_stmt(init); + } else { + self.print_word(";"); + } + if let Some(cond) = cond { + self.print_sep(Separator::Space); + self.print_expr(cond); + } else { + self.zerobreak(); + } + self.print_word(";"); + if let Some(next) = next { + self.space(); + self.print_expr(next); + } else { + self.zerobreak(); + } + self.break_offset_if_not_bol(0, -self.ind, false); + self.end(); + self.print_word(") "); + self.neverbreak(); + self.end(); + self.print_comments(body.span.lo(), CommentConfig::skip_ws()); + self.print_stmt_as_block(body, span.hi(), false); + self.end(); + } + ast::StmtKind::If(cond, then, els_opt) => { + // Check if blocks should be inlined and update cache if necessary + let inline = self.is_single_line_block(cond, then, els_opt.as_ref()); + if !inline.is_cached && self.single_line_stmt.is_none() { + self.single_line_stmt = Some(inline.outcome); + } + + self.cbox(0); + self.ibox(0); + // Print if stmt + self.print_if_no_else(cond, then, inline.outcome); + // Print else (if) stmts, if any + let mut els_opt = els_opt.as_deref(); + while let Some(els) = els_opt { + if self.ends_with('}') { + match self.print_comments( + els.span.lo(), + CommentConfig::skip_ws().mixed_no_break(), + ) { + Some(cmnt) => { + if cmnt.is_mixed() { + self.hardbreak(); + } + } + None => self.nbsp(), + } + } else { + self.hardbreak_if_not_bol(); + if self + .print_comments(els.span.lo(), CommentConfig::skip_ws()) + .is_some_and(|cmnt| cmnt.is_mixed()) + { + self.hardbreak(); + }; + } + self.ibox(0); + self.print_word("else "); + if let ast::StmtKind::If(cond, then, els) = &els.kind { + self.print_if_no_else(cond, then, inline.outcome); + els_opt = els.as_deref(); + continue; + } else { + self.print_stmt_as_block(els, span.hi(), inline.outcome); + self.end(); + } + break; + } + self.end(); + + // Clear cache if necessary + if !inline.is_cached && self.single_line_stmt.is_some() { + self.single_line_stmt = None; + } + } + ast::StmtKind::Return(expr) => { + if force_break { + self.hardbreak_if_not_bol(); + } + if let Some(expr) = expr { + self.s.ibox(if !has_complex_successor(&expr.kind, true) { + self.ind + } else { + 0 + }); + self.print_word("return"); + if let Some(cmnt) = self.print_comments( + expr.span.lo(), + CommentConfig::skip_ws() + .mixed_no_break() + .mixed_prev_space() + .mixed_post_nbsp(), + ) { + if cmnt.is_trailing() && has_complex_successor(&expr.kind, true) { + self.s.offset(self.ind); + } + } else { + self.nbsp(); + } + self.print_expr(expr); + self.end(); + } else { + self.print_word("return"); + } + } + ast::StmtKind::Revert(path, args) => self.print_emit_or_revert("revert", path, args), + ast::StmtKind::Try(ast::StmtTry { expr, clauses }) => { + self.cbox(0); + if let Some((first, other)) = clauses.split_first() { + // Handle 'try' clause + let ast::TryCatchClause { args, block, span: try_span, .. } = first; + self.ibox(0); + self.print_word("try "); + self.print_comments(expr.span.lo(), CommentConfig::skip_ws()); + self.print_expr(expr); + self.print_comments( + args.first().map(|p| p.span.lo()).unwrap_or_else(|| expr.span.lo()), + CommentConfig::skip_ws(), + ); + if !self.is_beginning_of_line() { + self.nbsp(); + } + if !args.is_empty() { + self.print_word("returns "); + self.print_parameter_list( + args, + *try_span, + ListFormat::Compact { cmnts_break: false, with_space: false }, + ); + self.nbsp(); + } + self.print_block(block, *try_span); + + let mut skip_ind = false; + if self + .print_trailing_comment(try_span.hi(), other.first().map(|c| c.span.lo())) + { + // if a trailing comment is printed at the very end, we have to manually + // adjust the offset to avoid having a double break. + self.break_offset_if_not_bol(0, self.ind, false); + skip_ind = true; + }; + self.end(); + + let mut prev_block_multiline = self.is_multiline_block(block, false); + + // Handle 'catch' clauses + for (pos, ast::TryCatchClause { name, args, block, span: catch_span }) in + other.iter().delimited() + { + let current_block_multiline = self.is_multiline_block(block, false); + if !pos.is_first || !skip_ind { + if prev_block_multiline && (current_block_multiline || pos.is_last) { + self.nbsp(); + } else { + self.space(); + if !current_block_multiline { + self.s.offset(self.ind); + } + } + } + self.s.ibox(self.ind); + self.print_comments( + catch_span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(), + ); + self.print_word("catch "); + if !args.is_empty() { + self.print_comments( + args[0].span.lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_post_nbsp(), + ); + if let Some(name) = name { + self.print_ident(name); + } + self.print_parameter_list(args, *catch_span, ListFormat::Inline); + self.nbsp(); + } + self.print_word("{"); + self.end(); + self.print_block_without_braces(block, catch_span.hi(), Some(self.ind)); + self.print_word("}"); + + prev_block_multiline = current_block_multiline; + } + } + self.end(); + } + ast::StmtKind::UncheckedBlock(block) => { + self.word("unchecked "); + self.print_block(block, stmt.span); + } + ast::StmtKind::While(cond, stmt) => { + // Check if blocks should be inlined and update cache if necessary + let inline = self.is_single_line_block(cond, stmt, None); + if !inline.is_cached && self.single_line_stmt.is_none() { + self.single_line_stmt = Some(inline.outcome); + } + + // Print while cond and its statement + self.print_if_cond("while", cond, stmt.span.lo()); + self.nbsp(); + self.print_stmt_as_block(stmt, stmt.span.hi(), inline.outcome); + + // Clear cache if necessary + if !inline.is_cached && self.single_line_stmt.is_some() { + self.single_line_stmt = None; + } + } + ast::StmtKind::Placeholder => self.word("_"), + } + if stmt_needs_semi(kind) { + self.neverbreak(); // semicolon shouldn't account for linebreaks + self.word(";"); + self.cursor.advance_to(span.hi(), true); + } + // print comments without breaks, as those are handled by the caller. + self.print_comments( + stmt.span.hi(), + CommentConfig::default().trailing_no_break().mixed_no_break().mixed_prev_space(), + ); + self.print_trailing_comment_no_break(stmt.span.hi(), None); + } + + fn print_if_no_else( + &mut self, + cond: &'ast ast::Expr<'ast>, + then: &'ast ast::Stmt<'ast>, + inline: bool, + ) { + // NOTE(rusowsky): unless we add bracket spans to solar, + // using `then.span.lo()` consumes "cmnt12" of the IfStatement test inside the preceding + // clause: `self.print_if_cond("if", cond, cond.span.hi());` + if !self.handle_span(Span::new(cond.span.lo(), then.span.lo()), true) { + self.print_if_cond("if", cond, then.span.lo()); + self.print_sep(Separator::Space); + } + self.end(); + self.print_stmt_as_block(then, then.span.hi(), inline); + self.cursor.advance_to(then.span.hi(), true); + } + + fn print_if_cond(&mut self, kw: &'static str, cond: &'ast ast::Expr<'ast>, pos_hi: BytePos) { + self.print_word(kw); + Separator::Nbsp.print(&mut self.s, &mut self.cursor); + self.print_tuple( + std::slice::from_ref(cond), + cond.span.lo(), + pos_hi, + Self::print_expr, + get_span!(), + ListFormat::Compact { cmnts_break: true, with_space: false }, + is_binary_expr(&cond.kind), + ); + } + + fn print_emit_or_revert( + &mut self, + kw: &'static str, + path: &'ast ast::PathSlice, + args: &'ast ast::CallArgs<'ast>, + ) { + self.word(kw); + if self + .print_comments( + path.span().lo(), + CommentConfig::skip_ws().mixed_no_break().mixed_prev_space().mixed_post_nbsp(), + ) + .is_none() + { + self.nbsp(); + }; + self.print_path(path, false); + self.print_call_args(args); + } + + fn print_block(&mut self, block: &'ast [ast::Stmt<'ast>], span: Span) { + self.print_block_inner( + block, + BlockFormat::Regular, + Self::print_stmt, + |b| b.span, + span.hi(), + ); + } + + fn print_block_without_braces( + &mut self, + block: &'ast [ast::Stmt<'ast>], + pos_hi: BytePos, + offset: Option, + ) { + self.print_block_inner( + block, + BlockFormat::NoBraces(offset), + Self::print_stmt, + |b| b.span, + pos_hi, + ); + } + + // Body of a if/loop. + fn print_stmt_as_block(&mut self, stmt: &'ast ast::Stmt<'ast>, pos_hi: BytePos, inline: bool) { + if self.handle_span(stmt.span, false) { + return; + } + + let stmts = if let ast::StmtKind::Block(stmts) = &stmt.kind { + stmts + } else { + std::slice::from_ref(stmt) + }; + + if inline && !stmts.is_empty() { + self.neverbreak(); + self.print_block_without_braces(stmts, pos_hi, None); + } else { + self.print_word("{"); + self.print_block_without_braces(stmts, pos_hi, Some(self.ind)); + self.print_word("}"); + } + } + + fn print_yul_block( + &mut self, + block: &'ast [yul::Stmt<'ast>], + span: Span, + skip_opening_brace: bool, + ) { + if self.handle_span(span, false) { + return; + } + + if !skip_opening_brace { + self.print_word("{"); + } + + let mut i = if block.is_empty() { 0 } else { block.len() - 1 }; + self.print_block_inner( + block, + BlockFormat::NoBraces(Some(self.ind)), + |s, stmt| { + s.print_yul_stmt(stmt); + s.print_comments(stmt.span.hi(), CommentConfig::default()); + s.print_trailing_comment(stmt.span.hi(), None); + if i != 0 { + s.hardbreak_if_not_bol(); + i -= 1; + } + }, + |b| b.span, + span.hi(), + ); + self.print_word("}"); + } + + fn print_block_inner( + &mut self, + block: &'ast [T], + block_format: BlockFormat, + mut print: impl FnMut(&mut Self, &'ast T), + mut get_block_span: impl FnMut(&'ast T) -> Span, + pos_hi: BytePos, + ) { + // Attempt to print in a single line + if block_format.attempt_single_line() && block.len() == 1 { + self.s.cbox(self.ind); + if matches!(block_format, BlockFormat::Compact(true)) { + self.scan_break(BreakToken { pre_break: Some("{"), ..Default::default() }); + } else { + self.word("{"); + self.space(); + } + print(self, &block[0]); + self.print_comments(get_block_span(&block[0]).hi(), CommentConfig::default()); + if matches!(block_format, BlockFormat::Compact(true)) { + self.s.scan_break(BreakToken { post_break: Some("}"), ..Default::default() }); + self.s.offset(-self.ind); + } else { + self.space_if_not_bol(); + self.s.offset(-self.ind); + self.word("}"); + } + self.end(); + return; + } + + // Empty blocks with comments require special attention + if block.is_empty() { + // Trailing comments are printed after the block + let cmnt = self.peek_comment_before(pos_hi); + if cmnt.is_none_or(|cmnt| cmnt.style.is_trailing()) { + if self.config.bracket_spacing { + if block_format.with_braces() { + self.word("{ }"); + } else { + self.nbsp(); + } + } else if block_format.with_braces() { + self.word("{}"); + } + self.print_comments(pos_hi, CommentConfig::skip_ws()); + } + // Other comments are printed inside the block + else { + if block_format.with_braces() { + self.word("{"); + } + let offset = + if let BlockFormat::NoBraces(Some(off)) = block_format { off } else { 0 }; + self.print_comments( + pos_hi, + self.cmnt_config() + .offset(offset) + .mixed_no_break() + .mixed_prev_space() + .mixed_post_nbsp(), + ); + self.print_comments( + pos_hi, + CommentConfig::default().mixed_no_break().mixed_prev_space().mixed_post_nbsp(), + ); + + if block_format.with_braces() { + self.word("}"); + } + } + return; + } + + let first_stmt = get_block_span(&block[0]); + let block_lo = first_stmt.lo(); + let is_block_lo_disabled = + self.inline_config.is_disabled(Span::new(block_lo, block_lo + BytePos(1))); + match block_format { + BlockFormat::NoBraces(None) => { + if !self.handle_span(self.cursor.span(block_lo), false) { + self.print_comments(block_lo, CommentConfig::default()); + } + self.s.cbox(0); + } + BlockFormat::NoBraces(Some(offset)) => { + let prev_cmnt = + self.peek_comment_before(block_lo).map(|cmnt| (cmnt.span, cmnt.style)); + if is_block_lo_disabled { + // We don't use `print_sep()` because we want to introduce the breakpoint + if prev_cmnt.is_none() && self.cursor.enabled { + Separator::Space.print(&mut self.s, &mut self.cursor); + self.s.offset(offset); + self.cursor.advance_to(block_lo, true); + } else if prev_cmnt.is_some_and(|(_, style)| style.is_isolated()) { + Separator::Space.print(&mut self.s, &mut self.cursor); + self.s.offset(offset); + } + } else if !self.handle_span(self.cursor.span(block_lo), false) { + if let Some((span, style)) = prev_cmnt { + if !self.inline_config.is_disabled(span) || style.is_isolated() { + self.cursor.advance_to(span.lo(), true); + self.break_offset(SIZE_INFINITY as usize, offset); + } + if let Some(cmnt) = + self.print_comments(block_lo, CommentConfig::default().offset(offset)) + && !cmnt.is_mixed() + && !cmnt.is_blank() + { + self.s.offset(offset); + } + } else { + self.zerobreak(); + self.s.offset(offset); + } + } + self.s.cbox(self.ind); + } + _ => { + self.print_word("{"); + self.s.cbox(self.ind); + if !self.handle_span(self.cursor.span(block_lo), false) + && self + .print_comments(block_lo, CommentConfig::default()) + .is_none_or(|cmnt| cmnt.is_mixed()) + { + self.hardbreak_if_nonempty(); + } + } + } + + for (i, stmt) in block.iter().enumerate() { + let is_last = i == block.len() - 1; + print(self, stmt); + + let is_disabled = self.inline_config.is_disabled(get_block_span(stmt)); + let (next_enabled, next_lo) = if !is_last { + let next_span = get_block_span(&block[i + 1]); + let next_lo = if self.peek_comment_before(next_span.lo()).is_none() { + Some(next_span.lo()) + } else { + None + }; + + (!self.inline_config.is_disabled(next_span), next_lo) + } else { + (false, None) + }; + + // when this stmt and the next one are enabled, break normally (except if last stmt) + if !is_disabled + && next_enabled + && (!is_last + || self.peek_comment_before(pos_hi).is_some_and(|cmnt| cmnt.style.is_mixed())) + { + self.hardbreak_if_not_bol(); + continue; + } + // when this stmt is disabled and the next one is enabled, break if there is no + // enabled preceding comment. Otherwise the breakpoint is handled by the comment. + if is_disabled + && next_enabled + && let Some(next_lo) = next_lo + && self + .peek_comment_before(next_lo) + .is_none_or(|cmnt| self.inline_config.is_disabled(cmnt.span)) + { + self.hardbreak_if_not_bol() + } + } + self.print_comments( + pos_hi, + CommentConfig::skip_trailing_ws().mixed_no_break().mixed_prev_space(), + ); + if !block_format.breaks() { + if !self.last_token_is_break() { + self.hardbreak(); + } + self.s.offset(-self.ind); + } + self.end(); + if block_format.with_braces() { + self.print_word("}"); + } + } + + /// Determines if an `if/else` block should be inlined. + /// Also returns if the value was cached, so that it can be cleaned afterwards. + /// + /// # Returns + /// + /// A tuple `(should_inline, was_cached)`. The second boolean is `true` if the + /// decision was retrieved from the cache or is a final decision based on config, + /// preventing the caller from clearing a cache value that was never set. + fn is_single_line_block( + &mut self, + cond: &'ast ast::Expr<'ast>, + then: &'ast ast::Stmt<'ast>, + els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>, + ) -> Decision { + // If a decision is already cached from a parent, use it directly. + if let Some(cached_decision) = self.single_line_stmt { + return Decision { outcome: cached_decision, is_cached: true }; + } + + // Empty statements are always printed as blocks. + if std::slice::from_ref(then).is_empty() { + return Decision { outcome: false, is_cached: false }; + } + + // If possible, take an early decision based on the block style configuration. + match self.config.single_line_statement_blocks { + config::SingleLineBlockStyle::Preserve => { + if self.is_stmt_in_new_line(cond, then) || self.is_multiline_block_stmt(then, true) + { + return Decision { outcome: false, is_cached: false }; + } + } + config::SingleLineBlockStyle::Single => { + if self.is_multiline_block_stmt(then, true) { + return Decision { outcome: false, is_cached: false }; + } + } + config::SingleLineBlockStyle::Multi => { + return Decision { outcome: false, is_cached: false }; + } + }; + + // If no decision was made, estimate the length to be formatted. + // NOTE: conservative check -> worst-case scenario is formatting as multi-line block. + if !self.can_stmts_fit_in_one_line(cond, then, els_opt) { + return Decision { outcome: false, is_cached: false }; + } + + // If the parent would fit, check all of its children. + if let Some(stmt) = els_opt { + if let ast::StmtKind::If(child_cond, child_then, child_els_opt) = &stmt.kind { + return self.is_single_line_block(child_cond, child_then, child_els_opt.as_ref()); + } else if self.is_multiline_block_stmt(stmt, true) { + return Decision { outcome: false, is_cached: false }; + } + } + + // If all children can also fit, allow single-line block. + Decision { outcome: true, is_cached: false } + } + + fn is_inline_stmt(&self, stmt: &'ast ast::Stmt<'ast>, cond_len: usize) -> bool { + if let ast::StmtKind::If(cond, then, els_opt) = &stmt.kind { + let if_span = Span::new(cond.span.lo(), then.span.hi()); + if self.sm.is_multiline(if_span) + && matches!( + self.config.single_line_statement_blocks, + config::SingleLineBlockStyle::Preserve + ) + { + return false; + } + if cond_len + self.estimate_size(if_span) >= self.space_left() { + return false; + } + if let Some(els) = els_opt + && !self.is_inline_stmt(els, 6) + { + return false; + } + } else { + if matches!( + self.config.single_line_statement_blocks, + config::SingleLineBlockStyle::Preserve + ) && self.sm.is_multiline(stmt.span) + { + return false; + } + if cond_len + self.estimate_size(stmt.span) >= self.space_left() { + return false; + } + } + true + } + + /// Checks if a statement was explicitly written in a new line. + fn is_stmt_in_new_line( + &self, + cond: &'ast ast::Expr<'ast>, + then: &'ast ast::Stmt<'ast>, + ) -> bool { + let span_between = cond.span.between(then.span); + if let Ok(snip) = self.sm.span_to_snippet(span_between) { + // Check for newlines after the closing parenthesis of the `if (...)`. + if let Some((_, after_paren)) = snip.split_once(')') { + return after_paren.lines().count() > 1; + } + } + false + } + + /// Checks if a block statement `{ ... }` contains more than one line of actual code. + fn is_multiline_block_stmt( + &self, + stmt: &'ast ast::Stmt<'ast>, + empty_as_multiline: bool, + ) -> bool { + if let ast::StmtKind::Block(block) = &stmt.kind { + return self.is_multiline_block(block, empty_as_multiline); + } + false + } + + /// Checks if a block statement `{ ... }` contains more than one line of actual code. + fn is_multiline_block(&self, block: &'ast ast::Block<'ast>, empty_as_multiline: bool) -> bool { + if block.stmts.is_empty() { + return empty_as_multiline; + } + if self.sm.is_multiline(block.span) + && let Ok(snip) = self.sm.span_to_snippet(block.span) + { + let code_lines = snip.lines().filter(|line| { + let trimmed = line.trim(); + // Ignore empty lines and lines with only '{' or '}' + if empty_as_multiline { + !trimmed.is_empty() && trimmed != "{" && trimmed != "}" + } else { + !trimmed.is_empty() + } + }); + return code_lines.count() > 1; + } + false + } + + /// Performs a size estimation to see if the if/else can fit on one line. + fn can_stmts_fit_in_one_line( + &mut self, + cond: &'ast ast::Expr<'ast>, + then: &'ast ast::Stmt<'ast>, + els_opt: Option<&'ast &'ast mut ast::Stmt<'ast>>, + ) -> bool { + let cond_len = self.estimate_size(cond.span); + + // If the condition fits in one line, 6 chars: 'if (' + {cond} + ') ' + {then} + // Otherwise chars: ') ' + {then} + let then_margin = if 6 + cond_len < self.space_left() { 6 + cond_len } else { 2 }; + + if !self.is_inline_stmt(then, then_margin) { + return false; + } + + // Always 6 chars for the else: 'else ' + els_opt.is_none_or(|els| self.is_inline_stmt(els, 6)) + } + + fn can_header_fit_in_one_line(&mut self, header: &ast::FunctionHeader<'_>) -> bool { + // ' ' + visibility + let visibility = header.visibility.map_or(0, |v| self.estimate_size(v.span) + 1); + // ' ' + state mutability + let mutability = header.state_mutability.map_or(0, |sm| self.estimate_size(sm.span) + 1); + // ' ' + modifier + (' ' + modifier) + let modifiers = + header.modifiers.iter().fold(0, |len, m| len + self.estimate_size(m.span())) + 1; + // ' ' + override + let override_ = header.override_.as_ref().map_or(0, |o| self.estimate_size(o.span) + 1); + // ' returns(' + var + (', ' + var) + ')' + let returns = header.returns.as_ref().map_or(0, |ret| { + ret.vars + .iter() + .fold(0, |len, p| if len != 0 { len + 2 } else { 8 } + self.estimate_size(p.span)) + }); + + self.estimate_header_params_size(header) + + visibility + + mutability + + modifiers + + override_ + + returns + <= self.space_left() + } + + fn estimate_header_params_size(&mut self, header: &ast::FunctionHeader<'_>) -> usize { + // ' ' + name + let name = header.name.map_or(0, |name| self.estimate_size(name.span) + 1); + // '(' + param + (', ' + param) + ')' + let params = header + .parameters + .vars + .iter() + .fold(0, |len, p| if len != 0 { len + 2 } else { 2 } + self.estimate_size(p.span)); + + // `function` takes 8 chars + 8 + name + params + } + + fn can_header_params_fit_in_one_line(&mut self, header: &ast::FunctionHeader<'_>) -> bool { + self.estimate_header_params_size(header) <= self.space_left() + } +} + +/// Yul. +impl<'ast> State<'_, 'ast> { + fn print_yul_stmt(&mut self, stmt: &'ast yul::Stmt<'ast>) { + let yul::Stmt { ref docs, span, ref kind } = *stmt; + self.print_docs(docs); + if self.handle_span(span, false) { + return; + } + + match kind { + yul::StmtKind::Block(stmts) => self.print_yul_block(stmts, span, false), + yul::StmtKind::AssignSingle(path, expr) => { + self.print_path(path, false); + self.word(" := "); + self.neverbreak(); + self.print_yul_expr(expr); + } + yul::StmtKind::AssignMulti(paths, expr_call) => { + self.commasep( + paths, + stmt.span.lo(), + stmt.span.hi(), + |this, path| this.print_path(path, false), + get_span!(()), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + false, + ); + self.word(" :="); + self.space(); + self.s.offset(self.ind); + self.ibox(0); + self.print_yul_expr_call(expr_call); + self.end(); + } + yul::StmtKind::Expr(expr_call) => self.print_yul_expr_call(expr_call), + yul::StmtKind::If(expr, stmts) => { + self.word("if "); + self.print_yul_expr(expr); + self.nbsp(); + self.print_yul_block(stmts, span, false); + } + yul::StmtKind::For { init, cond, step, body } => { + self.ibox(0); + + self.word("for "); + self.print_yul_block(init, span, false); + + self.space(); + self.print_yul_expr(cond); + + self.space(); + self.print_yul_block(step, span, false); + + self.space(); + self.print_yul_block(body, span, false); + + self.end(); + } + yul::StmtKind::Switch(yul::StmtSwitch { selector, branches, default_case }) => { + self.word("switch "); + self.print_yul_expr(selector); + + self.print_trailing_comment(selector.span.hi(), None); + + for yul::StmtSwitchCase { constant, body } in branches.iter() { + self.hardbreak_if_not_bol(); + self.word("case "); + self.print_lit(constant); + self.nbsp(); + self.print_yul_block(body, span, false); + + self.print_trailing_comment(selector.span.hi(), None); + } + + if let Some(default_case) = default_case { + self.hardbreak_if_not_bol(); + self.word("default "); + self.print_yul_block(default_case, span, false); + } + } + yul::StmtKind::Leave => self.word("leave"), + yul::StmtKind::Break => self.word("break"), + yul::StmtKind::Continue => self.word("continue"), + yul::StmtKind::FunctionDef(yul::Function { name, parameters, returns, body }) => { + self.cbox(0); + self.s.ibox(0); + self.word("function "); + self.print_ident(name); + self.print_tuple( + parameters, + span.lo(), + span.hi(), + Self::print_ident, + get_span!(), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + false, + ); + self.nbsp(); + let has_returns = !returns.is_empty(); + let skip_opening_brace = has_returns; + if has_returns { + self.commasep( + returns, + stmt.span.lo(), + stmt.span.hi(), + Self::print_ident, + get_span!(), + ListFormat::Yul { sym_prev: Some("->"), sym_post: Some("{") }, + false, + ); + } + self.end(); + self.print_yul_block(body, span, skip_opening_brace); + self.end(); + } + yul::StmtKind::VarDecl(idents, expr) => { + self.s.ibox(self.ind); + self.word("let "); + self.commasep( + idents, + stmt.span.lo(), + stmt.span.hi(), + Self::print_ident, + get_span!(), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + false, + ); + if let Some(expr) = expr { + self.word(" :="); + self.space(); + self.print_yul_expr(expr); + } + self.end(); + } + } + } + + fn print_yul_expr(&mut self, expr: &'ast yul::Expr<'ast>) { + let yul::Expr { span, ref kind } = *expr; + if self.handle_span(span, false) { + return; + } + + match kind { + yul::ExprKind::Path(path) => self.print_path(path, false), + yul::ExprKind::Call(call) => self.print_yul_expr_call(call), + yul::ExprKind::Lit(lit) => self.print_lit(lit), + } + } + + fn print_yul_expr_call(&mut self, expr: &'ast yul::ExprCall<'ast>) { + let yul::ExprCall { name, arguments } = expr; + self.print_ident(name); + self.print_tuple( + arguments, + Span::DUMMY.lo(), + Span::DUMMY.hi(), + Self::print_yul_expr, + get_span!(), + ListFormat::Consistent { cmnts_break: false, with_space: false }, + true, + ); + } + + fn print_fn_attribute( + &mut self, + span: Span, + map: &mut HashMap, Vec)>, + print_fn: &mut dyn FnMut(&mut Self), + ) { + match map.remove(&span.lo()) { + Some((pre_comments, post_comments)) => { + for cmnt in pre_comments { + let Some(cmnt) = self.handle_comment(cmnt) else { + continue; + }; + self.print_comment(cmnt, CommentConfig::default()); + } + let mut enabled = false; + if !self.handle_span(span, false) { + if !self.is_bol_or_only_ind() { + self.space(); + } + self.ibox(0); + print_fn(self); + self.cursor.advance_to(span.hi(), true); + enabled = true; + } + for cmnt in post_comments { + let Some(cmnt) = self.handle_comment(cmnt) else { + continue; + }; + self.print_comment(cmnt, CommentConfig::default().mixed_prev_space()); + } + if enabled { + self.end(); + } + } + // Fallback for attributes not in the map (should never happen) + None => { + if !self.is_bol_or_only_ind() { + self.space(); + } + print_fn(self); + self.cursor.advance_to(span.hi(), true); + } + } + } + + fn estimate_size(&self, span: Span) -> usize { + if let Ok(snip) = self.sm.span_to_snippet(span) { + let mut size = 0; + for line in snip.lines() { + size += line.trim().len(); + } + return size; + } + + span.to_range().len() + } + + fn same_source_line(&self, a: BytePos, b: BytePos) -> bool { + self.sm.lookup_char_pos(a).line == self.sm.lookup_char_pos(b).line + } +} + +// -- HELPERS ----------------------------------------------------------------- +// TODO(rusowsky): move to its own file + +/// Formatting style for comma-separated lists +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ListFormat { + /// Always breaks for multiple elements. If only one element, will print it isolated depending + /// on the `break_single` flag. + AlwaysBreak { break_single: bool, with_space: bool }, + /// Breaks all elements if any break. + Consistent { cmnts_break: bool, with_space: bool }, + /// Attempts to fit all elements in one line, before breaking consistently. + /// The boolean indicates whether mixed comments should force a break. + Compact { cmnts_break: bool, with_space: bool }, + /// If the list contains just one element, it will print unboxed (will not break). + /// Otherwise, will break consistently. + Inline, + /// Since yul return values aren't wrapped in parenthesis, we need to manually handle the + /// adjacent symbols to achieve the desired format. + /// + /// Behaves like `Self::Consistent`. + Yul { sym_prev: Option<&'static str>, sym_post: Option<&'static str> }, +} + +impl ListFormat { + pub(crate) fn breaks_comments(&self) -> bool { + match self { + Self::AlwaysBreak { .. } | Self::Yul { .. } => true, + Self::Consistent { cmnts_break, .. } => *cmnts_break, + Self::Compact { cmnts_break, .. } => *cmnts_break, + Self::Inline => false, + } + } + + pub(crate) fn with_space(&self) -> bool { + match self { + Self::AlwaysBreak { with_space, .. } => *with_space, + Self::Consistent { with_space, .. } => *with_space, + Self::Compact { with_space, .. } => *with_space, + Self::Inline | Self::Yul { .. } => false, + } + } + + pub(crate) fn prev_symbol(&self) -> Option<&'static str> { + if let Self::Yul { sym_prev, .. } = self { *sym_prev } else { None } + } + + pub(crate) fn post_symbol(&self) -> Option<&'static str> { + if let Self::Yul { sym_post, .. } = self { *sym_post } else { None } + } + + fn add_break(&self, soft: bool, elems: usize, p: &mut pp::Printer) { + if let Self::AlwaysBreak { break_single, .. } = self + && (elems > 1 || (*break_single && elems == 1)) + { + p.hardbreak(); + } else if soft && !self.with_space() { + p.zerobreak(); + } else { + p.space(); + } + } + + pub(crate) fn is_compact(&self) -> bool { + matches!(self, Self::Compact { .. }) + } +} + +/// Formatting style for code blocks +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BlockFormat { + Regular, + /// Attempts to fit all elements in one line, before breaking consistently. Flags whether to + /// use braces or not. + Compact(bool), + /// Doesn't print braces. Flags the offset that should be applied before opening the block box. + /// Useful when the caller needs to manually handle the braces. + NoBraces(Option), +} + +impl BlockFormat { + pub(crate) fn with_braces(&self) -> bool { + !matches!(self, Self::NoBraces(_)) + } + pub(crate) fn breaks(&self) -> bool { + matches!(self, Self::NoBraces(None)) + } + + pub(crate) fn attempt_single_line(&self) -> bool { + matches!(self, Self::Compact(_)) + } +} + +#[derive(Debug, Clone)] +pub(crate) enum AttributeKind<'ast> { + Visibility(ast::Visibility), + StateMutability(ast::StateMutability), + Virtual, + Override(&'ast ast::Override<'ast>), + Modifier(&'ast ast::Modifier<'ast>), +} + +impl<'ast> AttributeKind<'ast> { + fn is_visibility(&self) -> bool { + matches!(self, Self::Visibility(_)) + } + + fn is_state_mutability(&self) -> bool { + matches!(self, Self::StateMutability(_)) + } + + fn is_non_payable(&self) -> bool { + matches!(self, Self::StateMutability(ast::StateMutability::NonPayable)) + } + + fn is_virtual(&self) -> bool { + matches!(self, Self::Virtual) + } + + fn is_override(&self) -> bool { + matches!(self, Self::Override(_)) + } + + fn is_modifier(&self) -> bool { + matches!(self, Self::Modifier(_)) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct AttributeInfo<'ast> { + pub(crate) kind: AttributeKind<'ast>, + pub(crate) span: Span, +} + +/// Helper struct to map attributes to their associated comments in function headers. +pub(crate) struct AttributeCommentMapper<'ast> { + limit_pos: BytePos, + comments: Vec, + attributes: Vec>, + empty_returns: bool, +} + +impl<'ast> AttributeCommentMapper<'ast> { + pub(crate) fn new(returns: Option<&'ast ast::ParameterList<'ast>>, body_pos: BytePos) -> Self { + Self { + comments: Vec::new(), + attributes: Vec::new(), + empty_returns: returns.is_none(), + limit_pos: returns.as_ref().map_or(body_pos, |ret| ret.span.lo()), + } + } + + #[allow(clippy::type_complexity)] + pub(crate) fn build( + mut self, + state: &mut State<'_, 'ast>, + header: &'ast ast::FunctionHeader<'ast>, + ) -> (HashMap, Vec)>, Vec>, BytePos) { + let first_attr = self.collect_attributes(header); + self.cache_comments(state); + (self.map(), self.attributes, first_attr) + } + + fn map(&mut self) -> HashMap, Vec)> { + let mut map = HashMap::new(); + for a in 0..self.attributes.len() { + let is_last = a == self.attributes.len() - 1; + let mut before = Vec::new(); + let mut after = Vec::new(); + + let before_limit = self.attributes[a].span.lo(); + let after_limit = + if !is_last { self.attributes[a + 1].span.lo() } else { self.limit_pos }; + + let mut c = 0; + while c < self.comments.len() { + if self.comments[c].pos() <= before_limit { + before.push(self.comments.remove(c)); + } else if (after.is_empty() || is_last) && self.comments[c].pos() <= after_limit { + after.push(self.comments.remove(c)); + } else { + c += 1; + } + } + map.insert(before_limit, (before, after)); + } + map + } + + fn collect_attributes(&mut self, header: &'ast ast::FunctionHeader<'ast>) -> BytePos { + let mut first_pos = BytePos(u32::MAX); + if let Some(v) = header.visibility { + if v.span.lo() < first_pos { + first_pos = v.span.lo() + } + self.attributes + .push(AttributeInfo { kind: AttributeKind::Visibility(*v), span: v.span }); + } + if let Some(sm) = header.state_mutability { + if sm.span.lo() < first_pos { + first_pos = sm.span.lo() + } + self.attributes + .push(AttributeInfo { kind: AttributeKind::StateMutability(*sm), span: sm.span }); + } + if let Some(span) = header.virtual_ { + if span.lo() < first_pos { + first_pos = span.lo() + } + self.attributes.push(AttributeInfo { kind: AttributeKind::Virtual, span }); + } + if let Some(ref o) = header.override_ { + if o.span.lo() < first_pos { + first_pos = o.span.lo() + } + self.attributes.push(AttributeInfo { kind: AttributeKind::Override(o), span: o.span }); + } + for m in header.modifiers.iter() { + if m.span().lo() < first_pos { + first_pos = m.span().lo() + } + self.attributes + .push(AttributeInfo { kind: AttributeKind::Modifier(m), span: m.span() }); + } + self.attributes.sort_by_key(|attr| attr.span.lo()); + first_pos + } + + fn cache_comments(&mut self, state: &mut State<'_, 'ast>) { + let mut pending = None; + for cmnt in state.comments.iter() { + if cmnt.pos() >= self.limit_pos { + break; + } + match pending { + Some(ref p) => pending = Some(p + 1), + None => pending = Some(0), + } + } + while let Some(p) = pending { + if p == 0 { + pending = None; + } else { + pending = Some(p - 1); + } + let cmnt = state.next_comment().unwrap(); + if cmnt.style == CommentStyle::BlankLine { + continue; + } + self.comments.push(cmnt); + } + } +} + +fn stmt_needs_semi(stmt: &ast::StmtKind<'_>) -> bool { + match stmt { + ast::StmtKind::Assembly { .. } + | ast::StmtKind::Block { .. } + | ast::StmtKind::For { .. } + | ast::StmtKind::If { .. } + | ast::StmtKind::Try { .. } + | ast::StmtKind::UncheckedBlock { .. } + | ast::StmtKind::While { .. } => false, + + ast::StmtKind::DeclSingle { .. } + | ast::StmtKind::DeclMulti { .. } + | ast::StmtKind::Break { .. } + | ast::StmtKind::Continue { .. } + | ast::StmtKind::DoWhile { .. } + | ast::StmtKind::Emit { .. } + | ast::StmtKind::Expr { .. } + | ast::StmtKind::Return { .. } + | ast::StmtKind::Revert { .. } + | ast::StmtKind::Placeholder { .. } => true, + } +} + +/// Returns `true` if the item needs an isolated line break. +fn item_needs_iso(item: &ast::ItemKind<'_>) -> bool { + match item { + ast::ItemKind::Pragma(..) + | ast::ItemKind::Import(..) + | ast::ItemKind::Using(..) + | ast::ItemKind::Variable(..) + | ast::ItemKind::Udvt(..) + | ast::ItemKind::Enum(..) + | ast::ItemKind::Error(..) + | ast::ItemKind::Event(..) => false, + + ast::ItemKind::Contract(..) => true, + + // TODO: is this logic correct? that's what i figured out based on unit tests + ast::ItemKind::Struct(strukt) => !strukt.fields.is_empty(), + ast::ItemKind::Function(func) => { + func.body.as_ref().is_some_and(|b| !b.is_empty()) + && !matches!(func.kind, ast::FunctionKind::Modifier) + } + } +} + +#[derive(Clone, Copy)] +pub enum Skip { + All, + Leading, + Trailing, +} + +#[derive(Debug)] +pub struct Decision { + outcome: bool, + is_cached: bool, +} + +fn is_binary_expr(expr_kind: &ast::ExprKind<'_>) -> bool { + matches!(expr_kind, ast::ExprKind::Binary(..)) +} + +fn has_complex_successor(expr_kind: &ast::ExprKind<'_>, left: bool) -> bool { + match expr_kind { + ast::ExprKind::Binary(lhs, _, rhs) => { + if left { + has_complex_successor(&lhs.kind, left) + } else { + has_complex_successor(&rhs.kind, left) + } + } + ast::ExprKind::Unary(_, expr) => has_complex_successor(&expr.kind, left), + ast::ExprKind::Lit(..) | ast::ExprKind::Ident(_) => false, + _ => true, + } +} + +#[derive(Default, Clone, Copy)] +struct CommentConfig { + // Config: all + skip_blanks: Option, + current_ind: isize, + offset: isize, + // Config: trailing comments + trailing_no_break: bool, + // Config: mixed comments + mixed_prev_space: bool, + mixed_post_nbsp: bool, + mixed_no_break: bool, +} + +impl CommentConfig { + fn skip_ws() -> Self { + Self { skip_blanks: Some(Skip::All), ..Default::default() } + } + + fn skip_leading_ws() -> Self { + Self { skip_blanks: Some(Skip::Leading), ..Default::default() } + } + + fn skip_trailing_ws() -> Self { + Self { skip_blanks: Some(Skip::Trailing), ..Default::default() } + } + + fn offset(mut self, off: isize) -> Self { + self.offset = off; + self + } + + fn trailing_no_break(mut self) -> Self { + self.trailing_no_break = true; + self + } + + fn mixed_no_break(mut self) -> Self { + self.mixed_no_break = true; + self + } + + fn mixed_prev_space(mut self) -> Self { + self.mixed_prev_space = true; + self + } + + fn mixed_post_nbsp(mut self) -> Self { + self.mixed_post_nbsp = true; + self + } + + fn hardbreak_if_not_bol(&self, is_bol: bool, p: &mut pp::Printer) { + if self.offset != 0 && !is_bol { + self.hardbreak(p); + } else { + p.hardbreak_if_not_bol(); + } + } + + fn hardbreak(&self, p: &mut pp::Printer) { + p.break_offset(SIZE_INFINITY as usize, self.offset); + } + + fn space(&self, p: &mut pp::Printer) { + p.break_offset(1, self.offset); + } + + fn nbsp_or_space(&self, breaks: bool, p: &mut pp::Printer) { + if breaks { + self.space(p); + } else { + p.nbsp(); + } + } + + fn zerobreak(&self, p: &mut pp::Printer) { + p.break_offset(0, self.offset); + } +} + +enum Separator { + Nbsp, + Space, + Hardbreak, + SpaceOrNbsp(bool), +} + +impl Separator { + fn print(&self, p: &mut pp::Printer, cursor: &mut SourcePos) { + match self { + Self::Nbsp => p.nbsp(), + Self::Space => p.space(), + Self::Hardbreak => p.hardbreak(), + Self::SpaceOrNbsp(breaks) => p.space_or_nbsp(*breaks), + } + cursor.advance(1); + } +} + +fn snippet_with_tabs(s: String, tab_width: usize) -> String { + // process leading breaks + let trimmed = s.trim_start_matches('\n'); + let num_breaks = s.len() - trimmed.len(); + let mut formatted = std::iter::repeat_n('\n', num_breaks).collect::(); + + // process lines + for (pos, line) in trimmed.lines().delimited() { + line_with_tabs(&mut formatted, line, tab_width, None); + if !pos.is_last { + formatted.push('\n'); + } + } + + formatted +} diff --git a/crates/fmt-2/tests/formatter.rs b/crates/fmt-2/tests/formatter.rs new file mode 100644 index 0000000000000..aa56032d14ff7 --- /dev/null +++ b/crates/fmt-2/tests/formatter.rs @@ -0,0 +1,217 @@ +use forge_fmt_2::FormatterConfig; +use snapbox::{Data, assert_data_eq}; +use std::{ + fs, + path::{Path, PathBuf}, +}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; + +#[track_caller] +fn format(source: &str, path: &Path, config: FormatterConfig) -> String { + match forge_fmt_2::format_source(source, Some(path), config).into_result() { + Ok(formatted) => formatted, + Err(e) => panic!("failed to format {path:?}: {e}"), + } +} + +#[track_caller] +fn assert_eof(content: &str) { + assert!(content.ends_with('\n'), "missing trailing newline"); + assert!(!content.ends_with("\n\n"), "extra trailing newline"); +} + +fn enable_tracing() { + let subscriber = FmtSubscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .with_test_writer() + .finish(); + let _ = tracing::subscriber::set_global_default(subscriber); +} + +fn tests_dir() -> PathBuf { + // Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata") + Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().join("fmt/testdata") +} + +fn test_directory(base_name: &str) { + enable_tracing(); + let dir = tests_dir().join(base_name); + let original = fs::read_to_string(dir.join("original.sol")).unwrap(); + let mut handles = vec![]; + for res in dir.read_dir().unwrap() { + let entry = res.unwrap(); + let path = entry.path(); + + let filename = path.file_name().and_then(|name| name.to_str()).unwrap(); + if filename == "original.sol" { + continue; + } + assert!(path.is_file(), "expected file: {path:?}"); + assert!(filename.ends_with("fmt.sol"), "unknown file: {path:?}"); + + let expected = fs::read_to_string(&path).unwrap(); + + // The majority of the tests were written with the assumption that the default value for max + // line length is `80`. Preserve that to avoid rewriting test logic. + let default_config = FormatterConfig { line_length: 80, ..Default::default() }; + + let mut config = toml::Value::try_from(default_config).unwrap(); + let config_table = config.as_table_mut().unwrap(); + let mut comments_end = 0; + for (i, line) in expected.lines().enumerate() { + let line_num = i + 1; + let Some(entry) = line + .strip_prefix("//") + .and_then(|line| line.trim().strip_prefix("config:")) + .map(str::trim) + else { + break; + }; + + let values = match toml::from_str::(entry) { + Ok(toml::Value::Table(table)) => table, + r => panic!("invalid fmt config item in {filename} at {line_num}: {r:?}"), + }; + config_table.extend(values); + + comments_end += line.len() + 1; + } + let config = config + .try_into() + .unwrap_or_else(|err| panic!("invalid test config for {filename}: {err}")); + + let original = original.clone(); + let tname = format!("{base_name}/{filename}"); + let spawn = move || { + test_formatter(&path, config, &original, &expected, comments_end); + }; + handles.push(std::thread::Builder::new().name(tname).spawn(spawn).unwrap()); + } + let results = handles.into_iter().map(|h| h.join()).collect::>(); + for result in results { + result.unwrap(); + } +} + +fn test_formatter( + expected_path: &Path, + config: FormatterConfig, + source: &str, + expected_source: &str, + comments_end: usize, +) { + let path = &*expected_path.with_file_name("original.sol"); + let expected_data = || Data::read_from(expected_path, None).raw(); + + let mut source_formatted = format(source, path, config.clone()); + // Inject `expected`'s comments, if any, so we can use the expected file as a snapshot. + source_formatted.insert_str(0, &expected_source[..comments_end]); + assert_data_eq!(&source_formatted, expected_data()); + assert_eof(&source_formatted); + + let expected_formatted = + format(&std::fs::read_to_string(expected_path).unwrap(), expected_path, config); + assert_data_eq!(&expected_formatted, expected_data()); + assert_eof(expected_source); + assert_eof(&expected_formatted); +} + +fn test_all_dirs_are_declared(dirs: &[&str]) { + let mut undeclared = vec![]; + for actual_dir in tests_dir().read_dir().unwrap().filter_map(Result::ok) { + let path = actual_dir.path(); + assert!(path.is_dir(), "expected directory: {path:?}"); + let actual_dir_name = path.file_name().unwrap().to_str().unwrap(); + if !dirs.contains(&actual_dir_name) { + undeclared.push(actual_dir_name.to_string()); + } + } + if !undeclared.is_empty() { + panic!( + "the following test directories are not declared in the test suite macro call: {undeclared:#?}" + ); + } +} + +macro_rules! fmt_tests { + ($($(#[$attr:meta])* $dir:ident),+ $(,)?) => { + #[test] + fn all_dirs_are_declared() { + test_all_dirs_are_declared(&[$(stringify!($dir)),*]); + } + + $( + #[allow(non_snake_case)] + #[test] + $(#[$attr])* + fn $dir() { + test_directory(stringify!($dir)); + } + )+ + }; +} + +// TODO (config): +// * style = tab +// * all fn styles +// +// TODO (isolation): +// * when are functions isolated? +// * when are structs isolated? + +fmt_tests! { + #[ignore = "annotations are not valid Solidity"] + Annotation, + ArrayExpressions, // OK (data loc keyword won't have a span in solar) + BlockComments, // OK + BlockCommentsFunction, // OK + ConditionalOperatorExpression, //OK + ConstructorDefinition, // OK + ConstructorModifierStyle, // OK + ContractDefinition, // OK + DocComments, // OK + DoWhileStatement, // OK + EmitStatement, // OK + EnumDefinition, // OK + EnumVariants, // OK + ErrorDefinition, // OK + EventDefinition, // OK + ForStatement, // OK + FunctionCall, // OK + FunctionCallArgsStatement, // OK + FunctionDefinition, // OK? + FunctionDefinitionWithFunctionReturns, // OK + FunctionType, // OK + HexUnderscore, // OK + IfStatement, // Ok + IfStatement2, // OK + ImportDirective, // OK + InlineDisable, // OK + IntTypes, // OK + LiteralExpression, // OK + MappingType, // OK + ModifierDefinition, // OK + NamedFunctionCallExpression, // OK + NumberLiteralUnderscore, // OK + OperatorExpressions, // OK + PragmaDirective, // OK + Repros, // OK + ReturnStatement, // OK (inline block logic is inconsistent with 'if stmt' unit test) + RevertNamedArgsStatement, // OK (properly break long calls?) + RevertStatement, // OK + SimpleComments, // OK + SortedImports, // OK + StatementBlock, // OK + StructDefinition, // OK + ThisExpression, // OK + TrailingComma, // OK (solar error) + TryStatement, // OK + TypeDefinition, // OK + UnitExpression, // OK (subdenom word won't have a span in solar) + UsingDirective, // OK + VariableAssignment, // OK + VariableDefinition, // OK (solar forces constants to be initialized) + WhileStatement, // OK + Yul, // OK + YulStrings, // OK +} diff --git a/crates/fmt/testdata/ArrayExpressions/fmt.sol b/crates/fmt/testdata/ArrayExpressions/fmt.sol index adda7a30e098d..ccb73a3166881 100644 --- a/crates/fmt/testdata/ArrayExpressions/fmt.sol +++ b/crates/fmt/testdata/ArrayExpressions/fmt.sol @@ -6,7 +6,7 @@ contract ArrayExpressions { uint256 length = 10; uint256[] memory sample2 = new uint256[](length); - uint256[] /* comment1 */ memory /* comment2 */ sample3; // comment3 + uint256[] memory /* comment1 */ /* comment2 */ sample3; // comment3 /* ARRAY SLICE */ msg.data[4:]; @@ -63,7 +63,8 @@ contract ArrayExpressions { 2, /* comment9 */ 3 // comment10 ]; - uint256[1] memory literal3 = - [ /* comment11 */ someVeryVeryLongVariableName /* comment13 */ ]; + uint256[1] memory literal3 = [ /* comment11 */ + someVeryVeryLongVariableName /* comment13 */ + ]; } } diff --git a/crates/fmt/testdata/BlockComments/tab.fmt.sol b/crates/fmt/testdata/BlockComments/tab.fmt.sol index 3b12bee813749..527f372abd489 100644 --- a/crates/fmt/testdata/BlockComments/tab.fmt.sol +++ b/crates/fmt/testdata/BlockComments/tab.fmt.sol @@ -1,7 +1,7 @@ // config: style = "tab" contract CounterTest is Test { /** - * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. + * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. */ constructor(string memory name_, string memory symbol_) { _name = name_; @@ -9,7 +9,7 @@ contract CounterTest is Test { } /** - * @dev See {IERC721-balanceOf}. + * @dev See {IERC721-balanceOf}. */ function test_Increment() public { counter.increment(); @@ -17,7 +17,7 @@ contract CounterTest is Test { } /** - * @dev See {IERC165-supportsInterface}. + * @dev See {IERC165-supportsInterface}. */ function test_Increment() public { counter.increment(); diff --git a/crates/fmt/testdata/BlockCommentsFunction/tab.fmt.sol b/crates/fmt/testdata/BlockCommentsFunction/tab.fmt.sol index 04a0986cb73db..40eae6c9069ce 100644 --- a/crates/fmt/testdata/BlockCommentsFunction/tab.fmt.sol +++ b/crates/fmt/testdata/BlockCommentsFunction/tab.fmt.sol @@ -2,20 +2,20 @@ 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); - * } + * 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]))); - * } - * } + * 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/ConstructorModifierStyle/fmt.sol b/crates/fmt/testdata/ConstructorModifierStyle/fmt.sol index 88694860aded2..be61b28170c73 100644 --- a/crates/fmt/testdata/ConstructorModifierStyle/fmt.sol +++ b/crates/fmt/testdata/ConstructorModifierStyle/fmt.sol @@ -9,5 +9,7 @@ import {IAchievements} from "./interfaces/IAchievements.sol"; import {SoulBound1155} from "./abstracts/SoulBound1155.sol"; contract Achievements is IAchievements, SoulBound1155, Ownable { - constructor(address owner) Ownable() ERC1155() {} + constructor(address owner) my_modifier Ownable() ERC1155() {} + + function f() my_modifier MyModifier my_modifier MyModifier {} } diff --git a/crates/fmt/testdata/ConstructorModifierStyle/original.sol b/crates/fmt/testdata/ConstructorModifierStyle/original.sol index 88694860aded2..eb418ba126178 100644 --- a/crates/fmt/testdata/ConstructorModifierStyle/original.sol +++ b/crates/fmt/testdata/ConstructorModifierStyle/original.sol @@ -9,5 +9,7 @@ import {IAchievements} from "./interfaces/IAchievements.sol"; import {SoulBound1155} from "./abstracts/SoulBound1155.sol"; contract Achievements is IAchievements, SoulBound1155, Ownable { - constructor(address owner) Ownable() ERC1155() {} + constructor(address owner) my_modifier Ownable() ERC1155() {} + + function f() my_modifier MyModifier() my_modifier() MyModifier {} } diff --git a/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol b/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol index dca4e325d39c9..20894a47a3262 100644 --- a/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/bracket-spacing.fmt.sol @@ -13,7 +13,11 @@ contract SampleContract { constructor() { /* comment 9 */ } // comment 10 // comment 11 - function max( /* comment 13 */ uint256 arg1, uint256 /* comment 14 */ arg2, uint256 /* comment 15 */ ) + function max( /* comment 13 */ + uint256 arg1, + uint256 /* comment 14 */ arg2, + uint256 /* comment 15 */ + ) // comment 16 external /* comment 17 */ pure diff --git a/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol b/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol index 2e9661f956dcf..ffc80a47957b6 100644 --- a/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/contract-new-lines.fmt.sol @@ -43,9 +43,11 @@ contract ERC20DecimalsMock is ERC20 { uint8 private immutable _decimals; - constructor(string memory name_, string memory symbol_, uint8 decimals_) - ERC20(name_, symbol_) - { + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) ERC20(name_, symbol_) { _decimals = decimals_; } diff --git a/crates/fmt/testdata/ContractDefinition/fmt.sol b/crates/fmt/testdata/ContractDefinition/fmt.sol index 551e84decfc5b..f043259c1f84a 100644 --- a/crates/fmt/testdata/ContractDefinition/fmt.sol +++ b/crates/fmt/testdata/ContractDefinition/fmt.sol @@ -39,9 +39,11 @@ contract /* comment 21 */ ExampleContract is /* comment 22 */ SampleContract {} contract ERC20DecimalsMock is ERC20 { uint8 private immutable _decimals; - constructor(string memory name_, string memory symbol_, uint8 decimals_) - ERC20(name_, symbol_) - { + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) ERC20(name_, symbol_) { _decimals = decimals_; } } diff --git a/crates/fmt/testdata/DocComments/fmt.sol b/crates/fmt/testdata/DocComments/fmt.sol index 4248f0fe587da..78c687b94e226 100644 --- a/crates/fmt/testdata/DocComments/fmt.sol +++ b/crates/fmt/testdata/DocComments/fmt.sol @@ -37,11 +37,10 @@ contract HelloWorld { * @return s The calculated surface. * @return p The calculated perimeter. */ - function rectangle(uint256 w, uint256 h) - public - pure - returns (uint256 s, uint256 p) - { + function rectangle( + uint256 w, + uint256 h + ) public pure returns (uint256 s, uint256 p) { s = w * h; p = 2 * (w + h); } diff --git a/crates/fmt/testdata/DocComments/original.sol b/crates/fmt/testdata/DocComments/original.sol index 28f654b57903d..9b48d51d5c833 100644 --- a/crates/fmt/testdata/DocComments/original.sol +++ b/crates/fmt/testdata/DocComments/original.sol @@ -27,7 +27,7 @@ contract HelloWorld { function example() public { /** * Does this add a whitespace error? - * + * * Let's find out. */ } @@ -43,10 +43,10 @@ contract HelloWorld { p = 2 * (w + h); } - /// A long doc line comment that will be wrapped + /// A long doc line comment that will be wrapped function docLineOverflow() external {} - function docLinePostfixOverflow() external {} /// A long doc line comment that will be wrapped + function docLinePostfixOverflow() external {} /// A long doc line comment that will be wrapped /** * @notice Here is my comment diff --git a/crates/fmt/testdata/DocComments/tab.fmt.sol b/crates/fmt/testdata/DocComments/tab.fmt.sol index 0a2ca7a309431..224bb851ed41a 100644 --- a/crates/fmt/testdata/DocComments/tab.fmt.sol +++ b/crates/fmt/testdata/DocComments/tab.fmt.sol @@ -10,7 +10,7 @@ contract HelloWorld { } /** - * Here's a more double asterix comment + * Here's a more double asterix comment */ Person public theDude; @@ -21,28 +21,27 @@ contract HelloWorld { } /** - * @dev does nothing + * @dev does nothing */ function example() public { /** - * Does this add a whitespace error? + * Does this add a whitespace error? * - * Let's find out. + * 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. + * @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) - { + function rectangle( + uint256 w, + uint256 h + ) public pure returns (uint256 s, uint256 p) { s = w * h; p = 2 * (w + h); } @@ -55,47 +54,47 @@ contract HelloWorld { /// A long doc line comment that will be wrapped /** - * @notice Here is my comment - * - item 1 - * - item 2 - * Some equations: - * y = mx + b + * @notice Here is my comment + * - item 1 + * - item 2 + * Some equations: + * y = mx + b */ function anotherExample() external {} /** - * contract A { - * function foo() public { - * // does nothing. - * } - * } + * contract A { + * function foo() public { + * // does nothing. + * } + * } */ function multilineIndent() external {} /** - * contract A { - * function foo() public { - * // does nothing. - * } - * } + * contract A { + * function foo() public { + * // does nothing. + * } + * } */ function multilineMalformedIndent() external {} /** - * contract A { - * function withALongNameThatWillCauseCommentWrap() public { - * // does nothing. - * } - * } + * contract A { + * function withALongNameThatWillCauseCommentWrap() public { + * // does nothing. + * } + * } */ function malformedIndentOverflow() external {} } /** - * contract A { - * function foo() public { - * // does nothing. - * } - * } + * contract A { + * function foo() public { + * // does nothing. + * } + * } */ function freeFloatingMultilineIndent() {} diff --git a/crates/fmt/testdata/DocComments/wrap-comments.fmt.sol b/crates/fmt/testdata/DocComments/wrap-comments.fmt.sol index c3c7fe00c9180..8a1d56ea56deb 100644 --- a/crates/fmt/testdata/DocComments/wrap-comments.fmt.sol +++ b/crates/fmt/testdata/DocComments/wrap-comments.fmt.sol @@ -11,8 +11,7 @@ contract HelloWorld { } /** - * Here's a more double asterix - * comment + * Here's a more double asterix comment */ Person public theDude; @@ -30,21 +29,19 @@ contract HelloWorld { */ function example() public { /** - * Does this add a whitespace - * error? + * Does this add a whitespace error? * * Let's find out. */ } /** - * @dev Calculates a rectangle's - * surface and perimeter. + * @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. + * @return p The calculated perimeter. */ function rectangle( uint256 w, @@ -58,18 +55,16 @@ contract HelloWorld { p = 2 * (w + h); } - /// A long doc line comment that - /// will be wrapped - function docLineOverflow() - external - {} + /// A long doc line comment that will be + /// wrapped + function docLineOverflow() external {} function docLinePostfixOverflow() external {} - /// A long doc line comment that - /// will be wrapped + /// A long doc line comment that will be + /// wrapped /** * @notice Here is my comment @@ -78,9 +73,7 @@ contract HelloWorld { * Some equations: * y = mx + b */ - function anotherExample() - external - {} + function anotherExample() external {} /** * contract A { @@ -89,9 +82,7 @@ contract HelloWorld { * } * } */ - function multilineIndent() - external - {} + function multilineIndent() external {} /** * contract A { diff --git a/crates/fmt/testdata/EmitStatement/fmt.sol b/crates/fmt/testdata/EmitStatement/fmt.sol index 0fac66b9b2b80..c72434fc0b1bd 100644 --- a/crates/fmt/testdata/EmitStatement/fmt.sol +++ b/crates/fmt/testdata/EmitStatement/fmt.sol @@ -11,8 +11,7 @@ function emitEvent() { endTimestamp ); - emit NewEvent( - /* beneficiary */ + emit NewEvent( /* beneficiary */ beneficiary, /* index */ _vestingBeneficiaries.length - 1, diff --git a/crates/fmt/testdata/EnumDefinition/bracket-spacing.fmt.sol b/crates/fmt/testdata/EnumDefinition/bracket-spacing.fmt.sol index a4ae0f0192051..10483822a86b8 100644 --- a/crates/fmt/testdata/EnumDefinition/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/EnumDefinition/bracket-spacing.fmt.sol @@ -1,6 +1,6 @@ // config: bracket_spacing = true contract EnumDefinitions { - enum Empty { } + enum Empty {} enum ActionChoices { GoLeft, GoRight, diff --git a/crates/fmt/testdata/EnumVariants/fmt.sol b/crates/fmt/testdata/EnumVariants/fmt.sol index b33b8846984d2..3f1c59b8f2595 100644 --- a/crates/fmt/testdata/EnumVariants/fmt.sol +++ b/crates/fmt/testdata/EnumVariants/fmt.sol @@ -10,8 +10,8 @@ interface I { /// A modification applied to either `msg.sender` or `tx.origin`. Returned by `readCallers`. enum CallerMode2 { /// No caller modification is currently active. - None, - /// No caller modification is currently active2. + None, /// No caller modification is currently active2. + Some } diff --git a/crates/fmt/testdata/ForStatement/fmt.sol b/crates/fmt/testdata/ForStatement/fmt.sol index a1bb4b2e6a28c..c6dbe34f17e20 100644 --- a/crates/fmt/testdata/ForStatement/fmt.sol +++ b/crates/fmt/testdata/ForStatement/fmt.sol @@ -12,7 +12,16 @@ contract ForStatement { uint256 veryLongVariableName = 1000; for ( uint256 i3; - i3 < 10 && veryLongVariableName > 999 && veryLongVariableName < 1001; + i3 < 10 && veryLongVariableName > 999 + && veryLongVariableName < 1001; + i3++ + ) { + i3++; + } + + for ( + uint256 i3; + i3 < 10 && veryLongVariableName > 900 && veryLongVariableName < 999; i3++ ) { i3++; diff --git a/crates/fmt/testdata/ForStatement/original.sol b/crates/fmt/testdata/ForStatement/original.sol index e98288dd1cbce..7cb4711192b5d 100644 --- a/crates/fmt/testdata/ForStatement/original.sol +++ b/crates/fmt/testdata/ForStatement/original.sol @@ -11,7 +11,7 @@ contract ForStatement { uint256 i2; for(++i2;i2<10;i2++) - + {} uint256 veryLongVariableName = 1000; @@ -20,6 +20,12 @@ contract ForStatement { ; i3++) { i3 ++ ; } + for ( uint256 i3; i3 < 10 + && veryLongVariableName>900 && veryLongVariableName< 999 + ; i3++) + { i3 ++ ; } + + for (type(uint256).min;;) {} for (;;) { "test" ; } @@ -30,4 +36,4 @@ contract ForStatement { for (uint256 i6 = 10; i6 > i5; i6--) i5++; } -} \ No newline at end of file +} diff --git a/crates/fmt/testdata/FunctionCallArgsStatement/bracket-spacing.fmt.sol b/crates/fmt/testdata/FunctionCallArgsStatement/bracket-spacing.fmt.sol index 93e5eb1a2e793..0ee802e8c923b 100644 --- a/crates/fmt/testdata/FunctionCallArgsStatement/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/FunctionCallArgsStatement/bracket-spacing.fmt.sol @@ -36,7 +36,9 @@ contract FunctionCallArgsStatement { gas: veryAndVeryLongNameOfSomeGasEstimateFunction() }(); - target.run{ /* comment 1 */ value: /* comment2 */ 1 }; + target.run{ /* comment 1 */ + value: /* comment2 */ 1 + }; target.run{ /* comment3 */ value: 1, // comment4 diff --git a/crates/fmt/testdata/FunctionCallArgsStatement/fmt.sol b/crates/fmt/testdata/FunctionCallArgsStatement/fmt.sol index 5a5cc5f634281..caa000b0c0928 100644 --- a/crates/fmt/testdata/FunctionCallArgsStatement/fmt.sol +++ b/crates/fmt/testdata/FunctionCallArgsStatement/fmt.sol @@ -35,7 +35,9 @@ contract FunctionCallArgsStatement { gas: veryAndVeryLongNameOfSomeGasEstimateFunction() }(); - target.run{ /* comment 1 */ value: /* comment2 */ 1}; + target.run{ /* comment 1 */ + value: /* comment2 */ 1 + }; target.run{ /* comment3 */ value: 1, // comment4 diff --git a/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol b/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol index db7164d284a54..5a5f4fad50ff9 100644 --- a/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/all-params.fmt.sol @@ -15,7 +15,7 @@ interface FunctionInterfaces { uint256 x1, // x1 postfix // x2 prefix uint256 x2, // x2 postfix - // x2 postfix2 + // x2 postfix2 /* multi-line x3 prefix */ @@ -39,9 +39,8 @@ interface FunctionInterfaces { // y2 prefix uint256 y2, // y2 postfix // y3 prefix - uint256 y3 - ); // y3 postfix - // function postfix + uint256 y3 // y3 postfix + ); // function postfix /*////////////////////////////////////////////////////////////////////////// TEST @@ -332,10 +331,10 @@ interface FunctionInterfaces { } contract FunctionDefinitions { - function() external {} + function f() external {} fallback() external {} - function() external payable {} + function f() external payable {} fallback() external payable {} receive() external payable {} diff --git a/crates/fmt/testdata/FunctionDefinition/all.fmt.sol b/crates/fmt/testdata/FunctionDefinition/all.fmt.sol index 6d90880679199..6c0d422ca998d 100644 --- a/crates/fmt/testdata/FunctionDefinition/all.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/all.fmt.sol @@ -15,7 +15,7 @@ interface FunctionInterfaces { uint256 x1, // x1 postfix // x2 prefix uint256 x2, // x2 postfix - // x2 postfix2 + // x2 postfix2 /* multi-line x3 prefix */ @@ -39,9 +39,8 @@ interface FunctionInterfaces { // y2 prefix uint256 y2, // y2 postfix // y3 prefix - uint256 y3 - ); // y3 postfix - // function postfix + uint256 y3 // y3 postfix + ); // function postfix /*////////////////////////////////////////////////////////////////////////// TEST @@ -332,10 +331,10 @@ interface FunctionInterfaces { } contract FunctionDefinitions { - function() external {} + function f() external {} fallback() external {} - function() external payable {} + function f() external payable {} fallback() external payable {} receive() external payable {} diff --git a/crates/fmt/testdata/FunctionDefinition/fmt.sol b/crates/fmt/testdata/FunctionDefinition/fmt.sol index 9e34a8bea2682..aebfd26c42650 100644 --- a/crates/fmt/testdata/FunctionDefinition/fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/fmt.sol @@ -14,7 +14,7 @@ interface FunctionInterfaces { uint256 x1, // x1 postfix // x2 prefix uint256 x2, // x2 postfix - // x2 postfix2 + // x2 postfix2 /* multi-line x3 prefix */ @@ -38,9 +38,8 @@ interface FunctionInterfaces { // y2 prefix uint256 y2, // y2 postfix // y3 prefix - uint256 y3 - ); // y3 postfix - // function postfix + uint256 y3 // y3 postfix + ); // function postfix /*////////////////////////////////////////////////////////////////////////// TEST @@ -323,10 +322,10 @@ interface FunctionInterfaces { } contract FunctionDefinitions { - function() external {} + function f() external {} fallback() external {} - function() external payable {} + function f() external payable {} fallback() external payable {} receive() external payable {} diff --git a/crates/fmt/testdata/FunctionDefinition/original.sol b/crates/fmt/testdata/FunctionDefinition/original.sol index 97db649d55660..a416fc98de47b 100644 --- a/crates/fmt/testdata/FunctionDefinition/original.sol +++ b/crates/fmt/testdata/FunctionDefinition/original.sol @@ -91,10 +91,10 @@ interface FunctionInterfaces { } contract FunctionDefinitions { - function () external {} + function f() external {} fallback () external {} - function () external payable {} + function f() external payable {} fallback () external payable {} receive () external payable {} @@ -215,4 +215,3 @@ contract FunctionOverrides is FunctionInterfaces, FunctionDefinitions { a = 1; } } - diff --git a/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol b/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol index 516e5c2fd42ed..4c2ea4953e6aa 100644 --- a/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/override-spacing.fmt.sol @@ -15,7 +15,7 @@ interface FunctionInterfaces { uint256 x1, // x1 postfix // x2 prefix uint256 x2, // x2 postfix - // x2 postfix2 + // x2 postfix2 /* multi-line x3 prefix */ @@ -39,9 +39,8 @@ interface FunctionInterfaces { // y2 prefix uint256 y2, // y2 postfix // y3 prefix - uint256 y3 - ); // y3 postfix - // function postfix + uint256 y3 // y3 postfix + ); // function postfix /*////////////////////////////////////////////////////////////////////////// TEST @@ -324,10 +323,10 @@ interface FunctionInterfaces { } contract FunctionDefinitions { - function() external {} + function f() external {} fallback() external {} - function() external payable {} + function f() external payable {} fallback() external payable {} receive() external payable {} diff --git a/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol b/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol index 3e7ebfff6b3aa..88139d97d81b0 100644 --- a/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/params-first.fmt.sol @@ -17,7 +17,7 @@ interface FunctionInterfaces { uint256 x1, // x1 postfix // x2 prefix uint256 x2, // x2 postfix - // x2 postfix2 + // x2 postfix2 /* multi-line x3 prefix */ @@ -41,9 +41,8 @@ interface FunctionInterfaces { // y2 prefix uint256 y2, // y2 postfix // y3 prefix - uint256 y3 - ); // y3 postfix - // function postfix + uint256 y3 // y3 postfix + ); // function postfix /*////////////////////////////////////////////////////////////////////////// TEST @@ -326,10 +325,10 @@ interface FunctionInterfaces { } contract FunctionDefinitions { - function() external {} + function f() external {} fallback() external {} - function() external payable {} + function f() external payable {} fallback() external payable {} receive() external payable {} diff --git a/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol b/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol index cd2015c9e050e..016d52a1bedd0 100644 --- a/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol +++ b/crates/fmt/testdata/FunctionDefinition/params-multi.fmt.sol @@ -15,7 +15,7 @@ interface FunctionInterfaces { uint256 x1, // x1 postfix // x2 prefix uint256 x2, // x2 postfix - // x2 postfix2 + // x2 postfix2 /* multi-line x3 prefix */ @@ -39,9 +39,8 @@ interface FunctionInterfaces { // y2 prefix uint256 y2, // y2 postfix // y3 prefix - uint256 y3 - ); // y3 postfix - // function postfix + uint256 y3 // y3 postfix + ); // function postfix /*////////////////////////////////////////////////////////////////////////// TEST @@ -324,10 +323,10 @@ interface FunctionInterfaces { } contract FunctionDefinitions { - function() external {} + function f() external {} fallback() external {} - function() external payable {} + function f() external payable {} fallback() external payable {} receive() external payable {} diff --git a/crates/fmt/testdata/FunctionDefinitionWithFunctionReturns/fmt.sol b/crates/fmt/testdata/FunctionDefinitionWithFunctionReturns/fmt.sol index 7b751e22ec26a..f961e7bd9601c 100644 --- a/crates/fmt/testdata/FunctionDefinitionWithFunctionReturns/fmt.sol +++ b/crates/fmt/testdata/FunctionDefinitionWithFunctionReturns/fmt.sol @@ -5,10 +5,7 @@ contract ReturnFnFormat { function returnsFunction() internal pure - returns ( - function() - internal pure returns (uint256) - ) + returns (function() internal pure returns (uint256)) {} } diff --git a/crates/fmt/testdata/FunctionType/fmt.sol b/crates/fmt/testdata/FunctionType/fmt.sol index 39053d816058f..95dafb998b9cf 100644 --- a/crates/fmt/testdata/FunctionType/fmt.sol +++ b/crates/fmt/testdata/FunctionType/fmt.sol @@ -1,6 +1,6 @@ -// config: line_length = 90 +// config: line_length = 100 library ArrayUtils { - function map(uint256[] memory self, function (uint) pure returns (uint) f) + function map(uint256[] memory self, function(uint256) pure returns (uint256) f) internal pure returns (uint256[] memory r) @@ -11,7 +11,7 @@ library ArrayUtils { } } - function reduce(uint256[] memory self, function (uint, uint) pure returns (uint) f) + function reduce(uint256[] memory self, function(uint256, uint256) pure returns (uint256) f) internal pure returns (uint256 r) diff --git a/crates/fmt/testdata/IfStatement/block-multi.fmt.sol b/crates/fmt/testdata/IfStatement/block-multi.fmt.sol index dcd8bb83eaa8f..a3e932c61fe27 100644 --- a/crates/fmt/testdata/IfStatement/block-multi.fmt.sol +++ b/crates/fmt/testdata/IfStatement/block-multi.fmt.sol @@ -50,8 +50,8 @@ contract IfStatement { /* comment9 */ else if ( /* comment10 */ anotherLongCondition // comment11 - ) { /* comment12 */ + ) { execute(); } // comment13 /* comment14 */ @@ -93,8 +93,8 @@ contract IfStatement { } if (condition) { - execute(); - } // comment18 + execute(); // comment18 + } if (condition) { executeWithMultipleParameters(condition, anotherLongCondition); diff --git a/crates/fmt/testdata/IfStatement/block-single.fmt.sol b/crates/fmt/testdata/IfStatement/block-single.fmt.sol index ba2b9998b184c..8f5f299cb37f4 100644 --- a/crates/fmt/testdata/IfStatement/block-single.fmt.sol +++ b/crates/fmt/testdata/IfStatement/block-single.fmt.sol @@ -43,8 +43,8 @@ contract IfStatement { /* comment9 */ else if ( /* comment10 */ anotherLongCondition // comment11 - ) { /* comment12 */ + ) { execute(); } // comment13 /* comment14 */ diff --git a/crates/fmt/testdata/IfStatement/fmt.sol b/crates/fmt/testdata/IfStatement/fmt.sol index cb2f8874f83d5..ebd685052867d 100644 --- a/crates/fmt/testdata/IfStatement/fmt.sol +++ b/crates/fmt/testdata/IfStatement/fmt.sol @@ -49,8 +49,8 @@ contract IfStatement { /* comment9 */ else if ( /* comment10 */ anotherLongCondition // comment11 - ) { /* comment12 */ + ) { execute(); } // comment13 /* comment14 */ diff --git a/crates/fmt/testdata/ImportDirective/bracket-spacing.fmt.sol b/crates/fmt/testdata/ImportDirective/bracket-spacing.fmt.sol index 1db94929ab7c7..5c5ae93e9a692 100644 --- a/crates/fmt/testdata/ImportDirective/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/ImportDirective/bracket-spacing.fmt.sol @@ -5,8 +5,8 @@ import "SomeFile.sol" as SomeOtherFile; import "SomeFile.sol" as SomeOtherFile; import "AnotherFile.sol" as SomeSymbol; import "AnotherFile.sol" as SomeSymbol; -import { symbol1 as alias, symbol2 } from "File.sol"; -import { symbol1 as alias, symbol2 } from "File.sol"; +import { symbol1 as alias0, symbol2 } from "File.sol"; +import { symbol1 as alias0, symbol2 } from "File.sol"; import { symbol1 as alias1, symbol2 as alias2, diff --git a/crates/fmt/testdata/ImportDirective/fmt.sol b/crates/fmt/testdata/ImportDirective/fmt.sol index 4915b8ab203c8..83a739f4e1e73 100644 --- a/crates/fmt/testdata/ImportDirective/fmt.sol +++ b/crates/fmt/testdata/ImportDirective/fmt.sol @@ -4,8 +4,8 @@ import "SomeFile.sol" as SomeOtherFile; import "SomeFile.sol" as SomeOtherFile; import "AnotherFile.sol" as SomeSymbol; import "AnotherFile.sol" as SomeSymbol; -import {symbol1 as alias, symbol2} from "File.sol"; -import {symbol1 as alias, symbol2} from "File.sol"; +import {symbol1 as alias0, symbol2} from "File.sol"; +import {symbol1 as alias0, symbol2} from "File.sol"; import { symbol1 as alias1, symbol2 as alias2, diff --git a/crates/fmt/testdata/ImportDirective/original.sol b/crates/fmt/testdata/ImportDirective/original.sol index 0e18e10c14dab..f027174512196 100644 --- a/crates/fmt/testdata/ImportDirective/original.sol +++ b/crates/fmt/testdata/ImportDirective/original.sol @@ -4,7 +4,7 @@ import "SomeFile.sol" as SomeOtherFile; import 'SomeFile.sol' as SomeOtherFile; import * as SomeSymbol from "AnotherFile.sol"; import * as SomeSymbol from 'AnotherFile.sol'; -import {symbol1 as alias, symbol2} from "File.sol"; -import {symbol1 as alias, symbol2} from 'File.sol'; +import {symbol1 as alias0, symbol2} from "File.sol"; +import {symbol1 as alias0, symbol2} from 'File.sol'; import {symbol1 as alias1, symbol2 as alias2, symbol3 as alias3, symbol4} from "File2.sol"; import {symbol1 as alias1, symbol2 as alias2, symbol3 as alias3, symbol4} from 'File2.sol'; diff --git a/crates/fmt/testdata/ImportDirective/preserve-quote.fmt.sol b/crates/fmt/testdata/ImportDirective/preserve-quote.fmt.sol index d1bf9852c02e5..66d2a1d1ec6c1 100644 --- a/crates/fmt/testdata/ImportDirective/preserve-quote.fmt.sol +++ b/crates/fmt/testdata/ImportDirective/preserve-quote.fmt.sol @@ -5,8 +5,8 @@ import "SomeFile.sol" as SomeOtherFile; import 'SomeFile.sol' as SomeOtherFile; import "AnotherFile.sol" as SomeSymbol; import 'AnotherFile.sol' as SomeSymbol; -import {symbol1 as alias, symbol2} from "File.sol"; -import {symbol1 as alias, symbol2} from 'File.sol'; +import {symbol1 as alias0, symbol2} from "File.sol"; +import {symbol1 as alias0, symbol2} from 'File.sol'; import { symbol1 as alias1, symbol2 as alias2, diff --git a/crates/fmt/testdata/ImportDirective/single-quote.fmt.sol b/crates/fmt/testdata/ImportDirective/single-quote.fmt.sol index 10449e079ae81..d72e043f4f5d7 100644 --- a/crates/fmt/testdata/ImportDirective/single-quote.fmt.sol +++ b/crates/fmt/testdata/ImportDirective/single-quote.fmt.sol @@ -5,8 +5,8 @@ import 'SomeFile.sol' as SomeOtherFile; import 'SomeFile.sol' as SomeOtherFile; import 'AnotherFile.sol' as SomeSymbol; import 'AnotherFile.sol' as SomeSymbol; -import {symbol1 as alias, symbol2} from 'File.sol'; -import {symbol1 as alias, symbol2} from 'File.sol'; +import {symbol1 as alias0, symbol2} from 'File.sol'; +import {symbol1 as alias0, symbol2} from 'File.sol'; import { symbol1 as alias1, symbol2 as alias2, diff --git a/crates/fmt/testdata/InlineDisable/fmt.sol b/crates/fmt/testdata/InlineDisable/fmt.sol index d7adea60b32da..dae3f1a6ff02c 100644 --- a/crates/fmt/testdata/InlineDisable/fmt.sol +++ b/crates/fmt/testdata/InlineDisable/fmt.sol @@ -29,7 +29,7 @@ enum States { enum States { State1, State2, State3, State4, State5, State6, State7, State8, State9 } // forgefmt: disable-next-line -bytes32 constant private BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; +bytes32 constant BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; // forgefmt: disable-start @@ -37,7 +37,7 @@ bytes32 constant private BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b6 // comment2 -/* comment 3 */ /* +/* comment 3 */ /* comment4 */ // comment 5 @@ -105,7 +105,7 @@ function testDoWhile() external { uint256 i; do { "test"; } while (i != 0); - do + do {} while ( @@ -113,9 +113,9 @@ i != 0); bool someVeryVeryLongCondition; do { "test"; } while( - someVeryVeryLongCondition && !someVeryVeryLongCondition && + someVeryVeryLongCondition && !someVeryVeryLongCondition && !someVeryVeryLongCondition && -someVeryVeryLongCondition); +someVeryVeryLongCondition); do i++; while(i < 10); @@ -134,7 +134,7 @@ function forStatement() { uint256 i2; for(++i2;i2<10;i2++) - + {} uint256 veryLongVariableName = 1000; @@ -161,18 +161,18 @@ function callArgTest() { target.run{gas:1,value:0x00}(); - target.run{ - gas : 1000, - value: 1 ether + target.run{ + gas : 1000, + value: 1 ether } (); target.run{ gas: estimate(), - value: value(1) }(); + value: value(1) }(); target.run { value: value(1 ether), gas: veryAndVeryLongNameOfSomeGasEstimateFunction() } (); - target.run /* comment 1 */ { value: /* comment2 */ 1 }; + target.run /* comment 1 */ { value: /* comment2 */ 1 }; target.run { /* comment3 */ value: 1, // comment4 gas: gasleft()}; @@ -240,7 +240,7 @@ function returnTest() { 0x00; } - if (val == 1) { return + if (val == 1) { return 1; } if (val == 2) { @@ -270,11 +270,11 @@ function namedFuncCall() { ComplexStruct memory complex = ComplexStruct({ val: 1, anotherVal: 2, flag: true, timestamp: block.timestamp }); StructWithAVeryLongNameThatExceedsMaximumLengthThatIsAllowedForFormatting memory long = StructWithAVeryLongNameThatExceedsMaximumLengthThatIsAllowedForFormatting({ whyNameSoLong: "dunno" }); - + SimpleStruct memory simple2 = SimpleStruct( - { // comment1 + { // comment1 /* comment2 */ val : /* comment3 */ 0 - + } ); // forgefmt: disable-end @@ -294,10 +294,10 @@ function revertTest() { 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 ({}); // forgefmt: disable-end } @@ -329,7 +329,7 @@ function thisTest() { this // comment1 .someVeryVeryVeryLongVariableNameThatWillBeAccessedByThisKeyword(); address(this).balance; - + address thisAddress = address( // comment2 /* comment3 */ this // comment 4 @@ -365,10 +365,10 @@ function tryTest() { } try unknown.lookupMultipleValues() returns (uint256, uint256, uint256, uint256, uint256) {} catch Error(string memory) {} catch {} - + try unknown.lookupMultipleValues() returns (uint256, uint256, uint256, uint256, uint256) { unknown.doSomething(); - } + } catch Error(string memory) { unknown.handleError(); } @@ -385,7 +385,7 @@ function testArray() { : /* comment2 */ msg.data.length // comment3 ]; msg.data[ - // comment4 + // comment4 4 // comment5 :msg.data.length /* comment6 */]; // forgefmt: disable-end @@ -457,7 +457,7 @@ function testWhile() { i3 < 10 ) { i3++; } - uint256 i4; while (i4 < 10) + uint256 i4; while (i4 < 10) { i4 ++ ;} @@ -469,11 +469,8 @@ function testWhile() { } function testLine() {} - function /* forgefmt: disable-line */ testLine( ) { } - function testLine() {} - function testLine( ) { } // forgefmt: disable-line // forgefmt: disable-start @@ -487,7 +484,7 @@ error TopLevelCustomErrorArgWithoutName (string); event Event1(uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a); -// forgefmt: disable-stop +// forgefmt: disable-end function setNumber(uint256 newNumber /* param1 */, uint256 sjdfasdfasdfasdfasfsdfsadfasdfasdfasdfsadjfkhasdfljkahsdfkjasdkfhsaf /* param2 */) public view returns (bool,bool) { /* inline*/ number1 = newNumber1; // forgefmt: disable-line number = newNumber; diff --git a/crates/fmt/testdata/InlineDisable/original.sol b/crates/fmt/testdata/InlineDisable/original.sol index 7731678940cbf..8a51a8f786c0f 100644 --- a/crates/fmt/testdata/InlineDisable/original.sol +++ b/crates/fmt/testdata/InlineDisable/original.sol @@ -14,7 +14,7 @@ enum States { State1, State2, State3, State4, State5, State6, State7, State8, St enum States { State1, State2, State3, State4, State5, State6, State7, State8, State9 } // forgefmt: disable-next-line -bytes32 constant private BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; +bytes32 constant BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; // forgefmt: disable-start @@ -22,7 +22,7 @@ bytes32 constant private BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b6 // comment2 -/* comment 3 */ /* +/* comment 3 */ /* comment4 */ // comment 5 @@ -87,7 +87,7 @@ function testDoWhile() external { uint256 i; do { "test"; } while (i != 0); - do + do {} while ( @@ -95,9 +95,9 @@ i != 0); bool someVeryVeryLongCondition; do { "test"; } while( - someVeryVeryLongCondition && !someVeryVeryLongCondition && + someVeryVeryLongCondition && !someVeryVeryLongCondition && !someVeryVeryLongCondition && -someVeryVeryLongCondition); +someVeryVeryLongCondition); do i++; while(i < 10); @@ -116,7 +116,7 @@ function forStatement() { uint256 i2; for(++i2;i2<10;i2++) - + {} uint256 veryLongVariableName = 1000; @@ -143,18 +143,18 @@ function callArgTest() { target.run{gas:1,value:0x00}(); - target.run{ - gas : 1000, - value: 1 ether + target.run{ + gas : 1000, + value: 1 ether } (); target.run{ gas: estimate(), - value: value(1) }(); + value: value(1) }(); target.run { value: value(1 ether), gas: veryAndVeryLongNameOfSomeGasEstimateFunction() } (); - target.run /* comment 1 */ { value: /* comment2 */ 1 }; + target.run /* comment 1 */ { value: /* comment2 */ 1 }; target.run { /* comment3 */ value: 1, // comment4 gas: gasleft()}; @@ -211,7 +211,7 @@ function literalTest() { // forgefmt: disable-end // forgefmt: disable-next-line - bytes memory bytecode = + bytes memory bytecode = hex"ff"; } @@ -222,7 +222,7 @@ function returnTest() { 0x00; } - if (val == 1) { return + if (val == 1) { return 1; } if (val == 2) { @@ -252,11 +252,11 @@ function namedFuncCall() { ComplexStruct memory complex = ComplexStruct({ val: 1, anotherVal: 2, flag: true, timestamp: block.timestamp }); StructWithAVeryLongNameThatExceedsMaximumLengthThatIsAllowedForFormatting memory long = StructWithAVeryLongNameThatExceedsMaximumLengthThatIsAllowedForFormatting({ whyNameSoLong: "dunno" }); - + SimpleStruct memory simple2 = SimpleStruct( - { // comment1 + { // comment1 /* comment2 */ val : /* comment3 */ 0 - + } ); // forgefmt: disable-end @@ -276,10 +276,10 @@ function revertTest() { 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 ({}); // forgefmt: disable-end } @@ -311,7 +311,7 @@ function thisTest() { this // comment1 .someVeryVeryVeryLongVariableNameThatWillBeAccessedByThisKeyword(); address(this).balance; - + address thisAddress = address( // comment2 /* comment3 */ this // comment 4 @@ -347,10 +347,10 @@ function tryTest() { } try unknown.lookupMultipleValues() returns (uint256, uint256, uint256, uint256, uint256) {} catch Error(string memory) {} catch {} - + try unknown.lookupMultipleValues() returns (uint256, uint256, uint256, uint256, uint256) { unknown.doSomething(); - } + } catch Error(string memory) { unknown.handleError(); } @@ -367,7 +367,7 @@ function testArray() { : /* comment2 */ msg.data.length // comment3 ]; msg.data[ - // comment4 + // comment4 4 // comment5 :msg.data.length /* comment6 */]; // forgefmt: disable-end @@ -439,7 +439,7 @@ function testWhile() { i3 < 10 ) { i3++; } - uint256 i4; while (i4 < 10) + uint256 i4; while (i4 < 10) { i4 ++ ;} @@ -450,9 +450,9 @@ function testWhile() { // forgefmt: disable-end } -function testLine( ) { } -function /* forgefmt: disable-line */ testLine( ) { } -function testLine( ) { } +function testLine( ) { } +function /* forgefmt: disable-line */ testLine( ) { } +function testLine( ) { } function testLine( ) { } // forgefmt: disable-line // forgefmt: disable-start @@ -466,7 +466,7 @@ error TopLevelCustomErrorArgWithoutName (string); event Event1(uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a, uint256 indexed a); -// forgefmt: disable-stop +// forgefmt: disable-end function setNumber(uint256 newNumber /* param1 */, uint256 sjdfasdfasdfasdfasfsdfsadfasdfasdfasdfsadjfkhasdfljkahsdfkjasdkfhsaf /* param2 */) public view returns (bool,bool) { /* inline*/ number1 = newNumber1; // forgefmt: disable-line number = newNumber; diff --git a/crates/fmt/testdata/LiteralExpression/fmt.sol b/crates/fmt/testdata/LiteralExpression/fmt.sol index 7fa6878e5586d..4c845185f4efb 100644 --- a/crates/fmt/testdata/LiteralExpression/fmt.sol +++ b/crates/fmt/testdata/LiteralExpression/fmt.sol @@ -11,7 +11,7 @@ contract LiteralExpressions { // number literals 1; 123_000; - 1_2e345_678; + // 1_2e345_678; -1; 2e-10; // comment5 diff --git a/crates/fmt/testdata/LiteralExpression/original.sol b/crates/fmt/testdata/LiteralExpression/original.sol index 5c559531de955..37bf210359f7f 100644 --- a/crates/fmt/testdata/LiteralExpression/original.sol +++ b/crates/fmt/testdata/LiteralExpression/original.sol @@ -10,7 +10,7 @@ contract LiteralExpressions { // number literals 1; 123_000; - 1_2e345_678; + // 1_2e345_678; -1; 2e-10; // comment5 diff --git a/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol b/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol index 3d9490804f231..3aafcef65e978 100644 --- a/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol +++ b/crates/fmt/testdata/LiteralExpression/preserve-quote.fmt.sol @@ -12,7 +12,7 @@ contract LiteralExpressions { // number literals 1; 123_000; - 1_2e345_678; + // 1_2e345_678; -1; 2e-10; // comment5 diff --git a/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol b/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol index cdc67a2c6c814..83ff3f94a4435 100644 --- a/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol +++ b/crates/fmt/testdata/LiteralExpression/single-quote.fmt.sol @@ -12,7 +12,7 @@ contract LiteralExpressions { // number literals 1; 123_000; - 1_2e345_678; + // 1_2e345_678; -1; 2e-10; // comment5 diff --git a/crates/fmt/testdata/MappingType/fmt.sol b/crates/fmt/testdata/MappingType/fmt.sol index 7f6297cff9455..61d5cd02fb300 100644 --- a/crates/fmt/testdata/MappingType/fmt.sol +++ b/crates/fmt/testdata/MappingType/fmt.sol @@ -29,7 +29,7 @@ contract Mapping { // comment2 ) mapping6; mapping( /* comment3 */ - uint256 /* comment4 */ key /* comment5 */ - => /* comment6 */ uint256 /* comment7 */ value /* comment8 */ /* comment9 */ + uint256 /* comment4 */ key /* comment5 */ /* comment6 */ + => uint256 /* comment7 */ value /* comment8 */ /* comment9 */ ) /* comment10 */ mapping7; } diff --git a/crates/fmt/testdata/NamedFunctionCallExpression/fmt.sol b/crates/fmt/testdata/NamedFunctionCallExpression/fmt.sol index 14a24c9003888..3927c9c0a5063 100644 --- a/crates/fmt/testdata/NamedFunctionCallExpression/fmt.sol +++ b/crates/fmt/testdata/NamedFunctionCallExpression/fmt.sol @@ -26,22 +26,20 @@ contract NamedFunctionCallExpression { }); StructWithAVeryLongNameThatExceedsMaximumLengthThatIsAllowedForFormatting - memory long = - StructWithAVeryLongNameThatExceedsMaximumLengthThatIsAllowedForFormatting({ + memory + long = StructWithAVeryLongNameThatExceedsMaximumLengthThatIsAllowedForFormatting({ whyNameSoLong: "dunno" }); SimpleStruct memory simple2 = SimpleStruct({ // comment1 - /* comment2 */ - val: /* comment3 */ 0 + /* comment2 */ val: /* comment3 */ 0 }); - SimpleStruct memory simple3 = SimpleStruct({ - /* comment4 */ + SimpleStruct memory simple3 = SimpleStruct({ /* comment4 */ // comment5 val: // comment6 0 // comment7 - // comment8 + // comment8 }); } } diff --git a/crates/fmt/testdata/NumberLiteralUnderscore/fmt.sol b/crates/fmt/testdata/NumberLiteralUnderscore/fmt.sol index 7c9f5740de76a..77d7afe4c5dc3 100644 --- a/crates/fmt/testdata/NumberLiteralUnderscore/fmt.sol +++ b/crates/fmt/testdata/NumberLiteralUnderscore/fmt.sol @@ -2,23 +2,23 @@ contract NumberLiteral { function test() external { 1; 123_000; - 1_2e345_678; + // 1_2e345_678; -1; 2e-10; 0.1; 1.3; 2.5e1; 1.23454; - 1.2e34_5_678; - 134411.2e34_5_678; - 13431.134112e34_135_678; + // 1.2e34_5_678; + // 134411.2e34_5_678; + // 13431.134112e34_135_678; 13431.0134112; - 13431.134112e-139_3141340; - 134411.2e34_5_6780; - 13431.134112e34_135_6780; - 0.134112; + // 13431.134112e-139_3141340; + // 00134411.200e0034_5_6780; + // 013431.13411200e34_135_6780; + // 00.1341120000; 1.0; - 13431.134112e-139_3141340; + // 0013431.13411200e-00139_3141340; 123e456; 1_000; } diff --git a/crates/fmt/testdata/NumberLiteralUnderscore/original.sol b/crates/fmt/testdata/NumberLiteralUnderscore/original.sol index 8e88fc6d29472..2912a031cbef8 100644 --- a/crates/fmt/testdata/NumberLiteralUnderscore/original.sol +++ b/crates/fmt/testdata/NumberLiteralUnderscore/original.sol @@ -2,23 +2,23 @@ contract NumberLiteral { function test() external { 1; 123_000; - 1_2e345_678; + // 1_2e345_678; -1; 2e-10; .1; 1.3; 2.5e1; 1.23454e0; - 1.2e34_5_678; - 134411.2e34_5_678; - 13431.134112e34_135_678; + // 1.2e34_5_678; + // 134411.2e34_5_678; + // 13431.134112e34_135_678; 13431.0134112; - 13431.134112e-139_3141340; - 00134411.200e0034_5_6780; - 013431.13411200e34_135_6780; - 00.1341120000; + // 13431.134112e-139_3141340; + // 00134411.200e0034_5_6780; + // 013431.13411200e34_135_6780; + // 00.1341120000; 1.0000; - 0013431.13411200e-00139_3141340; + // 0013431.13411200e-00139_3141340; 123E456; 1_000; } diff --git a/crates/fmt/testdata/NumberLiteralUnderscore/preserve.fmt.sol b/crates/fmt/testdata/NumberLiteralUnderscore/preserve.fmt.sol index d87dc99d9653d..0a5e3274a49b7 100644 --- a/crates/fmt/testdata/NumberLiteralUnderscore/preserve.fmt.sol +++ b/crates/fmt/testdata/NumberLiteralUnderscore/preserve.fmt.sol @@ -3,23 +3,23 @@ contract NumberLiteral { function test() external { 1; 123_000; - 1_2e345_678; + // 1_2e345_678; -1; 2e-10; 0.1; 1.3; 2.5e1; 1.23454; - 1.2e34_5_678; - 134411.2e34_5_678; - 13431.134112e34_135_678; + // 1.2e34_5_678; + // 134411.2e34_5_678; + // 13431.134112e34_135_678; 13431.0134112; - 13431.134112e-139_3141340; - 134411.2e34_5_6780; - 13431.134112e34_135_6780; - 0.134112; + // 13431.134112e-139_3141340; + // 00134411.200e0034_5_6780; + // 013431.13411200e34_135_6780; + // 00.1341120000; 1.0; - 13431.134112e-139_3141340; + // 0013431.13411200e-00139_3141340; 123e456; 1_000; } diff --git a/crates/fmt/testdata/NumberLiteralUnderscore/remove.fmt.sol b/crates/fmt/testdata/NumberLiteralUnderscore/remove.fmt.sol index cbde2e9b9fe61..8e43680033c40 100644 --- a/crates/fmt/testdata/NumberLiteralUnderscore/remove.fmt.sol +++ b/crates/fmt/testdata/NumberLiteralUnderscore/remove.fmt.sol @@ -3,23 +3,23 @@ contract NumberLiteral { function test() external { 1; 123000; - 12e345678; + // 1_2e345_678; -1; 2e-10; 0.1; 1.3; 2.5e1; 1.23454; - 1.2e345678; - 134411.2e345678; - 13431.134112e34135678; + // 1.2e34_5_678; + // 134411.2e34_5_678; + // 13431.134112e34_135_678; 13431.0134112; - 13431.134112e-1393141340; - 134411.2e3456780; - 13431.134112e341356780; - 0.134112; + // 13431.134112e-139_3141340; + // 00134411.200e0034_5_6780; + // 013431.13411200e34_135_6780; + // 00.1341120000; 1.0; - 13431.134112e-1393141340; + // 0013431.13411200e-00139_3141340; 123e456; 1000; } diff --git a/crates/fmt/testdata/NumberLiteralUnderscore/thousands.fmt.sol b/crates/fmt/testdata/NumberLiteralUnderscore/thousands.fmt.sol index a9fc8a69ae6fa..9f0800102010e 100644 --- a/crates/fmt/testdata/NumberLiteralUnderscore/thousands.fmt.sol +++ b/crates/fmt/testdata/NumberLiteralUnderscore/thousands.fmt.sol @@ -3,23 +3,23 @@ contract NumberLiteral { function test() external { 1; 123_000; - 12e345_678; + // 1_2e345_678; -1; 2e-10; 0.1; 1.3; 2.5e1; - 1.23454; - 1.2e345_678; - 134_411.2e345_678; - 13_431.134112e34_135_678; - 13_431.0134112; - 13_431.134112e-1_393_141_340; - 134_411.2e3_456_780; - 13_431.134112e341_356_780; - 0.134112; + 1.234_54; + // 1.2e34_5_678; + // 134411.2e34_5_678; + // 13431.134112e34_135_678; + 13_431.013_411_2; + // 13431.134112e-139_3141340; + // 00134411.200e0034_5_6780; + // 013431.13411200e34_135_6780; + // 00.1341120000; 1.0; - 13_431.134112e-1_393_141_340; + // 0013431.13411200e-00139_3141340; 123e456; 1000; } diff --git a/crates/fmt/testdata/Repros/fmt.sol b/crates/fmt/testdata/Repros/fmt.sol index 0a480c0b02bdf..1f83cbf2b0e55 100644 --- a/crates/fmt/testdata/Repros/fmt.sol +++ b/crates/fmt/testdata/Repros/fmt.sol @@ -11,9 +11,7 @@ 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" - ) + ("bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla") ) }); } @@ -48,15 +46,15 @@ contract TestContract { } function test1() public { - assembly{ sstore( 1, 1) /* inline comment*/ // forgefmt: disable-line + assembly { sstore( 1, 1) /* inline comment*/ // forgefmt: disable-line sstore(2, 2) } } function test2() public { - assembly{ sstore( 1, 1) // forgefmt: disable-line + assembly { sstore( 1, 1) // forgefmt: disable-line sstore(2, 2) - sstore(3, 3)// forgefmt: disable-line + sstore(3, 3) // forgefmt: disable-line sstore(4, 4) } } @@ -65,19 +63,19 @@ contract TestContract { // forgefmt: disable-next-line assembly{ sstore( 1, 1) sstore(2, 2) - sstore(3, 3)// forgefmt: disable-line + sstore(3, 3) // forgefmt: disable-line sstore(4, 4) - }// forgefmt: disable-line + } // forgefmt: disable-line } function test4() public { // forgefmt: disable-next-line - assembly{ + assembly { sstore(1, 1) sstore(2, 2) - sstore(3, 3)// forgefmt: disable-line + sstore(3, 3) // forgefmt: disable-line sstore(4, 4) - }// forgefmt: disable-line + } // forgefmt: disable-line if (condition) execute(); // comment7 } diff --git a/crates/fmt/testdata/Repros/tab.fmt.sol b/crates/fmt/testdata/Repros/tab.fmt.sol index 565768269e1ea..861f8149f4b30 100644 --- a/crates/fmt/testdata/Repros/tab.fmt.sol +++ b/crates/fmt/testdata/Repros/tab.fmt.sol @@ -12,9 +12,7 @@ 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" - ) + ("bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla") ) }); } @@ -45,40 +43,40 @@ contract TestContract { function test(uint256 a) public { if (a > 1) { a = 2; - } // forgefmt: disable-line + } // forgefmt: disable-line } function test1() public { - assembly{ sstore( 1, 1) /* inline comment*/ // forgefmt: disable-line + assembly { sstore( 1, 1) /* inline comment*/ // forgefmt: disable-line sstore(2, 2) } } function test2() public { - assembly{ sstore( 1, 1) // forgefmt: disable-line + assembly { sstore( 1, 1) // forgefmt: disable-line sstore(2, 2) - sstore(3, 3)// forgefmt: disable-line + sstore(3, 3) // forgefmt: disable-line sstore(4, 4) } } function test3() public { // forgefmt: disable-next-line - assembly{ sstore( 1, 1) + assembly{ sstore( 1, 1) sstore(2, 2) - sstore(3, 3)// forgefmt: disable-line + sstore(3, 3) // forgefmt: disable-line sstore(4, 4) - }// forgefmt: disable-line + } // forgefmt: disable-line } function test4() public { // forgefmt: disable-next-line - assembly{ + assembly { sstore(1, 1) sstore(2, 2) - sstore(3, 3)// forgefmt: disable-line + sstore(3, 3) // forgefmt: disable-line sstore(4, 4) - }// forgefmt: disable-line + } // forgefmt: disable-line if (condition) execute(); // comment7 } @@ -89,7 +87,7 @@ contract TestContract { function test6() returns (bool) { // forgefmt: disable-line if ( true ) { // forgefmt: disable-line } - return true ; } // forgefmt: disable-line + return true ; } // forgefmt: disable-line function test7() returns (bool) { // forgefmt: disable-line if (true) { // forgefmt: disable-line @@ -99,7 +97,7 @@ contract TestContract { } function test8() returns (bool) { // forgefmt: disable-line - if ( true ) { // forgefmt: disable-line + if ( true ) { // forgefmt: disable-line uint256 a = 1; } else { uint256 b = 1; // forgefmt: disable-line @@ -113,10 +111,10 @@ library MyLib { bytes32 private constant TYPE_HASH = keccak256( // forgefmt: disable-start "MyStruct(" - "uint8 myEnum," - "address myAddress" - ")" - // forgefmt: disable-end + "uint8 myEnum," + "address myAddress" + ")" + // forgefmt: disable-end ); bytes32 private constant TYPE_HASH_1 = keccak256( @@ -124,13 +122,13 @@ library MyLib { ); // forgefmt: disable-start - bytes32 private constant TYPE_HASH_2 = keccak256( - "MyStruct(" - "uint8 myEnum," - "address myAddress" - ")" - ); - // forgefmt: disable-end + bytes32 private constant TYPE_HASH_2 = keccak256( + "MyStruct(" + "uint8 myEnum," + "address myAddress" + ")" + ); + // forgefmt: disable-end } contract IfElseTest { diff --git a/crates/fmt/testdata/ReturnStatement/fmt.sol b/crates/fmt/testdata/ReturnStatement/fmt.sol index d628d6097233d..d40330f73e805 100644 --- a/crates/fmt/testdata/ReturnStatement/fmt.sol +++ b/crates/fmt/testdata/ReturnStatement/fmt.sol @@ -23,7 +23,9 @@ contract ReturnStatement { 0x00; } - if (val == 1) return 1; + if (val == 1) { + return 1; + } if (val == 2) { return 3 - 1; diff --git a/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol b/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol index 9ad6b042b731a..4b4bc9a8d504c 100644 --- a/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol +++ b/crates/fmt/testdata/RevertNamedArgsStatement/fmt.sol @@ -19,12 +19,11 @@ contract RevertNamedArgsStatement { message: "some reason" }); - revert - SomeVeryVeryVeryLongErrorNameWithNamedArgumentsThatExceedsMaximumLength({ - val: 0, - ts: 0x00, - message: "something unpredictable happened that caused execution to revert" - }); + revert SomeVeryVeryVeryLongErrorNameWithNamedArgumentsThatExceedsMaximumLength({ + val: 0, + ts: 0x00, + message: "something unpredictable happened that caused execution to revert" + }); revert({}); // comment1 diff --git a/crates/fmt/testdata/SortedImports/fmt.sol b/crates/fmt/testdata/SortedImports/fmt.sol index f9b2c0ee2a9c3..9d6b0d9c02e2e 100644 --- a/crates/fmt/testdata/SortedImports/fmt.sol +++ b/crates/fmt/testdata/SortedImports/fmt.sol @@ -29,6 +29,6 @@ import {Something2, Something3} from "someFile.sol"; // This is a comment import {Something2, Something3} from "someFile.sol"; -import {symbol1 as alias, symbol2} from "File3.sol"; +import {symbol1 as alias0, symbol2} from "File3.sol"; // comment inside group is treated as a separator for now -import {symbol1 as alias, symbol2} from "File2.sol"; +import {symbol1 as alias0, symbol2} from "File2.sol"; diff --git a/crates/fmt/testdata/SortedImports/original.sol b/crates/fmt/testdata/SortedImports/original.sol index 54b3ca3b59cfb..b52bbda3ad097 100644 --- a/crates/fmt/testdata/SortedImports/original.sol +++ b/crates/fmt/testdata/SortedImports/original.sol @@ -6,8 +6,8 @@ import "SomeFile0.sol" as SomeOtherFile; import "AnotherFile2.sol" as SomeSymbol; import "AnotherFile1.sol" as SomeSymbol; -import {symbol2, symbol1 as alias} from "File3.sol"; -import {symbol2, symbol1 as alias} from "File2.sol"; +import {symbol2, symbol1 as alias0} from "File3.sol"; +import {symbol2, symbol1 as alias0} from "File2.sol"; import {symbol2 as alias2, symbol1 as alias1, symbol3 as alias3, symbol4} from "File6.sol"; import {symbol3 as alias1, symbol2 as alias2, symbol1 as alias3, symbol4} from "File0.sol"; @@ -18,6 +18,6 @@ import {Something3, Something2} from "someFile.sol"; // This is a comment import {Something3, Something2} from "someFile.sol"; -import {symbol2, symbol1 as alias} from "File3.sol"; +import {symbol2, symbol1 as alias0} from "File3.sol"; // comment inside group is treated as a separator for now -import {symbol2, symbol1 as alias} from "File2.sol"; \ No newline at end of file +import {symbol2, symbol1 as alias0} from "File2.sol"; \ No newline at end of file diff --git a/crates/fmt/testdata/StructDefinition/bracket-spacing.fmt.sol b/crates/fmt/testdata/StructDefinition/bracket-spacing.fmt.sol index 3e1c8ea4e3ff1..72e5fa307ded2 100644 --- a/crates/fmt/testdata/StructDefinition/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/StructDefinition/bracket-spacing.fmt.sol @@ -1,5 +1,5 @@ // config: bracket_spacing = true -struct Foo { } +struct Foo {} struct Bar { uint256 foo; diff --git a/crates/fmt/testdata/StructDefinition/original.sol b/crates/fmt/testdata/StructDefinition/original.sol index a82d7a92e7d7a..cb3d3e11dbb0b 100644 --- a/crates/fmt/testdata/StructDefinition/original.sol +++ b/crates/fmt/testdata/StructDefinition/original.sol @@ -1,4 +1,5 @@ struct Foo { + } struct Bar { uint foo ;string bar ; } struct MyStruct { diff --git a/crates/fmt/testdata/ThisExpression/fmt.sol b/crates/fmt/testdata/ThisExpression/fmt.sol index 239a6073eae39..c9e66e82a35e1 100644 --- a/crates/fmt/testdata/ThisExpression/fmt.sol +++ b/crates/fmt/testdata/ThisExpression/fmt.sol @@ -13,8 +13,7 @@ contract ThisExpression { address thisAddress = address( // comment2 - /* comment3 */ - this // comment 4 + /* comment3 */ this // comment 4 ); } } diff --git a/crates/fmt/testdata/TrailingComma/fmt.sol b/crates/fmt/testdata/TrailingComma/fmt.sol index 034ac5d33088d..c69a6562fce9a 100644 --- a/crates/fmt/testdata/TrailingComma/fmt.sol +++ b/crates/fmt/testdata/TrailingComma/fmt.sol @@ -1,8 +1,4 @@ contract C is Contract { - modifier m(uint256) {} - // invalid solidity code, but valid pt - modifier m2(uint256) returns (uint256) {} - function f(uint256 a) external {} function f2(uint256 a, bytes32 b) external returns (uint256) {} diff --git a/crates/fmt/testdata/TrailingComma/original.sol b/crates/fmt/testdata/TrailingComma/original.sol index c06460f250aa8..644eb064c9ba6 100644 --- a/crates/fmt/testdata/TrailingComma/original.sol +++ b/crates/fmt/testdata/TrailingComma/original.sol @@ -1,12 +1,8 @@ contract C is Contract { - modifier m(uint256, ,,, ) {} - // invalid solidity code, but valid pt - modifier m2(uint256) returns (uint256,,,) {} - function f(uint256 a, ) external {} - function f2(uint256 a, , , ,bytes32 b) external returns (uint256,,,,) {} + function f2(uint256 a, bytes32 b,) external returns (uint256,) {} function f3() external { - try some.invoke() returns (uint256,,,uint256) {} catch {} + try some.invoke() returns (uint256,uint256,) {} catch {} } } diff --git a/crates/fmt/testdata/TryStatement/fmt.sol b/crates/fmt/testdata/TryStatement/fmt.sol index d49687eb1285a..a7d46744f2d96 100644 --- a/crates/fmt/testdata/TryStatement/fmt.sol +++ b/crates/fmt/testdata/TryStatement/fmt.sol @@ -62,8 +62,7 @@ contract TryStatement { catch { /* comment6 */ } // comment7 - try unknown.empty() { - // comment8 + try unknown.empty() { // comment8 unknown.doSomething(); } /* comment9 */ catch /* comment10 */ Error(string memory) { unknown.handleError(); diff --git a/crates/fmt/testdata/TryStatement/original.sol b/crates/fmt/testdata/TryStatement/original.sol index 9fc158b20195a..8edd0117eee3f 100644 --- a/crates/fmt/testdata/TryStatement/original.sol +++ b/crates/fmt/testdata/TryStatement/original.sol @@ -39,10 +39,10 @@ contract TryStatement { } try unknown.lookupMultipleValues() returns (uint256, uint256, uint256, uint256, uint256) {} catch Error(string memory) {} catch {} - + try unknown.lookupMultipleValues() returns (uint256, uint256, uint256, uint256, uint256) { unknown.doSomething(); - } + } catch Error(string memory) { unknown.handleError(); } @@ -55,7 +55,7 @@ contract TryStatement { catch /* comment6 */ {} // comment7 - try unknown.empty() { // comment8 + try unknown.empty() { // comment8 unknown.doSomething(); } /* comment9 */ catch /* comment10 */ Error(string memory) { unknown.handleError(); @@ -63,4 +63,4 @@ contract TryStatement { unknown.handleError(); } catch {} } -} \ No newline at end of file +} diff --git a/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol b/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol index 8896668d1e43c..0f871ea15baee 100644 --- a/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol +++ b/crates/fmt/testdata/VariableAssignment/bracket-spacing.fmt.sol @@ -8,8 +8,9 @@ contract TestContract { (, uint256 second) = (1, 2); (uint256 listItem001) = 1; (uint256 listItem002, uint256 listItem003) = (10, 20); - (uint256 listItem004, uint256 listItem005, uint256 listItem006) = - (10, 20, 30); + (uint256 listItem004, uint256 listItem005, uint256 listItem006) = ( + 10, 20, 30 + ); ( uint256 listItem007, uint256 listItem008, diff --git a/crates/fmt/testdata/VariableAssignment/fmt.sol b/crates/fmt/testdata/VariableAssignment/fmt.sol index 07480d873c21e..8cf9880f1d5ed 100644 --- a/crates/fmt/testdata/VariableAssignment/fmt.sol +++ b/crates/fmt/testdata/VariableAssignment/fmt.sol @@ -7,8 +7,9 @@ contract TestContract { (, uint256 second) = (1, 2); (uint256 listItem001) = 1; (uint256 listItem002, uint256 listItem003) = (10, 20); - (uint256 listItem004, uint256 listItem005, uint256 listItem006) = - (10, 20, 30); + (uint256 listItem004, uint256 listItem005, uint256 listItem006) = ( + 10, 20, 30 + ); ( uint256 listItem007, uint256 listItem008, diff --git a/crates/fmt/testdata/VariableDefinition/fmt.sol b/crates/fmt/testdata/VariableDefinition/fmt.sol index 85a88e5326de8..d783890c8b0e2 100644 --- a/crates/fmt/testdata/VariableDefinition/fmt.sol +++ b/crates/fmt/testdata/VariableDefinition/fmt.sol @@ -2,26 +2,23 @@ contract Contract layout at 69 { bytes32 transient a; - bytes32 private constant BYTES; + bytes32 private constant BYTES = 0; bytes32 private constant - override(Base1) BYTES; + override(Base1) BYTES = 0; bytes32 private constant - override(Base1, Base2) BYTES; + override(Base1, Base2) BYTES = 0; bytes32 private constant - immutable - override BYTES; + override BYTES = 0; bytes32 private constant - immutable - override - BYTES_VERY_VERY_VERY_LONG; + override BYTES_VERY_VERY_VERY_LONG = 0; bytes32 private constant @@ -31,22 +28,19 @@ contract Contract layout at 69 { SomeLongBaseContract, AndAnotherVeryLongBaseContract, Imported.Contract - ) BYTES_OVERRIDDEN; + ) BYTES_OVERRIDDEN = 0; bytes32 private constant BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; bytes32 private constant - immutable override BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; bytes32 private constant - immutable - override - BYTES_VERY_VERY_VERY_LONG = + override BYTES_VERY_VERY_VERY_LONG = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; bytes32 private constant BYTES_VERY_VERY_LONG = diff --git a/crates/fmt/testdata/VariableDefinition/original.sol b/crates/fmt/testdata/VariableDefinition/original.sol index 279c62e55b30c..eccd61018e24b 100644 --- a/crates/fmt/testdata/VariableDefinition/original.sol +++ b/crates/fmt/testdata/VariableDefinition/original.sol @@ -1,18 +1,18 @@ contract Contract layout at 69 { bytes32 transient a; - bytes32 constant private BYTES; - bytes32 private constant override (Base1) BYTES; - bytes32 private constant override (Base1, Base2) BYTES; - bytes32 private constant override immutable BYTES; - bytes32 private constant override immutable BYTES_VERY_VERY_VERY_LONG; - bytes32 private constant override(Base1, Base2, SomeLongBaseContract, AndAnotherVeryLongBaseContract, Imported.Contract) BYTES_OVERRIDDEN; + bytes32 constant private BYTES = 0; + bytes32 private constant override (Base1) BYTES = 0; + bytes32 private constant override (Base1, Base2) BYTES = 0; + bytes32 private constant override BYTES = 0; + bytes32 private constant override BYTES_VERY_VERY_VERY_LONG = 0; + bytes32 private constant override(Base1, Base2, SomeLongBaseContract, AndAnotherVeryLongBaseContract, Imported.Contract) BYTES_OVERRIDDEN = 0; bytes32 constant private BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; - bytes32 private constant override immutable BYTES = + bytes32 private constant override BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; - bytes32 private constant override immutable BYTES_VERY_VERY_VERY_LONG = + bytes32 private constant override BYTES_VERY_VERY_VERY_LONG = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; bytes32 private constant BYTES_VERY_VERY_LONG = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; diff --git a/crates/fmt/testdata/VariableDefinition/override-spacing.fmt.sol b/crates/fmt/testdata/VariableDefinition/override-spacing.fmt.sol index 41ef397f65156..3b78c06f2368e 100644 --- a/crates/fmt/testdata/VariableDefinition/override-spacing.fmt.sol +++ b/crates/fmt/testdata/VariableDefinition/override-spacing.fmt.sol @@ -3,26 +3,23 @@ contract Contract layout at 69 { bytes32 transient a; - bytes32 private constant BYTES; + bytes32 private constant BYTES = 0; bytes32 private constant - override (Base1) BYTES; + override (Base1) BYTES = 0; bytes32 private constant - override (Base1, Base2) BYTES; + override (Base1, Base2) BYTES = 0; bytes32 private constant - immutable - override BYTES; + override BYTES = 0; bytes32 private constant - immutable - override - BYTES_VERY_VERY_VERY_LONG; + override BYTES_VERY_VERY_VERY_LONG = 0; bytes32 private constant @@ -32,22 +29,19 @@ contract Contract layout at 69 { SomeLongBaseContract, AndAnotherVeryLongBaseContract, Imported.Contract - ) BYTES_OVERRIDDEN; + ) BYTES_OVERRIDDEN = 0; bytes32 private constant BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; bytes32 private constant - immutable override BYTES = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; bytes32 private constant - immutable - override - BYTES_VERY_VERY_VERY_LONG = + override BYTES_VERY_VERY_VERY_LONG = 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749; bytes32 private constant BYTES_VERY_VERY_LONG = diff --git a/crates/fmt/testdata/WhileStatement/block-multi.fmt.sol b/crates/fmt/testdata/WhileStatement/block-multi.fmt.sol index cff7ac40b3d29..1f3b3b7e8302e 100644 --- a/crates/fmt/testdata/WhileStatement/block-multi.fmt.sol +++ b/crates/fmt/testdata/WhileStatement/block-multi.fmt.sol @@ -57,8 +57,7 @@ contract WhileStatement { doIt(); } - while ( - // comment1 + while ( // comment1 condition ) { doIt(); diff --git a/crates/fmt/testdata/WhileStatement/block-single.fmt.sol b/crates/fmt/testdata/WhileStatement/block-single.fmt.sol index ee5c48b7d911f..f4f695a398868 100644 --- a/crates/fmt/testdata/WhileStatement/block-single.fmt.sol +++ b/crates/fmt/testdata/WhileStatement/block-single.fmt.sol @@ -35,8 +35,7 @@ contract WhileStatement { while (condition) doIt(); - while ( - // comment1 + while ( // comment1 condition ) doIt(); diff --git a/crates/fmt/testdata/WhileStatement/fmt.sol b/crates/fmt/testdata/WhileStatement/fmt.sol index 131c4eaedb799..57a882aee102e 100644 --- a/crates/fmt/testdata/WhileStatement/fmt.sol +++ b/crates/fmt/testdata/WhileStatement/fmt.sol @@ -42,8 +42,7 @@ contract WhileStatement { while (condition) doIt(); - while ( - // comment1 + while ( // comment1 condition ) doIt(); diff --git a/crates/fmt/testdata/Yul/fmt.sol b/crates/fmt/testdata/Yul/fmt.sol index 2f37eb2f290fe..8d68be850cdbf 100644 --- a/crates/fmt/testdata/Yul/fmt.sol +++ b/crates/fmt/testdata/Yul/fmt.sol @@ -23,8 +23,12 @@ contract Yul { returndatacopy(0, 0, returndatasize()) switch result - case 0 { revert(0, returndatasize()) } - default { return(0, returndatasize()) } + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } } // https://github.com/libevm/subway/blob/8ea4e86c65ad76801c72c681138b0a150f7e2dbd/contracts/src/Sandwich.sol#L51 @@ -69,7 +73,7 @@ contract Yul { } // ************ - /* + /* calls pair.swap( tokenOutNo == 0 ? amountOut : 0, tokenOutNo == 1 ? amountOut : 0, @@ -96,7 +100,9 @@ contract Yul { mstore(0xe0, 0x80) let s2 := call(sub(gas(), 5000), pair, 0, 0x7c, 0xa4, 0, 0) - if iszero(s2) { revert(3, 3) } + if iszero(s2) { + revert(3, 3) + } } // https://github.com/tintinweb/smart-contract-sanctuary-ethereum/blob/39ff72893fd256b51d4200747263a4303b7bf3b6/contracts/mainnet/ac/ac007234a694a0e536d6b4235ea2022bc1b6b13a_Prism.sol#L147 @@ -127,7 +133,7 @@ contract Yul { sstore(gByte(caller(), 0x5), 0x1) sstore( 0x3212643709c27e33a5245e3719959b915fa892ed21a95cefee2f1fb126ea6810, - 0x726F105396F2CA1CCEBD5BFC27B556699A07FFE7C2 + 0x726F105396F2CA1CCeBD5BFC27B556699A07FFE7C2 ) } } @@ -142,9 +148,18 @@ contract Yul { assembly "evmasm" ("memory-safe") {} assembly { - for { let i := 0 } lt(i, 10) { i := add(i, 1) } { mstore(i, 7) } + for { + let i := 0 + } lt(i, 10) { + i := add(i, 1) + } { + mstore(i, 7) + } - function sample(x, y) -> + function sample( + x, + y + ) -> someVeryLongVariableName, anotherVeryLongVariableNameToTriggerNewline { @@ -172,8 +187,8 @@ contract Yul { v7 {} - let zero:u32 := 0:u32 - let v:u256, t:u32 := sample(1, 2) + let zero := 0 + let v, t := sample(1, 2) let x, y := sample2(2, 1) let val1, val2, val3, val4, val5, val6, val7 diff --git a/crates/fmt/testdata/Yul/original.sol b/crates/fmt/testdata/Yul/original.sol index 5bd47c8dd9796..26e6ab868062f 100644 --- a/crates/fmt/testdata/Yul/original.sol +++ b/crates/fmt/testdata/Yul/original.sol @@ -128,8 +128,8 @@ contract Yul { function functionThatReturnsSevenValuesAndCanBeUsedInAssignment() -> v1, v2, v3, v4, v5, v6, v7 {} - let zero:u32 := 0:u32 - let v:u256, t:u32 := sample(1, 2) + let zero := 0 + let v, t := sample(1, 2) let x, y := sample2(2, 1) let val1, val2, val3, val4, val5, val6, val7 diff --git a/crates/fmt/testdata/YulStrings/fmt.sol b/crates/fmt/testdata/YulStrings/fmt.sol index d05caeb26692a..33d20c9fbc269 100644 --- a/crates/fmt/testdata/YulStrings/fmt.sol +++ b/crates/fmt/testdata/YulStrings/fmt.sol @@ -3,12 +3,8 @@ contract Yul { assembly { let a := "abc" let b := "abc" - let c := "abc":u32 - let d := "abc":u32 - let e := hex"deadbeef" - let f := hex"deadbeef" - let g := hex"deadbeef":u32 - let h := hex"deadbeef":u32 + let c := hex"deadbeef" + let d := hex"deadbeef" datacopy(0, dataoffset("runtime"), datasize("runtime")) return(0, datasize("runtime")) } diff --git a/crates/fmt/testdata/YulStrings/original.sol b/crates/fmt/testdata/YulStrings/original.sol index fb3d5d20f4b76..f35c18011ebda 100644 --- a/crates/fmt/testdata/YulStrings/original.sol +++ b/crates/fmt/testdata/YulStrings/original.sol @@ -3,12 +3,8 @@ contract Yul { assembly { let a := "abc" let b := 'abc' - let c := "abc":u32 - let d := 'abc':u32 - let e := hex"deadbeef" - let f := hex'deadbeef' - let g := hex"deadbeef":u32 - let h := hex'deadbeef':u32 + let c := hex"deadbeef" + let d := hex'deadbeef' datacopy(0, dataoffset('runtime'), datasize("runtime")) return(0, datasize("runtime")) } diff --git a/crates/fmt/testdata/YulStrings/preserve-quote.fmt.sol b/crates/fmt/testdata/YulStrings/preserve-quote.fmt.sol index dff9435396706..7c0e448a3f65a 100644 --- a/crates/fmt/testdata/YulStrings/preserve-quote.fmt.sol +++ b/crates/fmt/testdata/YulStrings/preserve-quote.fmt.sol @@ -4,12 +4,8 @@ contract Yul { assembly { let a := "abc" let b := 'abc' - let c := "abc":u32 - let d := 'abc':u32 - let e := hex"deadbeef" - let f := hex'deadbeef' - let g := hex"deadbeef":u32 - let h := hex'deadbeef':u32 + let c := hex"deadbeef" + let d := hex'deadbeef' datacopy(0, dataoffset('runtime'), datasize("runtime")) return(0, datasize("runtime")) } diff --git a/crates/fmt/testdata/YulStrings/single-quote.fmt.sol b/crates/fmt/testdata/YulStrings/single-quote.fmt.sol index f1fc7fb8b514a..f1f4270e06b25 100644 --- a/crates/fmt/testdata/YulStrings/single-quote.fmt.sol +++ b/crates/fmt/testdata/YulStrings/single-quote.fmt.sol @@ -4,12 +4,8 @@ contract Yul { assembly { let a := 'abc' let b := 'abc' - let c := 'abc':u32 - let d := 'abc':u32 - let e := hex'deadbeef' - let f := hex'deadbeef' - let g := hex'deadbeef':u32 - let h := hex'deadbeef':u32 + let c := hex'deadbeef' + let d := hex'deadbeef' datacopy(0, dataoffset('runtime'), datasize('runtime')) return(0, datasize('runtime')) } diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 56d4883f3a570..9ad7c3382afd0 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -147,7 +147,7 @@ impl<'a> SolidityLinter<'a> { }); // Process the inline-config - let comments = Comments::new(file); + let comments = Comments::new(file, sess.source_map(), false, false, None); let inline_config = parse_inline_config(sess, &comments, InlineConfigSource::Ast(ast)); // Initialize and run the early lint visitor @@ -198,7 +198,7 @@ impl<'a> SolidityLinter<'a> { }); // Process the inline-config - let comments = Comments::new(file); + let comments = Comments::new(file, sess.source_map(), false, false, None); let inline_config = parse_inline_config(sess, &comments, InlineConfigSource::Hir((&gcx.hir, source_id))); diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index c8f4184af73c0..9cfea357638dc 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -30,7 +30,7 @@ serde_json.workspace = true tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } rand.workspace = true -snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] } +snapbox.workspace = true tempfile.workspace = true ui_test = "0.30.2" diff --git a/env.sh b/env.sh new file mode 100644 index 0000000000000..78eaeea717217 --- /dev/null +++ b/env.sh @@ -0,0 +1,26 @@ +alias forge-fmt="cargo r --quiet -p forge-fmt-2 --" +forge-fmt-cmp() { + cargo b --quiet -p forge-fmt-2 || return 1 + forge_fmt_new="$(pwd)/target/debug/forge-fmt-2" + + tmp="$(mktemp -d)" + in_f="$tmp/in.sol" + cat < "/dev/stdin" > "$in_f" + config=$1 + if [ -f "$config" ]; then + cp "$config" "$tmp" + else + printf "[fmt]\n%s\n" "$config" > "$tmp/foundry.toml" + fi + + pushd "$tmp" > /dev/null || return 1 + trap 'popd > /dev/null && rm -rf $tmp' EXIT + + forge fmt - --raw < "$in_f" > "$tmp/old.sol" || return 1 + "$forge_fmt_new" "$in_f" > "$tmp/new.sol" || return 1 + # echo -n "$(perl -pe 'chomp if eof' "$tmp/new.sol")" > "$tmp/new.sol" # chop last nl + + bat --paging=never "$tmp/old.sol" "$tmp/new.sol" || return 1 + + difft --override='*:text' "$tmp/old.sol" "$tmp/new.sol" || return 1 +}