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