Skip to content

Commit

Permalink
refactor(forge): refactor fmt command and parsing (foundry-rs#5428)
Browse files Browse the repository at this point in the history
* refactor(forge): refactor fmt command and parsing

* chore: clippy
  • Loading branch information
DaniPopes authored Jul 18, 2023
1 parent c426820 commit e41318d
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 277 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
303 changes: 143 additions & 160 deletions cli/src/cmd/forge/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -48,38 +48,6 @@ impl_figment_convert_basic!(FmtArgs);

// === impl FmtArgs ===

impl FmtArgs {
/// Returns all inputs to format
fn inputs(&self, config: &Config) -> Vec<Input> {
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 = ();

Expand All @@ -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 <paths>`");
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 <paths>`"
);
return Ok(())
}
paths
.par_iter()
.map(|path| {
let source = fs::read_to_string(path)?;
format(source, Some(path))
})
.collect()
}
}?;

Ok(None)
})
.collect::<eyre::Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<String>>();

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());
}
}

Expand All @@ -237,24 +183,61 @@ struct Line(Option<usize>);

#[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<PathBuf>),
}

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
}
Loading

0 comments on commit e41318d

Please sign in to comment.