foundry_config/
fix.rs
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 {
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 jail.set_env("HOME", jail.directory().display().to_string());
235 std::fs::create_dir(jail.directory().join(".foundry")).unwrap();
236
237 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 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}