diff --git a/Cargo.lock b/Cargo.lock index 27f29332847b..18d642fae968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,7 +2291,6 @@ dependencies = [ "clap_complete_fig", "color-eyre", "comfy-table", - "console", "criterion", "dialoguer", "dotenvy", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7376d9da7525..acf373da341b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -34,7 +34,6 @@ yansi = "0.5" tracing-error = "0.2" tracing-subscriber = { version = "0.3", features = ["registry", "env-filter", "fmt"] } tracing = "0.1" -console = "0.15" watchexec = "2" is-terminal = "0.4" comfy-table = "6" diff --git a/cli/src/cmd/forge/fmt.rs b/cli/src/cmd/forge/fmt.rs index ee4d1130133c..e199947e3f1c 100644 --- a/cli/src/cmd/forge/fmt.rs +++ b/cli/src/cmd/forge/fmt.rs @@ -3,20 +3,20 @@ use crate::{ utils::FoundryPathExt, }; use clap::{Parser, ValueHint}; -use console::{style, Style}; use forge_fmt::{format, parse, print_diagnostics_report}; use foundry_common::{fs, term::cli_warn}; -use foundry_config::{impl_figment_convert_basic, Config}; +use foundry_config::impl_figment_convert_basic; use foundry_utils::glob::expand_globs; use rayon::prelude::*; use similar::{ChangeTag, TextDiff}; use std::{ fmt::{self, Write}, io, - io::Read, + io::{Read, Write as _}, path::{Path, PathBuf}, }; use tracing::log::warn; +use yansi::Color; /// CLI arguments for `forge fmt`. #[derive(Debug, Clone, Parser)] @@ -48,38 +48,6 @@ impl_figment_convert_basic!(FmtArgs); // === impl FmtArgs === -impl FmtArgs { - /// Returns all inputs to format - fn inputs(&self, config: &Config) -> Vec { - if self.paths.is_empty() { - return config.project_paths().input_files().into_iter().map(Input::Path).collect() - } - - let mut paths = self.paths.iter().peekable(); - - if let Some(path) = paths.peek() { - let mut stdin = io::stdin(); - if *path == Path::new("-") && !is_terminal::is_terminal(&stdin) { - let mut buf = String::new(); - stdin.read_to_string(&mut buf).expect("Failed to read from stdin"); - return vec![Input::Stdin(buf)] - } - } - - let mut out = Vec::with_capacity(self.paths.len()); - for path in self.paths.iter() { - if path.is_dir() { - out.extend(ethers::solc::utils::source_files(path).into_iter().map(Input::Path)); - } else if path.is_sol() { - out.push(Input::Path(path.to_path_buf())); - } else { - warn!("Cannot process path {}", path.display()); - } - } - out - } -} - impl Cmd for FmtArgs { type Output = (); @@ -90,137 +58,115 @@ impl Cmd for FmtArgs { let ignored = expand_globs(&config.__root.0, config.fmt.ignore.iter())?; let cwd = std::env::current_dir()?; - let mut inputs = vec![]; - for input in self.inputs(&config) { - match input { - Input::Path(p) => { - if (p.is_absolute() && !ignored.contains(&p)) || - !ignored.contains(&cwd.join(&p)) + let input = match &self.paths[..] { + [] => Input::Paths(config.project_paths().input_files_iter().collect()), + [one] if one == Path::new("-") => { + let mut s = String::new(); + io::stdin().read_to_string(&mut s).expect("Failed to read from stdin"); + Input::Stdin(s) + } + paths => { + let mut inputs = Vec::with_capacity(paths.len()); + for path in paths { + if !ignored.is_empty() && + ((path.is_absolute() && ignored.contains(path)) || + ignored.contains(&cwd.join(path))) { - inputs.push(Input::Path(p)); + continue } + + if path.is_dir() { + inputs.extend(ethers::solc::utils::source_files_iter(path)); + } else if path.is_sol() { + inputs.push(path.to_path_buf()); + } else { + warn!("Cannot process path {}", path.display()); + } + } + Input::Paths(inputs) + } + }; + + let format = |source: String, path: Option<&Path>| -> eyre::Result<_> { + let name = match path { + Some(path) => { + path.strip_prefix(&config.__root.0).unwrap_or(path).display().to_string() } - other => inputs.push(other), + None => "stdin".to_string(), }; - } - if inputs.is_empty() { - cli_warn!("Nothing to format.\nHINT: If you are working outside of the project, try providing paths to your source files: `forge fmt `"); - return Ok(()) - } + let parsed = parse(&source).map_err(|diagnostics| { + let _ = print_diagnostics_report(&source, path, diagnostics); + eyre::eyre!("Failed to parse Solidity code for {name}. Leaving source unchanged.") + })?; + + if !parsed.invalid_inline_config_items.is_empty() { + for (loc, warning) in &parsed.invalid_inline_config_items { + let mut lines = source[..loc.start().min(source.len())].split('\n'); + let col = lines.next_back().unwrap().len() + 1; + let row = lines.count() + 1; + cli_warn!("[{}:{}:{}] {}", name, row, col, warning); + } + } - let diffs = inputs - .par_iter() - .map(|input| { - let source = match input { - Input::Path(path) => fs::read_to_string(path)?, - Input::Stdin(source) => source.to_string() - }; + let mut output = String::new(); + format(&mut output, parsed, config.fmt.clone()).unwrap(); - let parsed = match parse(&source) { - Ok(result) => result, - Err(diagnostics) => { - let path = if let Input::Path(path) = input {Some(path)} else {None}; - print_diagnostics_report(&source,path, diagnostics)?; - eyre::bail!( - "Failed to parse Solidity code for {input}. Leaving source unchanged." - ) - } - }; + solang_parser::parse(&output, 0).map_err(|diags| { + eyre::eyre!( + "Failed to construct valid Solidity code for {name}. Leaving source unchanged.\n\ + Debug info: {diags:?}" + ) + })?; - if !parsed.invalid_inline_config_items.is_empty() { - let path = match input { - Input::Path(path) => { - let path = path.strip_prefix(&config.__root.0).unwrap_or(path); - format!("{}", path.display()) - } - Input::Stdin(_) => "stdin".to_string() - }; - for (loc, warning) in &parsed.invalid_inline_config_items { - let mut lines = source[..loc.start().min(source.len())].split('\n'); - let col = lines.next_back().unwrap().len() + 1; - let row = lines.count() + 1; - cli_warn!("[{}:{}:{}] {}", path, row, col, warning); - } + if self.check || path.is_none() { + if self.raw { + print!("{output}"); } - let mut output = String::new(); - format(&mut output, parsed, config.fmt.clone()).unwrap(); - - solang_parser::parse(&output, 0).map_err(|diags| { - eyre::eyre!( - "Failed to construct valid Solidity code for {}. Leaving source unchanged.\n\ - Debug info: {:?}", - input, - diags, - ) - })?; - - if self.check || matches!(input, Input::Stdin(_)) { - if self.raw { - print!("{output}"); - } - - let diff = TextDiff::from_lines(&source, &output); - - if diff.ratio() < 1.0 { - let mut diff_summary = String::new(); - - writeln!(diff_summary, "Diff in {input}:")?; - for (j, group) in diff.grouped_ops(3).iter().enumerate() { - if j > 0 { - writeln!(diff_summary, "{:-^1$}", "-", 80)?; - } - for op in group { - for change in diff.iter_inline_changes(op) { - let (sign, s) = match change.tag() { - ChangeTag::Delete => ("-", Style::new().red()), - ChangeTag::Insert => ("+", Style::new().green()), - ChangeTag::Equal => (" ", Style::new().dim()), - }; - write!( - diff_summary, - "{}{} |{}", - style(Line(change.old_index())).dim(), - style(Line(change.new_index())).dim(), - s.apply_to(sign).bold(), - )?; - for (emphasized, value) in change.iter_strings_lossy() { - if emphasized { - write!(diff_summary, "{}", s.apply_to(value).underlined().on_black())?; - } else { - write!(diff_summary, "{}", s.apply_to(value))?; - } - } - if change.missing_newline() { - writeln!(diff_summary)?; - } - } - } - } - - return Ok(Some(diff_summary)) - } - } else if let Input::Path(path) = input { - fs::write(path, output)?; + let diff = TextDiff::from_lines(&source, &output); + if diff.ratio() < 1.0 { + return Ok(Some(format_diff_summary(&name, &diff))) } + } else if let Some(path) = path { + fs::write(path, output)?; + } + Ok(None) + }; + + let diffs = match input { + Input::Stdin(source) => format(source, None).map(|diff| vec![diff]), + Input::Paths(paths) => { + if paths.is_empty() { + cli_warn!( + "Nothing to format.\n\ + HINT: If you are working outside of the project, \ + try providing paths to your source files: `forge fmt `" + ); + return Ok(()) + } + paths + .par_iter() + .map(|path| { + let source = fs::read_to_string(path)?; + format(source, Some(path)) + }) + .collect() + } + }?; - Ok(None) - }) - .collect::>>()? - .into_iter() - .flatten() - .collect::>(); - - if !diffs.is_empty() { + let mut diffs = diffs.iter().flatten(); + if let Some(first) = diffs.next() { // This branch is only reachable with stdin or --check if !self.raw { - for (i, diff) in diffs.iter().enumerate() { + let mut stdout = io::stdout().lock(); + let first = std::iter::once(first); + for (i, diff) in first.chain(diffs).enumerate() { if i > 0 { - println!(); + let _ = stdout.write_all(b"\n"); } - print!("{diff}"); + let _ = stdout.write_all(diff.as_bytes()); } } @@ -237,24 +183,61 @@ struct Line(Option); #[derive(Debug)] enum Input { - Path(PathBuf), Stdin(String), -} - -impl fmt::Display for Input { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Input::Path(path) => write!(f, "{}", path.display()), - Input::Stdin(_) => write!(f, "stdin"), - } - } + Paths(Vec), } impl fmt::Display for Line { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.0 { - None => write!(f, " "), + None => f.write_str(" "), Some(idx) => write!(f, "{:<4}", idx + 1), } } } + +fn format_diff_summary<'a, 'b, 'r>(name: &str, diff: &'r TextDiff<'a, 'b, '_, str>) -> String +where + 'r: 'a + 'b, +{ + let cap = 128; + let mut diff_summary = String::with_capacity(cap); + + let _ = writeln!(diff_summary, "Diff in {name}:"); + for (j, group) in diff.grouped_ops(3).into_iter().enumerate() { + if j > 0 { + let s = + "--------------------------------------------------------------------------------"; + diff_summary.push_str(s); + } + for op in group { + for change in diff.iter_inline_changes(&op) { + let dimmed = Color::Default.style().dimmed(); + let (sign, s) = match change.tag() { + ChangeTag::Delete => ("-", Color::Red.style()), + ChangeTag::Insert => ("+", Color::Green.style()), + ChangeTag::Equal => (" ", dimmed), + }; + + let _ = write!( + diff_summary, + "{}{} |{}", + dimmed.paint(Line(change.old_index())), + dimmed.paint(Line(change.new_index())), + s.bold().paint(sign), + ); + + for (emphasized, value) in change.iter_strings_lossy() { + let s = if emphasized { s.underline().bg(Color::Black) } else { s }; + let _ = write!(diff_summary, "{}", s.paint(value)); + } + + if change.missing_newline() { + diff_summary.push('\n'); + } + } + } + } + + diff_summary +} diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 23d1e2c20d66..1caaa694553b 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -1,4 +1,3 @@ -use console::Emoji; use ethers::{ abi::token::{LenientTokenizer, Tokenizer}, prelude::TransactionReceipt, @@ -203,40 +202,33 @@ pub fn enable_paint() { /// Prints parts of the receipt to stdout pub fn print_receipt(chain: Chain, receipt: &TransactionReceipt) { - let contract_address = receipt - .contract_address - .map(|addr| format!("\nContract Address: {}", to_checksum(&addr, None))) - .unwrap_or_default(); - let gas_used = receipt.gas_used.unwrap_or_default(); let gas_price = receipt.effective_gas_price.unwrap_or_default(); - - let gas_details = if gas_price.is_zero() { - format!("Gas Used: {gas_used}") - } else { - let paid = format_units(gas_used.mul(gas_price), 18).unwrap_or_else(|_| "N/A".into()); - let gas_price = format_units(gas_price, 9).unwrap_or_else(|_| "N/A".into()); - format!( - "Paid: {} ETH ({gas_used} gas * {} gwei)", - paid.trim_end_matches('0'), - gas_price.trim_end_matches('0').trim_end_matches('.') - ) - }; - - let check = if receipt.status.unwrap_or_default().is_zero() { - Emoji("❌ ", " [Failed] ") - } else { - Emoji("✅ ", " [Success] ") - }; - println!( - "\n##### {}\n{}Hash: 0x{}{}\nBlock: {}\n{}\n", - chain, - check, - hex::encode(receipt.transaction_hash.as_bytes()), - contract_address, - receipt.block_number.unwrap_or_default(), - gas_details + "\n##### {chain}\n{status}Hash: {tx_hash:?}{caddr}\nBlock: {bn}\n{gas}\n", + status = if receipt.status.map_or(true, |s| s.is_zero()) { + "❌ [Failed]" + } else { + "✅ [Success]" + }, + tx_hash = receipt.transaction_hash, + caddr = if let Some(addr) = &receipt.contract_address { + format!("\nContract Address: {}", to_checksum(addr, None)) + } else { + String::new() + }, + bn = receipt.block_number.unwrap_or_default(), + gas = if gas_price.is_zero() { + format!("Gas Used: {gas_used}") + } else { + let paid = format_units(gas_used.mul(gas_price), 18).unwrap_or_else(|_| "N/A".into()); + let gas_price = format_units(gas_price, 9).unwrap_or_else(|_| "N/A".into()); + format!( + "Paid: {} ETH ({gas_used} gas * {} gwei)", + paid.trim_end_matches('0'), + gas_price.trim_end_matches('0').trim_end_matches('.') + ) + }, ); } diff --git a/fmt/src/comments.rs b/fmt/src/comments.rs index 5ee62d38bb2a..a76c64ffc53a 100644 --- a/fmt/src/comments.rs +++ b/fmt/src/comments.rs @@ -86,74 +86,63 @@ impl CommentWithMetadata { let this_line = if src_before.ends_with('\n') { "" } else { lines_before.next().unwrap_or_default() }; let indent_len = this_line.chars().take_while(|c| c.is_whitespace()).count(); - let last_line = lines_before.next(); + let last_line = lines_before.next().map(str::trim_start); if matches!(comment, Comment::DocLine(..) | Comment::DocBlock(..)) { return Self::new( comment, CommentPosition::Prefix, - last_line.unwrap_or_default().trim_start().is_empty(), + last_line.map_or(true, str::is_empty), indent_len, ) } - let code_end = src_before - .comment_state_char_indices() - .filter_map(|(state, idx, ch)| { - if matches!(state, CommentState::None) && !ch.is_whitespace() { - Some(idx) + // TODO: this loop takes almost the entirety of the time spent in parsing, which is up to + // 80% of `crate::fmt` + let mut code_end = 0; + for (state, idx, ch) in src_before.comment_state_char_indices() { + if matches!(state, CommentState::None) && !ch.is_whitespace() { + code_end = idx; + } + } + + let (position, has_newline_before) = if src_before[code_end..].contains('\n') { + // comment sits on a line without code + if let Some(last_line) = last_line { + if last_line.is_empty() { + // line before is empty + (CommentPosition::Prefix, true) } else { - None - } - }) - .last() - .unwrap_or_default(); - - let (position, has_newline_before) = { - if src_before[code_end..].contains('\n') { - // comment sits on a line without code - if let Some(last_line) = last_line { - if last_line.trim_start().is_empty() { - // line before is empty - (CommentPosition::Prefix, true) - } else { - // line has something - // check if the last comment after code was a postfix comment - if last_comment - .filter(|last_comment| { - last_comment.loc.end() > code_end && !last_comment.is_prefix() - }) - .is_some() - { - // get the indent size of the next item of code - let next_indent_len = src[comment.loc().end()..] - .non_comment_chars() - .take_while(|ch| ch.is_whitespace()) - .fold( - indent_len, - |indent, ch| if ch == '\n' { 0 } else { indent + 1 }, - ); - if indent_len > next_indent_len { - // the comment indent is bigger than the next code indent - (CommentPosition::Postfix, false) - } else { - // the comment indent is equal to or less than the next code - // indent - (CommentPosition::Prefix, false) - } + // line has something + // check if the last comment after code was a postfix comment + if last_comment + .map_or(false, |last| last.loc.end() > code_end && !last.is_prefix()) + { + // get the indent size of the next item of code + let next_indent_len = src[comment.loc().end()..] + .non_comment_chars() + .take_while(|ch| ch.is_whitespace()) + .fold(indent_len, |indent, ch| if ch == '\n' { 0 } else { indent + 1 }); + if indent_len > next_indent_len { + // the comment indent is bigger than the next code indent + (CommentPosition::Postfix, false) } else { - // if there is no postfix comment after the piece of code + // the comment indent is equal to or less than the next code + // indent (CommentPosition::Prefix, false) } + } else { + // if there is no postfix comment after the piece of code + (CommentPosition::Prefix, false) } - } else { - // beginning of file - (CommentPosition::Prefix, false) } } else { - // comment is after some code - (CommentPosition::Postfix, false) + // beginning of file + (CommentPosition::Prefix, false) } + } else { + // comment is after some code + (CommentPosition::Postfix, false) }; Self::new(comment, position, has_newline_before, indent_len) @@ -171,15 +160,23 @@ impl CommentWithMetadata { self.loc.start() < byte } + /// Returns the contents of the comment without the start and end tokens pub fn contents(&self) -> &str { - self.comment - .strip_prefix(self.start_token()) - .map(|c| self.end_token().and_then(|end| c.strip_suffix(end)).unwrap_or(c)) - .unwrap_or(&self.comment) + let mut s = self.comment.as_str(); + if let Some(stripped) = s.strip_prefix(self.start_token()) { + s = stripped; + } + if let Some(end_token) = self.end_token() { + if let Some(stripped) = s.strip_suffix(end_token) { + s = stripped; + } + } + s } /// The start token of the comment - pub fn start_token(&self) -> &str { + #[inline] + pub const fn start_token(&self) -> &'static str { match self.ty { CommentType::Line => "//", CommentType::Block => "/*", @@ -190,7 +187,8 @@ impl CommentWithMetadata { /// The token that gets written on the newline when the /// comment is wrapped - pub fn wrap_token(&self) -> &str { + #[inline] + pub const fn wrap_token(&self) -> &'static str { match self.ty { CommentType::Line => "// ", CommentType::DocLine => "/// ", @@ -200,7 +198,8 @@ impl CommentWithMetadata { } /// The end token of the comment - pub fn end_token(&self) -> Option<&str> { + #[inline] + pub const fn end_token(&self) -> Option<&'static str> { match self.ty { CommentType::Line | CommentType::DocLine => None, CommentType::Block | CommentType::DocBlock => Some("*/"), @@ -217,20 +216,16 @@ pub struct Comments { impl Comments { pub fn new(mut comments: Vec, src: &str) -> Self { - let mut prefixes = VecDeque::new(); - let mut postfixes = VecDeque::new(); + let mut prefixes = VecDeque::with_capacity(comments.len()); + let mut postfixes = VecDeque::with_capacity(comments.len()); let mut last_comment = None; comments.sort_by_key(|comment| comment.loc()); for comment in comments { - let comment = - CommentWithMetadata::from_comment_and_src(comment, src, last_comment.as_ref()); - last_comment = Some(comment.clone()); - if comment.is_prefix() { - prefixes.push_back(comment) - } else { - postfixes.push_back(comment) - } + let comment = CommentWithMetadata::from_comment_and_src(comment, src, last_comment); + let vec = if comment.is_prefix() { &mut prefixes } else { &mut postfixes }; + vec.push_back(comment); + last_comment = Some(vec.back().unwrap()); } Self { prefixes, postfixes } } @@ -326,36 +321,42 @@ pub enum CommentState { /// An Iterator over characters and indices in a string slice with information about the state of /// comments pub struct CommentStateCharIndices<'a> { - iter: std::iter::Peekable>, + iter: std::str::CharIndices<'a>, state: CommentState, } impl<'a> CommentStateCharIndices<'a> { + #[inline] fn new(string: &'a str) -> Self { - Self { iter: string.char_indices().peekable(), state: CommentState::None } + Self { iter: string.char_indices(), state: CommentState::None } } + + #[inline] pub fn with_state(mut self, state: CommentState) -> Self { self.state = state; self } + + #[inline] + pub fn peek(&mut self) -> Option<(usize, char)> { + self.iter.clone().next() + } } -impl<'a> Iterator for CommentStateCharIndices<'a> { +impl Iterator for CommentStateCharIndices<'_> { type Item = (CommentState, usize, char); + + #[inline] fn next(&mut self) -> Option { let (idx, ch) = self.iter.next()?; match self.state { CommentState::None => { if ch == '/' { - match self.iter.peek() { - Some((_, '/')) => { - self.state = CommentState::LineStart1; - } - Some((_, '*')) => { - self.state = CommentState::BlockStart1; - } - _ => {} - } + self.state = match self.peek() { + Some((_, '/')) => CommentState::LineStart1, + Some((_, '*')) => CommentState::BlockStart1, + _ => CommentState::None, + }; } } CommentState::LineStart1 => { @@ -377,7 +378,7 @@ impl<'a> Iterator for CommentStateCharIndices<'a> { } CommentState::Block => { if ch == '*' { - if let Some((_, '/')) = self.iter.peek() { + if let Some((_, '/')) = self.peek() { self.state = CommentState::BlockEnd1; } } @@ -391,13 +392,27 @@ impl<'a> Iterator for CommentStateCharIndices<'a> { } Some((self.state, idx, ch)) } + + #[inline] + fn count(self) -> usize { + self.iter.count() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } } +impl std::iter::FusedIterator for CommentStateCharIndices<'_> {} + /// An Iterator over characters in a string slice which are not a apart of comments pub struct NonCommentChars<'a>(CommentStateCharIndices<'a>); impl<'a> Iterator for NonCommentChars<'a> { type Item = char; + + #[inline] fn next(&mut self) -> Option { for (state, _, ch) in self.0.by_ref() { if state == CommentState::None { @@ -411,9 +426,13 @@ impl<'a> Iterator for NonCommentChars<'a> { /// Helpers for iterating over comment containing strings pub trait CommentStringExt { fn comment_state_char_indices(&self) -> CommentStateCharIndices; + + #[inline] fn non_comment_chars(&self) -> NonCommentChars { NonCommentChars(self.comment_state_char_indices()) } + + #[inline] fn trim_comments(&self) -> String { self.non_comment_chars().collect() } @@ -423,12 +442,14 @@ impl CommentStringExt for T where T: AsRef, { + #[inline] fn comment_state_char_indices(&self) -> CommentStateCharIndices { CommentStateCharIndices::new(self.as_ref()) } } impl CommentStringExt for str { + #[inline] fn comment_state_char_indices(&self) -> CommentStateCharIndices { CommentStateCharIndices::new(self) } diff --git a/fmt/src/helpers.rs b/fmt/src/helpers.rs index 49b04c0a484e..d419b88a9d4a 100644 --- a/fmt/src/helpers.rs +++ b/fmt/src/helpers.rs @@ -5,7 +5,7 @@ use crate::{ use ariadne::{Color, Fmt, Label, Report, ReportKind, Source}; use itertools::Itertools; use solang_parser::{diagnostics::Diagnostic, pt::*}; -use std::{fmt::Write, path::PathBuf}; +use std::{fmt::Write, path::Path}; /// Result of parsing the source code #[derive(Debug)] @@ -75,7 +75,7 @@ pub fn offset_to_line_column(content: &str, start: usize) -> (usize, usize) { /// Print the report of parser's diagnostics pub fn print_diagnostics_report( content: &str, - path: Option<&PathBuf>, + path: Option<&Path>, diagnostics: Vec, ) -> std::io::Result<()> { let filename =