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 && 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                    // setup home directory,
231                    // **Note** this only has an effect on unix, as [`dirs::home_dir()`] on windows uses `FOLDERID_Profile`
232                    jail.set_env("HOME", jail.directory().display().to_string());
233                    std::fs::create_dir(jail.directory().join(".foundry")).unwrap();
234
235                    // define function type to allow implicit params / return
236                    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    // mocking the `$HOME` has no effect on windows, see [`dirs::home_dir()`]
304    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}