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