Skip to main content

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