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::{filter::expand_globs, impl_figment_convert_basic};
8use rayon::prelude::*;
9use similar::{ChangeTag, TextDiff};
10use solar::sema::Compiler;
11use std::{
12 fmt::{self, Write},
13 io,
14 io::Write as _,
15 path::{Path, PathBuf},
16 sync::Arc,
17};
18use yansi::{Color, Paint, Style};
19
20#[derive(Clone, Debug, Parser)]
22pub struct FmtArgs {
23 #[arg(value_hint = ValueHint::FilePath, value_name = "PATH", num_args(1..))]
25 paths: Vec<PathBuf>,
26
27 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
32 root: Option<PathBuf>,
33
34 #[arg(long)]
39 check: bool,
40
41 #[arg(long, short)]
43 raw: bool,
44
45 #[command(flatten)]
46 pub watch: WatchArgs,
47}
48
49impl_figment_convert_basic!(FmtArgs);
50
51impl FmtArgs {
52 pub fn run(self) -> Result<()> {
53 let config = self.load_config()?;
54 let cwd = std::env::current_dir()?;
55
56 let ignored = expand_globs(&config.root, config.fmt.ignore.iter())?
58 .iter()
59 .flat_map(fs::canonicalize_path)
60 .collect::<Vec<_>>();
61
62 let libs = expand_globs(&config.root, config.libs.iter().filter_map(|p| p.to_str()))?
64 .iter()
65 .flat_map(fs::canonicalize_path)
66 .collect::<Vec<_>>();
67
68 let is_under_ignored_dir = |file_path: &Path, include_libs: bool| -> bool {
70 let check_against_dir = |dir: &PathBuf| {
71 file_path.starts_with(dir)
72 || cwd.join(file_path).starts_with(dir)
73 || fs::canonicalize_path(file_path).is_ok_and(|p| p.starts_with(dir))
74 };
75
76 ignored.iter().any(&check_against_dir)
77 || (include_libs && libs.iter().any(&check_against_dir))
78 };
79
80 let input = match &self.paths[..] {
81 [] => {
82 let project_paths: Vec<PathBuf> = config
84 .project_paths::<SolcLanguage>()
85 .input_files_iter()
86 .filter(|p| {
87 !(ignored.contains(p)
88 || ignored.contains(&cwd.join(p))
89 || is_under_ignored_dir(p, true))
90 })
91 .collect();
92 Input::Paths(project_paths)
93 }
94 [one] if one == Path::new("-") => Input::Stdin,
95 paths => {
96 let mut inputs = Vec::with_capacity(paths.len());
97 for path in paths {
98 if !ignored.is_empty()
100 && ((path.is_absolute() && ignored.contains(path))
101 || ignored.contains(&cwd.join(path)))
102 {
103 continue;
104 }
105
106 if path.is_dir() {
107 let exclude_libs = !is_under_ignored_dir(path, true);
109 inputs.extend(
110 foundry_compilers::utils::source_files_iter(path, SOLC_EXTENSIONS)
111 .filter(|p| {
112 !(ignored.contains(p)
113 || ignored.contains(&cwd.join(p))
114 || is_under_ignored_dir(p, exclude_libs))
115 }),
116 );
117 } else if path.is_sol() {
118 inputs.push(path.to_path_buf());
120 } else {
121 warn!("Cannot process path {}", path.display());
122 }
123 }
124 Input::Paths(inputs)
125 }
126 };
127
128 let mut compiler = Compiler::new(
129 solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(),
130 );
131
132 compiler.enter_mut(|compiler| {
134 let mut pcx = compiler.parse();
135 pcx.set_resolve_imports(false);
136 match input {
137 Input::Paths(paths) if paths.is_empty() => {
138 sh_warn!(
139 "Nothing to format.\n\
140 HINT: If you are working outside of the project, \
141 try providing paths to your source files: `forge fmt <paths>`"
142 )?;
143 return Ok(());
144 }
145 Input::Paths(paths) => _ = pcx.par_load_files(paths),
146 Input::Stdin => _ = pcx.load_stdin(),
147 }
148 pcx.parse();
149
150 let gcx = compiler.gcx();
151 let fmt_config = Arc::new(config.fmt);
152 let diffs: Vec<String> = gcx
153 .sources
154 .raw
155 .par_iter()
156 .filter_map(|source_unit| {
157 let path = source_unit.file.name.as_real();
158 let original = source_unit.file.src.as_str();
159 let formatted = forge_fmt::format_ast(gcx, source_unit, fmt_config.clone())?;
160 let from_stdin = path.is_none();
161
162 if from_stdin && self.raw {
165 return Some(Ok(formatted));
166 }
167
168 if original == formatted {
169 return None;
170 }
171
172 if self.check || from_stdin {
173 let summary = if self.raw {
174 formatted
175 } else {
176 let name = match path {
177 Some(path) => path
178 .strip_prefix(&config.root)
179 .unwrap_or(path)
180 .display()
181 .to_string(),
182 None => "stdin".to_string(),
183 };
184 format_diff_summary(&name, &TextDiff::from_lines(original, &formatted))
185 };
186 Some(Ok(summary))
187 } else if let Some(path) = path {
188 match fs::write(path, formatted) {
189 Ok(()) => {}
190 Err(e) => return Some(Err(e.into())),
191 }
192 let _ = sh_println!("Formatted {}", path.display());
193 None
194 } else {
195 unreachable!()
196 }
197 })
198 .collect::<Result<_>>()?;
199
200 if !diffs.is_empty() {
201 let mut stdout = io::stdout().lock();
203 for (i, diff) in diffs.iter().enumerate() {
204 if i > 0 {
205 let _ = stdout.write_all(b"\n");
206 }
207 let _ = stdout.write_all(diff.as_bytes());
208 }
209 if self.check {
210 std::process::exit(1);
211 }
212 }
213
214 convert_solar_errors(compiler.dcx())
215 })
216 }
217
218 pub fn is_watch(&self) -> bool {
220 self.watch.watch.is_some()
221 }
222}
223
224#[derive(Debug)]
225enum Input {
226 Stdin,
227 Paths(Vec<PathBuf>),
228}
229
230struct Line(Option<usize>);
231
232impl fmt::Display for Line {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self.0 {
235 None => f.write_str(" "),
236 Some(idx) => write!(f, "{:<4}", idx + 1),
237 }
238 }
239}
240
241fn format_diff_summary<'a>(name: &str, diff: &'a TextDiff<'a, 'a, '_, str>) -> String {
242 let cap = 128;
243 let mut diff_summary = String::with_capacity(cap);
244
245 let _ = writeln!(diff_summary, "Diff in {name}:");
246 for (j, group) in diff.grouped_ops(3).into_iter().enumerate() {
247 if j > 0 {
248 let s =
249 "--------------------------------------------------------------------------------";
250 diff_summary.push_str(s);
251 }
252 for op in group {
253 for change in diff.iter_inline_changes(&op) {
254 let dimmed = Style::new().dim();
255 let (sign, s) = match change.tag() {
256 ChangeTag::Delete => ("-", Color::Red.foreground()),
257 ChangeTag::Insert => ("+", Color::Green.foreground()),
258 ChangeTag::Equal => (" ", dimmed),
259 };
260
261 let _ = write!(
262 diff_summary,
263 "{}{} |{}",
264 Line(change.old_index()).paint(dimmed),
265 Line(change.new_index()).paint(dimmed),
266 sign.paint(s.bold()),
267 );
268
269 for (emphasized, value) in change.iter_strings_lossy() {
270 let s = if emphasized { s.underline().bg(Color::Black) } else { s };
271 let _ = write!(diff_summary, "{}", value.paint(s));
272 }
273
274 if change.missing_newline() {
275 diff_summary.push('\n');
276 }
277 }
278 }
279 }
280
281 diff_summary
282}