Skip to main content

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    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/// The error emitted when failing to insert into a profile.
51#[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    /// Insert a name as `[profile.name]`. Creating the `[profile]` table where necessary and
67    /// throwing an error if there exists a conflict
68    #[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        // get or create the profile section
81        let profile_map = if let Some(map) = self.get_mut(Config::PROFILE_SECTION) {
82            map
83        } else {
84            // insert profile section at the beginning of the map
85            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        // ensure the profile section is a table
92        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        // check the profile map for structure and existing keys
101        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        // insert the profile
125        profile_map.insert(profile_str, value);
126        Ok(())
127    }
128}
129
130/// Making sure any implicit profile `[name]` becomes `[profile.name]` for the given file and
131/// returns the implicit profiles and the result of editing them
132fn fix_toml_non_strict_profiles(
133    toml_file: &mut TomlFile,
134) -> Vec<(String, Result<(), InsertProfileError>)> {
135    let mut results = vec![];
136
137    // get any non root level keys that need to be inserted into [profile]
138    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    // remove each profile and insert into [profile] section
146    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
158/// Fix foundry.toml files. Making sure any implicit profile `[name]` becomes
159/// `[profile.name]`. Return any warnings
160pub 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                    // setup home directory,
225                    // **Note** this only has an effect on unix, as [`dirs::home_dir()`] on windows uses `FOLDERID_Profile`
226                    jail.set_env("HOME", jail.directory().display().to_string());
227                    std::fs::create_dir(jail.directory().join(".foundry")).unwrap();
228
229                    // define function type to allow implicit params / return
230                    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    // mocking the `$HOME` has no effect on windows, see [`dirs::home_dir()`]
298    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}