foundry_config/
fix.rs

1//! Helpers to automatically fix configuration warnings.
2
3use crate::{Config, Warning};
4use figment::providers::Env;
5use std::{
6    fs, io,
7    ops::{Deref, DerefMut},
8    path::{Path, PathBuf},
9};
10
11/// A convenience wrapper around a TOML document and the path it was read from
12struct 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/// The error emitted when failing to insert into a profile.
55#[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    /// Insert a name as `[profile.name]`. Creating the `[profile]` table where necessary and
71    /// throwing an error if there exists a conflict
72    #[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        // get or create the profile section
85        let profile_map = if let Some(map) = self.get_mut(Config::PROFILE_SECTION) {
86            map
87        } else {
88            // insert profile section at the beginning of the map
89            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        // ensure the profile section is a table
96        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        // check the profile map for structure and existing keys
105        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        // insert the profile
129        profile_map.insert(profile_str, value);
130        Ok(())
131    }
132}
133
134/// Making sure any implicit profile `[name]` becomes `[profile.name]` for the given file and
135/// returns the implicit profiles and the result of editing them
136fn fix_toml_non_strict_profiles(
137    toml_file: &mut TomlFile,
138) -> Vec<(String, Result<(), InsertProfileError>)> {
139    let mut results = vec![];
140
141    // get any non root level keys that need to be inserted into [profile]
142    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    // remove each profile and insert into [profile] section
152    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
164/// Fix foundry.toml files. Making sure any implicit profile `[name]` becomes
165/// `[profile.name]`. Return any warnings
166pub 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 {
208            if 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
217    warnings
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use figment::Jail;
224    use similar_asserts::assert_eq;
225
226    macro_rules! fix_test {
227        ($(#[$attr:meta])* $name:ident, $fun:expr) => {
228            #[test]
229            $(#[$attr])*
230            fn $name() {
231                Jail::expect_with(|jail| {
232                    // setup home directory,
233                    // **Note** this only has an effect on unix, as [`dirs::home_dir()`] on windows uses `FOLDERID_Profile`
234                    jail.set_env("HOME", jail.directory().display().to_string());
235                    std::fs::create_dir(jail.directory().join(".foundry")).unwrap();
236
237                    // define function type to allow implicit params / return
238                    let f: Box<dyn FnOnce(&mut Jail) -> Result<(), figment::Error>> = Box::new($fun);
239                    f(jail)?;
240
241                    Ok(())
242                });
243            }
244        };
245    }
246
247    fix_test!(test_implicit_profile_name_changed, |jail| {
248        jail.create_file(
249            "foundry.toml",
250            r#"
251                [default]
252                src = "src"
253                # comment
254
255                [other]
256                src = "other-src"
257            "#,
258        )?;
259        fix_tomls();
260        assert_eq!(
261            fs::read_to_string("foundry.toml").unwrap(),
262            r#"
263                [profile.default]
264                src = "src"
265                # comment
266
267                [profile.other]
268                src = "other-src"
269            "#
270        );
271        Ok(())
272    });
273
274    fix_test!(test_leave_standalone_sections_alone, |jail| {
275        jail.create_file(
276            "foundry.toml",
277            r#"
278                [default]
279                src = "src"
280
281                [fmt]
282                line_length = 100
283
284                [rpc_endpoints]
285                optimism = "https://example.com/"
286            "#,
287        )?;
288        fix_tomls();
289        assert_eq!(
290            fs::read_to_string("foundry.toml").unwrap(),
291            r#"
292                [profile.default]
293                src = "src"
294
295                [fmt]
296                line_length = 100
297
298                [rpc_endpoints]
299                optimism = "https://example.com/"
300            "#
301        );
302        Ok(())
303    });
304
305    // mocking the `$HOME` has no effect on windows, see [`dirs::home_dir()`]
306    fix_test!(
307        #[cfg(not(windows))]
308        test_global_toml_is_edited,
309        |jail| {
310            jail.create_file(
311                "foundry.toml",
312                r#"
313                [other]
314                src = "other-src"
315            "#,
316            )?;
317            jail.create_file(
318                ".foundry/foundry.toml",
319                r#"
320                [default]
321                src = "src"
322            "#,
323            )?;
324            fix_tomls();
325            assert_eq!(
326                fs::read_to_string("foundry.toml").unwrap(),
327                r#"
328                [profile.other]
329                src = "other-src"
330            "#
331            );
332            assert_eq!(
333                fs::read_to_string(".foundry/foundry.toml").unwrap(),
334                r#"
335                [profile.default]
336                src = "src"
337            "#
338            );
339            Ok(())
340        }
341    );
342}