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