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(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 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| !Config::is_standalone_section(k))
147 .collect::<Vec<_>>();
148
149 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
162pub 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 jail.set_env("HOME", jail.directory().display().to_string());
231 std::fs::create_dir(jail.directory().join(".foundry")).unwrap();
232
233 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 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}