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 EXTERNAL_COMPILER_KEYS: &[&str] = &["id", "command", "args", "roots", "settings"];
29
30const 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
45const DOC_KEYS: &[&str] =
50 &["out", "title", "book", "homepage", "repository", "commit", "path", "ignore"];
51
52const 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
90const 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
103const RESERVED_KEYS: &[&str] = &["extends"];
105
106const 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
121pub 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 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 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 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 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 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 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 self.collect_nested_section_warnings(
232 profile_dict,
233 default_dict,
234 &source,
235 &mut out,
236 );
237 }
238 }
239
240 self.collect_standalone_section_warnings(&data, default_dict, &mut out);
242 }
243
244 Ok(out)
245 }
246
247 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 let section_profile = Profile::new(section_name);
264 let Some(section_dict) = data.get(§ion_profile) else {
265 continue;
266 };
267
268 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 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 for (key, value) in profile_dict {
314 let Some(nested_dict) = value.as_dict() else {
315 if let Some(arr) = value.as_array() {
317 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 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 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}