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