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