1use super::watch::WatchArgs;
2use clap::{Parser, ValueHint};
3use eyre::{Context, Result};
4use forge_fmt::{format_to, parse};
5use foundry_cli::utils::{FoundryPathExt, LoadConfig};
6use foundry_common::fs;
7use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS};
8use foundry_config::{filter::expand_globs, impl_figment_convert_basic};
9use rayon::prelude::*;
10use similar::{ChangeTag, TextDiff};
11use std::{
12 fmt::{self, Write},
13 io,
14 io::{Read, Write as _},
15 path::{Path, PathBuf},
16};
17use yansi::{Color, Paint, Style};
18
19#[derive(Clone, Debug, Parser)]
21pub struct FmtArgs {
22 #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
24 paths: Vec<PathBuf>,
25
26 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
31 root: Option<PathBuf>,
32
33 #[arg(long)]
38 check: bool,
39
40 #[arg(long, short)]
42 raw: bool,
43
44 #[command(flatten)]
45 pub watch: WatchArgs,
46}
47
48impl_figment_convert_basic!(FmtArgs);
49
50impl FmtArgs {
51 pub fn run(self) -> Result<()> {
52 let config = self.load_config()?;
53
54 let ignored = expand_globs(&config.root, config.fmt.ignore.iter())?
56 .iter()
57 .flat_map(foundry_common::fs::canonicalize_path)
58 .collect::<Vec<_>>();
59
60 let cwd = std::env::current_dir()?;
61 let input = match &self.paths[..] {
62 [] => {
63 let project_paths: Vec<PathBuf> = config
65 .project_paths::<SolcLanguage>()
66 .input_files_iter()
67 .filter(|p| !(ignored.contains(p) || ignored.contains(&cwd.join(p))))
68 .collect();
69 Input::Paths(project_paths)
70 }
71 [one] if one == Path::new("-") => {
72 let mut s = String::new();
73 io::stdin().read_to_string(&mut s).expect("Failed to read from stdin");
74 Input::Stdin(s)
75 }
76 paths => {
77 let mut inputs = Vec::with_capacity(paths.len());
78 for path in paths {
79 if !ignored.is_empty() &&
80 ((path.is_absolute() && ignored.contains(path)) ||
81 ignored.contains(&cwd.join(path)))
82 {
83 continue
84 }
85
86 if path.is_dir() {
87 inputs.extend(foundry_compilers::utils::source_files_iter(
88 path,
89 SOLC_EXTENSIONS,
90 ));
91 } else if path.is_sol() {
92 inputs.push(path.to_path_buf());
93 } else {
94 warn!("Cannot process path {}", path.display());
95 }
96 }
97 Input::Paths(inputs)
98 }
99 };
100
101 let format = |source: String, path: Option<&Path>| -> Result<_> {
102 let name = match path {
103 Some(path) => path.strip_prefix(&config.root).unwrap_or(path).display().to_string(),
104 None => "stdin".to_string(),
105 };
106
107 let parsed = parse(&source).wrap_err_with(|| {
108 format!("Failed to parse Solidity code for {name}. Leaving source unchanged.")
109 })?;
110
111 if !parsed.invalid_inline_config_items.is_empty() {
112 for (loc, warning) in &parsed.invalid_inline_config_items {
113 let mut lines = source[..loc.start().min(source.len())].split('\n');
114 let col = lines.next_back().unwrap().len() + 1;
115 let row = lines.count() + 1;
116 sh_warn!("[{}:{}:{}] {}", name, row, col, warning)?;
117 }
118 }
119
120 let mut output = String::new();
121 format_to(&mut output, parsed, config.fmt.clone()).unwrap();
122
123 solang_parser::parse(&output, 0).map_err(|diags| {
124 eyre::eyre!(
125 "Failed to construct valid Solidity code for {name}. Leaving source unchanged.\n\
126 Debug info: {diags:?}"
127 )
128 })?;
129
130 let diff = TextDiff::from_lines(&source, &output);
131 let new_format = diff.ratio() < 1.0;
132 if self.check || path.is_none() {
133 if self.raw {
134 sh_print!("{output}")?;
135 }
136
137 if new_format {
139 return Ok(Some(format_diff_summary(&name, &diff)))
140 }
141 } else if let Some(path) = path {
142 if new_format {
144 fs::write(path, output)?;
145 }
146 }
147 Ok(None)
148 };
149
150 let diffs = match input {
151 Input::Stdin(source) => format(source, None).map(|diff| vec![diff]),
152 Input::Paths(paths) => {
153 if paths.is_empty() {
154 sh_warn!(
155 "Nothing to format.\n\
156 HINT: If you are working outside of the project, \
157 try providing paths to your source files: `forge fmt <paths>`"
158 )?;
159 return Ok(())
160 }
161 paths
162 .par_iter()
163 .map(|path| {
164 let source = fs::read_to_string(path)?;
165 format(source, Some(path))
166 })
167 .collect()
168 }
169 }?;
170
171 let mut diffs = diffs.iter().flatten();
172 if let Some(first) = diffs.next() {
173 if !self.raw {
176 let mut stdout = io::stdout().lock();
177 let first = std::iter::once(first);
178 for (i, diff) in first.chain(diffs).enumerate() {
179 if i > 0 {
180 let _ = stdout.write_all(b"\n");
181 }
182 let _ = stdout.write_all(diff.as_bytes());
183 }
184 }
185
186 if self.check {
187 std::process::exit(1);
188 }
189 }
190
191 Ok(())
192 }
193
194 pub fn is_watch(&self) -> bool {
196 self.watch.watch.is_some()
197 }
198}
199
200struct Line(Option<usize>);
201
202#[derive(Debug)]
203enum Input {
204 Stdin(String),
205 Paths(Vec<PathBuf>),
206}
207
208impl fmt::Display for Line {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self.0 {
211 None => f.write_str(" "),
212 Some(idx) => write!(f, "{:<4}", idx + 1),
213 }
214 }
215}
216
217fn format_diff_summary<'a>(name: &str, diff: &'a TextDiff<'a, 'a, '_, str>) -> String {
218 let cap = 128;
219 let mut diff_summary = String::with_capacity(cap);
220
221 let _ = writeln!(diff_summary, "Diff in {name}:");
222 for (j, group) in diff.grouped_ops(3).into_iter().enumerate() {
223 if j > 0 {
224 let s =
225 "--------------------------------------------------------------------------------";
226 diff_summary.push_str(s);
227 }
228 for op in group {
229 for change in diff.iter_inline_changes(&op) {
230 let dimmed = Style::new().dim();
231 let (sign, s) = match change.tag() {
232 ChangeTag::Delete => ("-", Color::Red.foreground()),
233 ChangeTag::Insert => ("+", Color::Green.foreground()),
234 ChangeTag::Equal => (" ", dimmed),
235 };
236
237 let _ = write!(
238 diff_summary,
239 "{}{} |{}",
240 Line(change.old_index()).paint(dimmed),
241 Line(change.new_index()).paint(dimmed),
242 sign.paint(s.bold()),
243 );
244
245 for (emphasized, value) in change.iter_strings_lossy() {
246 let s = if emphasized { s.underline().bg(Color::Black) } else { s };
247 let _ = write!(diff_summary, "{}", value.paint(s));
248 }
249
250 if change.missing_newline() {
251 diff_summary.push('\n');
252 }
253 }
254 }
255 }
256
257 diff_summary
258}