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