foundry_config/providers/
warnings.rs1use 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
9const 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
23const SETTINGS_OVERRIDES_KEYS: &[&str] =
25 &["name", "via_ir", "evm_version", "optimizer", "optimizer_runs", "bytecode_hash"];
26
27const VYPER_KEYS: &[&str] = &["optimize", "path", "experimental_codegen"];
31
32const DOC_KEYS: &[&str] = &["out", "title", "book", "homepage", "repository", "path", "ignore"];
36
37const RESERVED_KEYS: &[&str] = &["extends"];
39
40const BACKWARD_COMPATIBLE_KEYS: &[&str] = &["solc_version"];
42
43pub 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 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 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 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 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 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 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 self.collect_nested_section_warnings(
159 profile_dict,
160 default_dict,
161 &source,
162 &mut out,
163 );
164 }
165 }
166
167 self.collect_standalone_section_warnings(&data, default_dict, &mut out);
169 }
170
171 Ok(out)
172 }
173
174 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 let section_profile = Profile::new(section_name);
191 let Some(section_dict) = data.get(§ion_profile) else {
192 continue;
193 };
194
195 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 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 for (key, value) in profile_dict {
237 let Some(nested_dict) = value.as_dict() else {
238 if let Some(arr) = value.as_array() {
240 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 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 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}