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