foundry_config/providers/
warnings.rs

1use crate::{Config, DEPRECATIONS, Warning};
2use figment::{
3    Error, Figment, Metadata, Profile, Provider,
4    value::{Dict, Map, Value},
5};
6use heck::ToSnakeCase;
7use std::collections::{BTreeMap, BTreeSet};
8
9/// Allowed keys for CompilationRestrictions.
10const COMPILATION_RESTRICTIONS_KEYS: &[&str] = &[
11    "paths",
12    "version",
13    "via_ir",
14    "bytecode_hash",
15    "min_optimizer_runs",
16    "optimizer_runs",
17    "max_optimizer_runs",
18    "min_evm_version",
19    "evm_version",
20    "max_evm_version",
21];
22
23/// Allowed keys for SettingsOverrides.
24const SETTINGS_OVERRIDES_KEYS: &[&str] =
25    &["name", "via_ir", "evm_version", "optimizer", "optimizer_runs", "bytecode_hash"];
26
27/// Reserved keys that should not trigger unknown key warnings.
28const RESERVED_KEYS: &[&str] = &["extends"];
29
30/// Keys kept for backward compatibility that should not trigger unknown key warnings.
31const BACKWARD_COMPATIBLE_KEYS: &[&str] = &["solc_version"];
32
33/// Generate warnings for unknown sections and deprecated keys
34pub struct WarningsProvider<P> {
35    provider: P,
36    profile: Profile,
37    old_warnings: Result<Vec<Warning>, Error>,
38}
39
40impl<P: Provider> WarningsProvider<P> {
41    const WARNINGS_KEY: &'static str = "__warnings";
42
43    /// Creates a new warnings provider.
44    pub fn new(
45        provider: P,
46        profile: impl Into<Profile>,
47        old_warnings: Result<Vec<Warning>, Error>,
48    ) -> Self {
49        Self { provider, profile: profile.into(), old_warnings }
50    }
51
52    /// Creates a new figment warnings provider.
53    pub fn for_figment(provider: P, figment: &Figment) -> Self {
54        let old_warnings = {
55            let warnings_res = figment.extract_inner(Self::WARNINGS_KEY);
56            if warnings_res.as_ref().err().map(|err| err.missing()).unwrap_or(false) {
57                Ok(vec![])
58            } else {
59                warnings_res
60            }
61        };
62        Self::new(provider, figment.profile().clone(), old_warnings)
63    }
64
65    /// Collects all warnings.
66    pub fn collect_warnings(&self) -> Result<Vec<Warning>, Error> {
67        let data = self.provider.data().unwrap_or_default();
68
69        let mut out = self.old_warnings.clone()?;
70
71        // Add warning for unknown sections.
72        out.extend(data.keys().filter(|k| !Config::is_standalone_section(k.as_str())).map(
73            |unknown_section| {
74                let source = self.provider.metadata().source.map(|s| s.to_string());
75                Warning::UnknownSection { unknown_section: unknown_section.clone(), source }
76            },
77        ));
78
79        // Add warning for deprecated keys.
80        let deprecated_key_warning = |key| {
81            DEPRECATIONS.iter().find_map(|(deprecated_key, new_value)| {
82                if key == *deprecated_key {
83                    Some(Warning::DeprecatedKey {
84                        old: deprecated_key.to_string(),
85                        new: new_value.to_string(),
86                    })
87                } else {
88                    None
89                }
90            })
91        };
92        let profiles = data
93            .iter()
94            .filter(|(profile, _)| **profile == Config::PROFILE_SECTION)
95            .map(|(_, dict)| dict);
96
97        out.extend(profiles.clone().flat_map(BTreeMap::keys).filter_map(deprecated_key_warning));
98        out.extend(
99            profiles
100                .clone()
101                .filter_map(|dict| dict.get(self.profile.as_str().as_str()))
102                .filter_map(Value::as_dict)
103                .flat_map(BTreeMap::keys)
104                .filter_map(deprecated_key_warning),
105        );
106
107        // Add warning for unknown keys within profiles (root keys only here).
108        if let Ok(default_map) = figment::providers::Serialized::defaults(&Config::default()).data()
109            && let Some(default_dict) = default_map.get(&Config::DEFAULT_PROFILE)
110        {
111            let allowed_keys: BTreeSet<String> = default_dict.keys().cloned().collect();
112            for profile_map in profiles.clone() {
113                for (profile, value) in profile_map {
114                    let Some(profile_dict) = value.as_dict() else {
115                        continue;
116                    };
117
118                    let source = self
119                        .provider
120                        .metadata()
121                        .source
122                        .map(|s| s.to_string())
123                        .unwrap_or(Config::FILE_NAME.to_string());
124                    for key in profile_dict.keys() {
125                        let is_not_deprecated =
126                            !DEPRECATIONS.iter().any(|(deprecated_key, _)| *deprecated_key == key);
127                        let is_not_allowed = !allowed_keys.contains(key)
128                            && !allowed_keys.contains(&key.to_snake_case());
129                        let is_not_reserved =
130                            !RESERVED_KEYS.contains(&key.as_str()) && key != Self::WARNINGS_KEY;
131                        let is_not_backward_compatible =
132                            !BACKWARD_COMPATIBLE_KEYS.contains(&key.as_str());
133
134                        if is_not_deprecated
135                            && is_not_allowed
136                            && is_not_reserved
137                            && is_not_backward_compatible
138                        {
139                            out.push(Warning::UnknownKey {
140                                key: key.clone(),
141                                profile: profile.clone(),
142                                source: source.clone(),
143                            });
144                        }
145                    }
146
147                    // Add warning for unknown keys in nested sections within profiles.
148                    self.collect_nested_section_warnings(
149                        profile_dict,
150                        default_dict,
151                        &source,
152                        &mut out,
153                    );
154                }
155            }
156
157            // Add warning for unknown keys in standalone sections.
158            self.collect_standalone_section_warnings(&data, default_dict, &mut out);
159        }
160
161        Ok(out)
162    }
163
164    /// Collects warnings for unknown keys in standalone sections like `[lint]`, `[fmt]`, etc.
165    fn collect_standalone_section_warnings(
166        &self,
167        data: &Map<Profile, Dict>,
168        default_dict: &Dict,
169        out: &mut Vec<Warning>,
170    ) {
171        let source = self
172            .provider
173            .metadata()
174            .source
175            .map(|s| s.to_string())
176            .unwrap_or(Config::FILE_NAME.to_string());
177
178        for section_name in Config::STANDALONE_SECTIONS {
179            // Get the section from the parsed data
180            let section_profile = Profile::new(section_name);
181            let Some(section_dict) = data.get(&section_profile) else {
182                continue;
183            };
184
185            // Get allowed keys for this section from the default config
186            let Some(default_section_value) = default_dict.get(*section_name) else {
187                continue;
188            };
189            let Some(default_section_dict) = default_section_value.as_dict() else {
190                continue;
191            };
192
193            let allowed_keys: BTreeSet<String> = default_section_dict.keys().cloned().collect();
194
195            for key in section_dict.keys() {
196                let is_not_allowed =
197                    !allowed_keys.contains(key) && !allowed_keys.contains(&key.to_snake_case());
198                if is_not_allowed {
199                    out.push(Warning::UnknownSectionKey {
200                        key: key.clone(),
201                        section: section_name.to_string(),
202                        source: source.clone(),
203                    });
204                }
205            }
206        }
207    }
208
209    /// Collects warnings for unknown keys in nested sections within profiles,
210    /// like `compilation_restrictions`.
211    fn collect_nested_section_warnings(
212        &self,
213        profile_dict: &Dict,
214        default_dict: &Dict,
215        source: &str,
216        out: &mut Vec<Warning>,
217    ) {
218        // Check nested sections that are dicts (like `lint`, `fmt` when defined in profile)
219        for (key, value) in profile_dict {
220            let Some(nested_dict) = value.as_dict() else {
221                // Also check arrays of dicts (like `compilation_restrictions`)
222                if let Some(arr) = value.as_array() {
223                    // Get allowed keys for known array item types
224                    let allowed_keys = Self::get_array_item_allowed_keys(key);
225
226                    if allowed_keys.is_empty() {
227                        continue;
228                    }
229
230                    for item in arr {
231                        let Some(item_dict) = item.as_dict() else {
232                            continue;
233                        };
234                        for item_key in item_dict.keys() {
235                            let is_not_allowed = !allowed_keys.contains(item_key)
236                                && !allowed_keys.contains(&item_key.to_snake_case());
237                            if is_not_allowed {
238                                out.push(Warning::UnknownSectionKey {
239                                    key: item_key.clone(),
240                                    section: key.clone(),
241                                    source: source.to_string(),
242                                });
243                            }
244                        }
245                    }
246                }
247                continue;
248            };
249
250            // Get allowed keys from the default config for this nested section
251            let Some(default_value) = default_dict.get(key) else {
252                continue;
253            };
254            let Some(default_nested_dict) = default_value.as_dict() else {
255                continue;
256            };
257
258            let allowed_keys: BTreeSet<String> = default_nested_dict.keys().cloned().collect();
259
260            for nested_key in nested_dict.keys() {
261                let is_not_allowed = !allowed_keys.contains(nested_key)
262                    && !allowed_keys.contains(&nested_key.to_snake_case());
263                if is_not_allowed {
264                    out.push(Warning::UnknownSectionKey {
265                        key: nested_key.clone(),
266                        section: key.clone(),
267                        source: source.to_string(),
268                    });
269                }
270            }
271        }
272    }
273
274    /// Returns the allowed keys for array item types based on the section name.
275    fn get_array_item_allowed_keys(section_name: &str) -> BTreeSet<String> {
276        match section_name {
277            "compilation_restrictions" => {
278                COMPILATION_RESTRICTIONS_KEYS.iter().map(|s| s.to_string()).collect()
279            }
280            "additional_compiler_profiles" => {
281                SETTINGS_OVERRIDES_KEYS.iter().map(|s| s.to_string()).collect()
282            }
283            _ => BTreeSet::new(),
284        }
285    }
286}
287
288impl<P: Provider> Provider for WarningsProvider<P> {
289    fn metadata(&self) -> Metadata {
290        if let Some(source) = self.provider.metadata().source {
291            Metadata::from("Warnings", source)
292        } else {
293            Metadata::named("Warnings")
294        }
295    }
296
297    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
298        let warnings = self.collect_warnings()?;
299        Ok(Map::from([(
300            self.profile.clone(),
301            Dict::from([(Self::WARNINGS_KEY.to_string(), Value::serialize(warnings)?)]),
302        )]))
303    }
304
305    fn profile(&self) -> Option<Profile> {
306        Some(self.profile.clone())
307    }
308}