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(Some(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| !Config::is_standalone_section(k))
147        .collect::<Vec<_>>();
148
149    // remove each profile and insert into [profile] section
150    for profile in profiles {
151        if let Some(value) = toml_file.remove(&profile) {
152            let res = toml_file.insert_profile(&profile, value);
153            if let Err(err) = res.as_ref() {
154                toml_file.insert(&profile, err.value.clone());
155            }
156            results.push((profile, res))
157        }
158    }
159    results
160}
161
162/// Fix foundry.toml files. Making sure any implicit profile `[name]` becomes
163/// `[profile.name]`. Return any warnings
164pub fn fix_tomls() -> Vec<Warning> {
165    let mut warnings = vec![];
166
167    let tomls = {
168        let mut tomls = vec![];
169        if let Some(global_toml) = Config::foundry_dir_toml().filter(|p| p.exists()) {
170            tomls.push(global_toml);
171        }
172        let local_toml = PathBuf::from(
173            Env::var("FOUNDRY_CONFIG").unwrap_or_else(|| Config::FILE_NAME.to_string()),
174        );
175        if local_toml.exists() {
176            tomls.push(local_toml);
177        } else {
178            warnings.push(Warning::NoLocalToml(local_toml));
179        }
180        tomls
181    };
182
183    for toml in tomls {
184        let mut toml_file = match TomlFile::open(&toml) {
185            Ok(toml_file) => toml_file,
186            Err(err) => {
187                warnings.push(Warning::CouldNotReadToml { path: toml, err: err.to_string() });
188                continue;
189            }
190        };
191
192        let results = fix_toml_non_strict_profiles(&mut toml_file);
193        let was_edited = results.iter().any(|(_, res)| res.is_ok());
194        for (profile, err) in results
195            .into_iter()
196            .filter_map(|(profile, res)| res.err().map(|err| (profile, err.message)))
197        {
198            warnings.push(Warning::CouldNotFixProfile {
199                path: toml_file.path().into(),
200                profile,
201                err,
202            })
203        }
204
205        if was_edited && let Err(err) = toml_file.save() {
206            warnings.push(Warning::CouldNotWriteToml {
207                path: toml_file.path().into(),
208                err: err.to_string(),
209            });
210        }
211    }
212
213    warnings
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use figment::Jail;
220    use similar_asserts::assert_eq;
221
222    macro_rules! fix_test {
223        ($(#[$attr:meta])* $name:ident, $fun:expr) => {
224            #[test]
225            $(#[$attr])*
226            fn $name() {
227                Jail::expect_with(|jail| {
228                    // setup home directory,
229                    // **Note** this only has an effect on unix, as [`dirs::home_dir()`] on windows uses `FOLDERID_Profile`
230                    jail.set_env("HOME", jail.directory().display().to_string());
231                    std::fs::create_dir(jail.directory().join(".foundry")).unwrap();
232
233                    // define function type to allow implicit params / return
234                    let f: Box<dyn FnOnce(&mut Jail) -> Result<(), figment::Error>> = Box::new($fun);
235                    f(jail)?;
236
237                    Ok(())
238                });
239            }
240        };
241    }
242
243    fix_test!(test_implicit_profile_name_changed, |jail| {
244        jail.create_file(
245            "foundry.toml",
246            r#"
247                [default]
248                src = "src"
249                # comment
250
251                [other]
252                src = "other-src"
253            "#,
254        )?;
255        fix_tomls();
256        assert_eq!(
257            fs::read_to_string("foundry.toml").unwrap(),
258            r#"
259                [profile.default]
260                src = "src"
261                # comment
262
263                [profile.other]
264                src = "other-src"
265            "#
266        );
267        Ok(())
268    });
269
270    fix_test!(test_leave_standalone_sections_alone, |jail| {
271        jail.create_file(
272            "foundry.toml",
273            r#"
274                [default]
275                src = "src"
276
277                [fmt]
278                line_length = 100
279
280                [rpc_endpoints]
281                optimism = "https://example.com/"
282            "#,
283        )?;
284        fix_tomls();
285        assert_eq!(
286            fs::read_to_string("foundry.toml").unwrap(),
287            r#"
288                [profile.default]
289                src = "src"
290
291                [fmt]
292                line_length = 100
293
294                [rpc_endpoints]
295                optimism = "https://example.com/"
296            "#
297        );
298        Ok(())
299    });
300
301    // mocking the `$HOME` has no effect on windows, see [`dirs::home_dir()`]
302    fix_test!(
303        #[cfg(not(windows))]
304        test_global_toml_is_edited,
305        |jail| {
306            jail.create_file(
307                "foundry.toml",
308                r#"
309                [other]
310                src = "other-src"
311            "#,
312            )?;
313            jail.create_file(
314                ".foundry/foundry.toml",
315                r#"
316                [default]
317                src = "src"
318            "#,
319            )?;
320            fix_tomls();
321            assert_eq!(
322                fs::read_to_string("foundry.toml").unwrap(),
323                r#"
324                [profile.other]
325                src = "other-src"
326            "#
327            );
328            assert_eq!(
329                fs::read_to_string(".foundry/foundry.toml").unwrap(),
330                r#"
331                [profile.default]
332                src = "src"
333            "#
334            );
335            Ok(())
336        }
337    );
338}