1use crate::{Config, Warning, providers::TomlFileProvider};
4use std::{
5 fs, io,
6 ops::{Deref, DerefMut},
7 path::{Path, PathBuf},
8};
9
10struct TomlFile {
12 doc: toml_edit::DocumentMut,
13 path: PathBuf,
14}
15
16impl TomlFile {
17 fn open(path: impl AsRef<Path>) -> eyre::Result<Self> {
18 let path = path.as_ref().to_owned();
19 let doc = fs::read_to_string(&path)?.parse()?;
20 Ok(Self { doc, path })
21 }
22
23 const fn doc(&self) -> &toml_edit::DocumentMut {
24 &self.doc
25 }
26
27 fn path(&self) -> &Path {
28 self.path.as_ref()
29 }
30
31 fn save(&self) -> io::Result<()> {
32 fs::write(self.path(), self.doc().to_string())
33 }
34}
35
36impl Deref for TomlFile {
37 type Target = toml_edit::DocumentMut;
38 fn deref(&self) -> &Self::Target {
39 self.doc()
40 }
41}
42
43impl DerefMut for TomlFile {
44 fn deref_mut(&mut self) -> &mut Self::Target {
45 &mut self.doc
46 }
47}
48
49#[derive(Debug)]
51struct InsertProfileError {
52 pub message: String,
53 pub value: toml_edit::Item,
54}
55
56impl std::fmt::Display for InsertProfileError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.write_str(&self.message)
59 }
60}
61
62impl std::error::Error for InsertProfileError {}
63
64impl TomlFile {
65 #[expect(clippy::result_large_err)]
68 fn insert_profile(
69 &mut self,
70 profile_str: &str,
71 value: toml_edit::Item,
72 ) -> Result<(), InsertProfileError> {
73 if !value.is_table_like() {
74 return Err(InsertProfileError {
75 message: format!("Expected [{profile_str}] to be a Table"),
76 value,
77 });
78 }
79 let profile_map = if let Some(map) = self.get_mut(Config::PROFILE_SECTION) {
81 map
82 } else {
83 let mut profile_section = toml_edit::Table::new();
85 profile_section.set_position(Some(0));
86 profile_section.set_implicit(true);
87 self.insert(Config::PROFILE_SECTION, toml_edit::Item::Table(profile_section));
88 self.get_mut(Config::PROFILE_SECTION).expect("exists per above")
89 };
90 let profile_map = if let Some(table) = profile_map.as_table_like_mut() {
92 table
93 } else {
94 return Err(InsertProfileError {
95 message: format!("Expected [{}] to be a Table", Config::PROFILE_SECTION),
96 value,
97 });
98 };
99 if let Some(profile) = profile_map.get(profile_str) {
101 if let Some(profile_table) = profile.as_table_like() {
102 if !profile_table.is_empty() {
103 return Err(InsertProfileError {
104 message: format!(
105 "[{}.{}] already exists",
106 Config::PROFILE_SECTION,
107 profile_str
108 ),
109 value,
110 });
111 }
112 } else {
113 return Err(InsertProfileError {
114 message: format!(
115 "Expected [{}.{}] to be a Table",
116 Config::PROFILE_SECTION,
117 profile_str
118 ),
119 value,
120 });
121 }
122 }
123 profile_map.insert(profile_str, value);
125 Ok(())
126 }
127}
128
129fn fix_toml_non_strict_profiles(
132 toml_file: &mut TomlFile,
133) -> Vec<(String, Result<(), InsertProfileError>)> {
134 let mut results = vec![];
135
136 let profiles = toml_file
138 .as_table()
139 .iter()
140 .map(|(k, _)| k.to_string())
141 .filter(|k| !Config::is_standalone_section(k))
142 .collect::<Vec<_>>();
143
144 for profile in profiles {
146 if let Some(value) = toml_file.remove(&profile) {
147 let res = toml_file.insert_profile(&profile, value);
148 if let Err(err) = res.as_ref() {
149 toml_file.insert(&profile, err.value.clone());
150 }
151 results.push((profile, res))
152 }
153 }
154 results
155}
156
157pub fn fix_tomls() -> Vec<Warning> {
160 fix_tomls_with_root(".")
161}
162
163pub fn fix_tomls_with_root(root: impl AsRef<Path>) -> Vec<Warning> {
168 let mut warnings = vec![];
169
170 let tomls = {
171 let mut tomls = vec![];
172 if let Some(global_toml) = Config::foundry_dir_toml().filter(|p| p.exists()) {
173 tomls.push(global_toml);
174 }
175 let local_toml =
176 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.as_ref().join(Config::FILE_NAME))
177 .file();
178 if local_toml.exists() {
179 tomls.push(local_toml);
180 } else {
181 warnings.push(Warning::NoLocalToml(local_toml));
182 }
183 tomls
184 };
185
186 for toml in tomls {
187 let mut toml_file = match TomlFile::open(&toml) {
188 Ok(toml_file) => toml_file,
189 Err(err) => {
190 warnings.push(Warning::CouldNotReadToml { path: toml, err: err.to_string() });
191 continue;
192 }
193 };
194
195 let results = fix_toml_non_strict_profiles(&mut toml_file);
196 let was_edited = results.iter().any(|(_, res)| res.is_ok());
197 for (profile, err) in results
198 .into_iter()
199 .filter_map(|(profile, res)| res.err().map(|err| (profile, err.message)))
200 {
201 warnings.push(Warning::CouldNotFixProfile {
202 path: toml_file.path().into(),
203 profile,
204 err,
205 })
206 }
207
208 if was_edited && let Err(err) = toml_file.save() {
209 warnings.push(Warning::CouldNotWriteToml {
210 path: toml_file.path().into(),
211 err: err.to_string(),
212 });
213 }
214 }
215
216 warnings
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use figment::Jail;
223 use similar_asserts::assert_eq;
224
225 macro_rules! fix_test {
226 ($(#[$attr:meta])* $name:ident, $fun:expr) => {
227 #[test]
228 $(#[$attr])*
229 fn $name() {
230 Jail::expect_with(|jail| {
231 jail.set_env("HOME", jail.directory().display().to_string());
234 std::fs::create_dir(jail.directory().join(".foundry")).unwrap();
235
236 let f: Box<dyn FnOnce(&mut Jail) -> Result<(), figment::Error>> = Box::new($fun);
238 f(jail)?;
239
240 Ok(())
241 });
242 }
243 };
244 }
245
246 fix_test!(test_implicit_profile_name_changed, |jail| {
247 jail.create_file(
248 "foundry.toml",
249 r#"
250 [default]
251 src = "src"
252 # comment
253
254 [other]
255 src = "other-src"
256 "#,
257 )?;
258 fix_tomls();
259 assert_eq!(
260 fs::read_to_string("foundry.toml").unwrap(),
261 r#"
262 [profile.default]
263 src = "src"
264 # comment
265
266 [profile.other]
267 src = "other-src"
268 "#
269 );
270 Ok(())
271 });
272
273 fix_test!(test_leave_standalone_sections_alone, |jail| {
274 jail.create_file(
275 "foundry.toml",
276 r#"
277 [default]
278 src = "src"
279
280 [fmt]
281 line_length = 100
282
283 [rpc_endpoints]
284 optimism = "https://example.com/"
285 "#,
286 )?;
287 fix_tomls();
288 assert_eq!(
289 fs::read_to_string("foundry.toml").unwrap(),
290 r#"
291 [profile.default]
292 src = "src"
293
294 [fmt]
295 line_length = 100
296
297 [rpc_endpoints]
298 optimism = "https://example.com/"
299 "#
300 );
301 Ok(())
302 });
303
304 fix_test!(
306 #[cfg(not(windows))]
307 test_global_toml_is_edited,
308 |jail| {
309 jail.create_file(
310 "foundry.toml",
311 r#"
312 [other]
313 src = "other-src"
314 "#,
315 )?;
316 jail.create_file(
317 ".foundry/foundry.toml",
318 r#"
319 [default]
320 src = "src"
321 "#,
322 )?;
323 fix_tomls();
324 assert_eq!(
325 fs::read_to_string("foundry.toml").unwrap(),
326 r#"
327 [profile.other]
328 src = "other-src"
329 "#
330 );
331 assert_eq!(
332 fs::read_to_string(".foundry/foundry.toml").unwrap(),
333 r#"
334 [profile.default]
335 src = "src"
336 "#
337 );
338 Ok(())
339 }
340 );
341}