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