1use super::watch::WatchArgs;
2use clap::{Parser, ValueHint};
3use eyre::Result;
4use foundry_cli::utils::{FoundryPathExt, LoadConfig};
5use foundry_common::{errors::convert_solar_errors, fs};
6use foundry_compilers::{compilers::solc::SolcLanguage, solc::SOLC_EXTENSIONS};
7use foundry_config::{
8 Config, filter::expand_globs, find_project_root, fmt::FormatterConfig,
9 impl_figment_convert_basic,
10};
11use rayon::prelude::*;
12use similar::{ChangeTag, TextDiff};
13use solar::sema::Compiler;
14use std::{
15 collections::{HashMap, hash_map::Entry},
16 fmt::{self, Write},
17 io,
18 io::Write as _,
19 path::{Path, PathBuf},
20 sync::Arc,
21};
22use yansi::{Color, Paint, Style};
23
24#[derive(Clone, Debug, Parser)]
26pub struct FmtArgs {
27 #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
29 paths: Vec<PathBuf>,
30
31 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
36 root: Option<PathBuf>,
37
38 #[arg(long, conflicts_with = "root")]
40 nearest: bool,
41
42 #[arg(long)]
47 check: bool,
48
49 #[arg(long, short)]
51 raw: bool,
52
53 #[command(flatten)]
54 pub watch: WatchArgs,
55}
56
57impl_figment_convert_basic!(FmtArgs);
58
59impl FmtArgs {
60 pub fn run(self) -> Result<()> {
61 if self.nearest {
62 for var in ["FOUNDRY_CONFIG", "FOUNDRY_ROOT", "DAPP_ROOT"] {
63 if std::env::var_os(var).is_some() {
64 eyre::bail!("`--nearest` cannot be used when `{var}` is set");
65 }
66 }
67 }
68 let config = self.load_config()?;
69 let cwd = std::env::current_dir()?;
70
71 let ignored = if self.nearest {
74 Vec::new()
75 } else {
76 expand_globs(&config.root, config.fmt.ignore.iter())?
77 .iter()
78 .flat_map(fs::canonicalize_path)
79 .collect::<Vec<_>>()
80 };
81
82 let libs = expand_globs(&config.root, config.libs.iter().filter_map(|p| p.to_str()))?
84 .iter()
85 .flat_map(fs::canonicalize_path)
86 .collect::<Vec<_>>();
87
88 let is_under_dir = |file_path: &Path, dirs: &[PathBuf]| -> bool {
90 let check_against_dir = |dir: &PathBuf| {
91 file_path.starts_with(dir)
92 || cwd.join(file_path).starts_with(dir)
93 || fs::canonicalize_path(file_path).is_ok_and(|p| p.starts_with(dir))
94 };
95
96 dirs.iter().any(check_against_dir)
97 };
98
99 let mut input = match &self.paths[..] {
100 [] => {
101 let project_paths: Vec<PathBuf> = config
103 .project_paths::<SolcLanguage>()
104 .input_files_iter()
105 .filter(|p| {
106 !((!self.nearest
107 && (ignored.contains(p)
108 || ignored.contains(&cwd.join(p))
109 || is_under_dir(p, &ignored)))
110 || is_under_dir(p, &libs))
111 })
112 .collect();
113 Input::Paths(project_paths)
114 }
115 [one] if one == Path::new("-") => Input::Stdin,
116 paths => {
117 let mut inputs = Vec::with_capacity(paths.len());
118 for path in paths {
119 if !self.nearest
121 && !ignored.is_empty()
122 && ((path.is_absolute() && ignored.contains(path))
123 || ignored.contains(&cwd.join(path)))
124 {
125 continue;
126 }
127
128 if path.is_dir() {
129 let exclude_libs = !is_under_dir(path, &libs);
131 inputs.extend(
132 foundry_compilers::utils::source_files_iter(path, SOLC_EXTENSIONS)
133 .filter(|p| {
134 !((!self.nearest
135 && (ignored.contains(p)
136 || ignored.contains(&cwd.join(p))
137 || is_under_dir(p, &ignored)))
138 || (exclude_libs && is_under_dir(p, &libs)))
139 }),
140 );
141 } else if path.is_sol() {
142 inputs.push(path.clone());
144 } else {
145 warn!("Cannot process path {}", path.display());
146 }
147 }
148 Input::Paths(inputs)
149 }
150 };
151
152 let nearest_fmt_configs = if self.nearest {
153 let Input::Paths(paths) = &mut input else {
154 eyre::bail!("`--nearest` cannot be used with stdin");
155 };
156 let mut root_configs: HashMap<PathBuf, (Arc<FormatterConfig>, Vec<PathBuf>)> =
157 HashMap::new();
158 let mut path_configs = HashMap::new();
159 let mut filtered_paths = Vec::with_capacity(paths.len());
160
161 for path in std::mem::take(paths) {
162 let path = fs::canonicalize_path(path)?;
163 let root = find_project_root(path.parent())?;
164 let (fmt_config, ignored) = match root_configs.entry(root) {
165 Entry::Occupied(entry) => entry.into_mut(),
166 Entry::Vacant(entry) => {
167 let nearest_config = Config::load_with_root(entry.key())?.sanitized();
168 if entry.key() != &config.root {
169 for warning in &nearest_config.warnings {
170 let _ = sh_warn!("{warning}");
171 }
172 }
173 let ignored =
174 expand_globs(&nearest_config.root, nearest_config.fmt.ignore.iter())?
175 .iter()
176 .flat_map(fs::canonicalize_path)
177 .collect();
178 entry.insert((Arc::new(nearest_config.fmt), ignored))
179 }
180 };
181 if ignored.iter().any(|ignored| path.starts_with(ignored)) {
184 continue;
185 }
186 path_configs.insert(path.clone(), fmt_config.clone());
187 filtered_paths.push(path);
188 }
189 *paths = filtered_paths;
190 Some(path_configs)
191 } else {
192 None
193 };
194
195 let mut compiler = Compiler::new(
196 solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(),
197 );
198
199 compiler.enter_mut(|compiler| {
201 let mut pcx = compiler.parse();
202 pcx.set_resolve_imports(false);
203 match input {
204 Input::Paths(paths) if paths.is_empty() => {
205 sh_warn!(
206 "Nothing to format.\n\
207 HINT: If you are working outside of the project, \
208 try providing paths to your source files: `forge fmt <paths>`"
209 )?;
210 return Ok(());
211 }
212 Input::Paths(paths) => _ = pcx.par_load_files(paths),
213 Input::Stdin => _ = pcx.load_stdin(),
214 }
215 pcx.parse();
216
217 let gcx = compiler.gcx();
218 let fmt_config = Arc::new(config.fmt);
219 let diffs: Vec<String> = gcx
220 .sources
221 .raw
222 .par_iter()
223 .filter_map(|source_unit| {
224 let path = source_unit.file.name.as_real();
225 let original = source_unit.file.src.as_str();
226 let source_fmt_config =
227 if let Some(nearest_fmt_configs) = nearest_fmt_configs.as_ref() {
228 let Some(source_path) = path else {
229 return Some(Err(eyre::eyre!(
230 "could not resolve formatter config for stdin"
231 )));
232 };
233 let source_path = match fs::canonicalize_path(source_path) {
234 Ok(path) => path,
235 Err(err) => return Some(Err(err.into())),
236 };
237 let Some(fmt_config) = nearest_fmt_configs.get(&source_path) else {
238 return Some(Err(eyre::eyre!(
239 "could not resolve formatter config for {}",
240 source_path.display()
241 )));
242 };
243 fmt_config.clone()
244 } else {
245 fmt_config.clone()
246 };
247 let formatted = forge_fmt::format_ast(gcx, source_unit, source_fmt_config)?;
248 let from_stdin = path.is_none();
249
250 if from_stdin && self.raw {
253 return Some(Ok(formatted));
254 }
255
256 if original == formatted {
257 return None;
258 }
259
260 if self.check || from_stdin {
261 let summary = if self.raw {
262 formatted
263 } else {
264 let name = match path {
265 Some(path) => path
266 .strip_prefix(&config.root)
267 .unwrap_or(path)
268 .display()
269 .to_string(),
270 None => "stdin".to_string(),
271 };
272 format_diff_summary(&name, &TextDiff::from_lines(original, &formatted))
273 };
274 Some(Ok(summary))
275 } else if let Some(path) = path {
276 match fs::write(path, formatted) {
277 Ok(()) => {}
278 Err(e) => return Some(Err(e.into())),
279 }
280 let _ = sh_status!("Formatted {}", path.display());
281 None
282 } else {
283 unreachable!()
284 }
285 })
286 .collect::<Result<_>>()?;
287
288 if !diffs.is_empty() {
289 let mut stdout = io::stdout().lock();
291 for (i, diff) in diffs.iter().enumerate() {
292 if i > 0 {
293 let _ = stdout.write_all(b"\n");
294 }
295 let _ = stdout.write_all(diff.as_bytes());
296 }
297 if self.check {
298 std::process::exit(1);
299 }
300 }
301
302 convert_solar_errors(compiler.dcx())
303 })
304 }
305
306 pub const fn is_watch(&self) -> bool {
308 self.watch.watch.is_some()
309 }
310}
311
312#[derive(Debug)]
313enum Input {
314 Stdin,
315 Paths(Vec<PathBuf>),
316}
317
318struct Line(Option<usize>);
319
320impl fmt::Display for Line {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 match self.0 {
323 None => f.write_str(" "),
324 Some(idx) => write!(f, "{:<4}", idx + 1),
325 }
326 }
327}
328
329fn format_diff_summary<'a>(name: &str, diff: &'a TextDiff<'a, 'a, str>) -> String {
330 let cap = 128;
331 let mut diff_summary = String::with_capacity(cap);
332
333 let _ = writeln!(diff_summary, "Diff in {name}:");
334 for (j, group) in diff.grouped_ops(3).into_iter().enumerate() {
335 if j > 0 {
336 let s =
337 "--------------------------------------------------------------------------------";
338 diff_summary.push_str(s);
339 }
340 for op in group {
341 for change in diff.iter_inline_changes(&op) {
342 let dimmed = Style::new().dim();
343 let (sign, s) = match change.tag() {
344 ChangeTag::Delete => ("-", Color::Red.foreground()),
345 ChangeTag::Insert => ("+", Color::Green.foreground()),
346 ChangeTag::Equal => (" ", dimmed),
347 };
348
349 let _ = write!(
350 diff_summary,
351 "{}{} |{}",
352 Line(change.old_index()).paint(dimmed),
353 Line(change.new_index()).paint(dimmed),
354 sign.paint(s.bold()),
355 );
356
357 for (emphasized, value) in change.iter_strings_lossy() {
358 let s = if emphasized { s.underline().bg(Color::Black) } else { s };
359 let _ = write!(diff_summary, "{}", value.paint(s));
360 }
361
362 if change.missing_newline() {
363 diff_summary.push('\n');
364 }
365 }
366 }
367 }
368
369 diff_summary
370}