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 RESERVED_KEYS: &[&str] = &["extends"];
34
35const BACKWARD_COMPATIBLE_KEYS: &[&str] = &["solc_version"];
37
38pub struct WarningsProvider<P> {
40 provider: P,
41 profile: Profile,
42 old_warnings: Result<Vec<Warning>, Error>,
43}
44
45impl<P: Provider> WarningsProvider<P> {
46 const WARNINGS_KEY: &'static str = "__warnings";
47
48 pub fn new(
50 provider: P,
51 profile: impl Into<Profile>,
52 old_warnings: Result<Vec<Warning>, Error>,
53 ) -> Self {
54 Self { provider, profile: profile.into(), old_warnings }
55 }
56
57 pub fn for_figment(provider: P, figment: &Figment) -> Self {
59 let old_warnings = {
60 let warnings_res = figment.extract_inner(Self::WARNINGS_KEY);
61 if warnings_res.as_ref().err().map(|err| err.missing()).unwrap_or(false) {
62 Ok(vec![])
63 } else {
64 warnings_res
65 }
66 };
67 Self::new(provider, figment.profile().clone(), old_warnings)
68 }
69
70 pub fn collect_warnings(&self) -> Result<Vec<Warning>, Error> {
72 let data = self.provider.data().unwrap_or_default();
73
74 let mut out = self.old_warnings.clone()?;
75
76 out.extend(data.keys().filter(|k| !Config::is_standalone_section(k.as_str())).map(
78 |unknown_section| {
79 let source = self.provider.metadata().source.map(|s| s.to_string());
80 Warning::UnknownSection { unknown_section: unknown_section.clone(), source }
81 },
82 ));
83
84 let deprecated_key_warning = |key| {
86 DEPRECATIONS.iter().find_map(|(deprecated_key, new_value)| {
87 if key == *deprecated_key {
88 Some(Warning::DeprecatedKey {
89 old: deprecated_key.to_string(),
90 new: new_value.to_string(),
91 })
92 } else {
93 None
94 }
95 })
96 };
97 let profiles = data
98 .iter()
99 .filter(|(profile, _)| **profile == Config::PROFILE_SECTION)
100 .map(|(_, dict)| dict);
101
102 out.extend(profiles.clone().flat_map(BTreeMap::keys).filter_map(deprecated_key_warning));
103 out.extend(
104 profiles
105 .clone()
106 .filter_map(|dict| dict.get(self.profile.as_str().as_str()))
107 .filter_map(Value::as_dict)
108 .flat_map(BTreeMap::keys)
109 .filter_map(deprecated_key_warning),
110 );
111
112 if let Ok(default_map) = figment::providers::Serialized::defaults(&Config::default()).data()
114 && let Some(default_dict) = default_map.get(&Config::DEFAULT_PROFILE)
115 {
116 let allowed_keys: BTreeSet<String> = default_dict.keys().cloned().collect();
117 for profile_map in profiles.clone() {
118 for (profile, value) in profile_map {
119 let Some(profile_dict) = value.as_dict() else {
120 continue;
121 };
122
123 let source = self
124 .provider
125 .metadata()
126 .source
127 .map(|s| s.to_string())
128 .unwrap_or(Config::FILE_NAME.to_string());
129 for key in profile_dict.keys() {
130 let is_not_deprecated =
131 !DEPRECATIONS.iter().any(|(deprecated_key, _)| *deprecated_key == key);
132 let is_not_allowed = !allowed_keys.contains(key)
133 && !allowed_keys.contains(&key.to_snake_case());
134 let is_not_reserved =
135 !RESERVED_KEYS.contains(&key.as_str()) && key != Self::WARNINGS_KEY;
136 let is_not_backward_compatible =
137 !BACKWARD_COMPATIBLE_KEYS.contains(&key.as_str());
138
139 if is_not_deprecated
140 && is_not_allowed
141 && is_not_reserved
142 && is_not_backward_compatible
143 {
144 out.push(Warning::UnknownKey {
145 key: key.clone(),
146 profile: profile.clone(),
147 source: source.clone(),
148 });
149 }
150 }
151
152 self.collect_nested_section_warnings(
154 profile_dict,
155 default_dict,
156 &source,
157 &mut out,
158 );
159 }
160 }
161
162 self.collect_standalone_section_warnings(&data, default_dict, &mut out);
164 }
165
166 Ok(out)
167 }
168
169 fn collect_standalone_section_warnings(
171 &self,
172 data: &Map<Profile, Dict>,
173 default_dict: &Dict,
174 out: &mut Vec<Warning>,
175 ) {
176 let source = self
177 .provider
178 .metadata()
179 .source
180 .map(|s| s.to_string())
181 .unwrap_or(Config::FILE_NAME.to_string());
182
183 for section_name in Config::STANDALONE_SECTIONS {
184 let section_profile = Profile::new(section_name);
186 let Some(section_dict) = data.get(§ion_profile) else {
187 continue;
188 };
189
190 let allowed_keys: BTreeSet<String> = if *section_name == "vyper" {
194 VYPER_KEYS.iter().map(|s| s.to_string()).collect()
195 } else {
196 let Some(default_section_value) = default_dict.get(*section_name) else {
197 continue;
198 };
199 let Some(default_section_dict) = default_section_value.as_dict() else {
200 continue;
201 };
202 default_section_dict.keys().cloned().collect()
203 };
204
205 for key in section_dict.keys() {
206 let is_not_allowed =
207 !allowed_keys.contains(key) && !allowed_keys.contains(&key.to_snake_case());
208 if is_not_allowed {
209 out.push(Warning::UnknownSectionKey {
210 key: key.clone(),
211 section: section_name.to_string(),
212 source: source.clone(),
213 });
214 }
215 }
216 }
217 }
218
219 fn collect_nested_section_warnings(
222 &self,
223 profile_dict: &Dict,
224 default_dict: &Dict,
225 source: &str,
226 out: &mut Vec<Warning>,
227 ) {
228 for (key, value) in profile_dict {
230 let Some(nested_dict) = value.as_dict() else {
231 if let Some(arr) = value.as_array() {
233 let allowed_keys = Self::get_array_item_allowed_keys(key);
235
236 if allowed_keys.is_empty() {
237 continue;
238 }
239
240 for item in arr {
241 let Some(item_dict) = item.as_dict() else {
242 continue;
243 };
244 for item_key in item_dict.keys() {
245 let is_not_allowed = !allowed_keys.contains(item_key)
246 && !allowed_keys.contains(&item_key.to_snake_case());
247 if is_not_allowed {
248 out.push(Warning::UnknownSectionKey {
249 key: item_key.clone(),
250 section: key.clone(),
251 source: source.to_string(),
252 });
253 }
254 }
255 }
256 }
257 continue;
258 };
259
260 let allowed_keys: BTreeSet<String> = if key == "vyper" {
264 VYPER_KEYS.iter().map(|s| s.to_string()).collect()
265 } else {
266 let Some(default_value) = default_dict.get(key) else {
267 continue;
268 };
269 let Some(default_nested_dict) = default_value.as_dict() else {
270 continue;
271 };
272 default_nested_dict.keys().cloned().collect()
273 };
274
275 for nested_key in nested_dict.keys() {
276 let is_not_allowed = !allowed_keys.contains(nested_key)
277 && !allowed_keys.contains(&nested_key.to_snake_case());
278 if is_not_allowed {
279 out.push(Warning::UnknownSectionKey {
280 key: nested_key.clone(),
281 section: key.clone(),
282 source: source.to_string(),
283 });
284 }
285 }
286 }
287 }
288
289 fn get_array_item_allowed_keys(section_name: &str) -> BTreeSet<String> {
291 match section_name {
292 "compilation_restrictions" => {
293 COMPILATION_RESTRICTIONS_KEYS.iter().map(|s| s.to_string()).collect()
294 }
295 "additional_compiler_profiles" => {
296 SETTINGS_OVERRIDES_KEYS.iter().map(|s| s.to_string()).collect()
297 }
298 _ => BTreeSet::new(),
299 }
300 }
301}
302
303impl<P: Provider> Provider for WarningsProvider<P> {
304 fn metadata(&self) -> Metadata {
305 if let Some(source) = self.provider.metadata().source {
306 Metadata::from("Warnings", source)
307 } else {
308 Metadata::named("Warnings")
309 }
310 }
311
312 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
313 let warnings = self.collect_warnings()?;
314 Ok(Map::from([(
315 self.profile.clone(),
316 Dict::from([(Self::WARNINGS_KEY.to_string(), Value::serialize(warnings)?)]),
317 )]))
318 }
319
320 fn profile(&self) -> Option<Profile> {
321 Some(self.profile.clone())
322 }
323}