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