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