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 (key == *deprecated_key).then(|| Warning::DeprecatedKey {
93 old: deprecated_key.to_string(),
94 new: new_value.to_string(),
95 })
96 })
97 };
98 let profiles = data
99 .iter()
100 .filter(|(profile, _)| **profile == Config::PROFILE_SECTION)
101 .map(|(_, dict)| dict);
102
103 out.extend(profiles.clone().flat_map(BTreeMap::keys).filter_map(deprecated_key_warning));
104 out.extend(
105 profiles
106 .clone()
107 .filter_map(|dict| dict.get(self.profile.as_str().as_str()))
108 .filter_map(Value::as_dict)
109 .flat_map(BTreeMap::keys)
110 .filter_map(deprecated_key_warning),
111 );
112
113 if let Ok(default_map) = figment::providers::Serialized::defaults(&Config::default()).data()
115 && let Some(default_dict) = default_map.get(&Config::DEFAULT_PROFILE)
116 {
117 let allowed_keys: BTreeSet<String> = default_dict.keys().cloned().collect();
118 for profile_map in profiles.clone() {
119 for (profile, value) in profile_map {
120 let Some(profile_dict) = value.as_dict() else {
121 continue;
122 };
123
124 let source = self
125 .provider
126 .metadata()
127 .source
128 .map(|s| s.to_string())
129 .unwrap_or(Config::FILE_NAME.to_string());
130 for key in profile_dict.keys() {
131 let is_not_deprecated =
132 !DEPRECATIONS.iter().any(|(deprecated_key, _)| *deprecated_key == key);
133 let is_not_allowed = !allowed_keys.contains(key)
134 && !allowed_keys.contains(&key.to_snake_case());
135 let is_not_reserved =
136 !RESERVED_KEYS.contains(&key.as_str()) && key != Self::WARNINGS_KEY;
137 let is_not_backward_compatible =
138 !BACKWARD_COMPATIBLE_KEYS.contains(&key.as_str());
139
140 if is_not_deprecated
141 && is_not_allowed
142 && is_not_reserved
143 && is_not_backward_compatible
144 {
145 out.push(Warning::UnknownKey {
146 key: key.clone(),
147 profile: profile.clone(),
148 source: source.clone(),
149 });
150 }
151 }
152
153 self.collect_nested_section_warnings(
155 profile_dict,
156 default_dict,
157 &source,
158 &mut out,
159 );
160 }
161 }
162
163 self.collect_standalone_section_warnings(&data, default_dict, &mut out);
165 }
166
167 Ok(out)
168 }
169
170 fn collect_standalone_section_warnings(
172 &self,
173 data: &Map<Profile, Dict>,
174 default_dict: &Dict,
175 out: &mut Vec<Warning>,
176 ) {
177 let source = self
178 .provider
179 .metadata()
180 .source
181 .map(|s| s.to_string())
182 .unwrap_or(Config::FILE_NAME.to_string());
183
184 for section_name in Config::STANDALONE_SECTIONS {
185 let section_profile = Profile::new(section_name);
187 let Some(section_dict) = data.get(§ion_profile) else {
188 continue;
189 };
190
191 let allowed_keys: BTreeSet<String> = if *section_name == "vyper" {
195 VYPER_KEYS.iter().map(|s| s.to_string()).collect()
196 } else if *section_name == "doc" {
197 DOC_KEYS.iter().map(|s| s.to_string()).collect()
198 } else {
199 let Some(default_section_value) = default_dict.get(*section_name) else {
200 continue;
201 };
202 let Some(default_section_dict) = default_section_value.as_dict() else {
203 continue;
204 };
205 default_section_dict.keys().cloned().collect()
206 };
207
208 for key in section_dict.keys() {
209 let is_not_allowed =
210 !allowed_keys.contains(key) && !allowed_keys.contains(&key.to_snake_case());
211 if is_not_allowed {
212 out.push(Warning::UnknownSectionKey {
213 key: key.clone(),
214 section: section_name.to_string(),
215 source: source.clone(),
216 });
217 }
218 }
219 }
220 }
221
222 fn collect_nested_section_warnings(
225 &self,
226 profile_dict: &Dict,
227 default_dict: &Dict,
228 source: &str,
229 out: &mut Vec<Warning>,
230 ) {
231 for (key, value) in profile_dict {
233 let Some(nested_dict) = value.as_dict() else {
234 if let Some(arr) = value.as_array() {
236 let allowed_keys = Self::get_array_item_allowed_keys(key);
238
239 if allowed_keys.is_empty() {
240 continue;
241 }
242
243 for item in arr {
244 let Some(item_dict) = item.as_dict() else {
245 continue;
246 };
247 for item_key in item_dict.keys() {
248 let is_not_allowed = !allowed_keys.contains(item_key)
249 && !allowed_keys.contains(&item_key.to_snake_case());
250 if is_not_allowed {
251 out.push(Warning::UnknownSectionKey {
252 key: item_key.clone(),
253 section: key.clone(),
254 source: source.to_string(),
255 });
256 }
257 }
258 }
259 }
260 continue;
261 };
262
263 let allowed_keys: BTreeSet<String> = if key == "vyper" {
267 VYPER_KEYS.iter().map(|s| s.to_string()).collect()
268 } else if key == "doc" {
269 DOC_KEYS.iter().map(|s| s.to_string()).collect()
270 } else {
271 let Some(default_value) = default_dict.get(key) else {
272 continue;
273 };
274 let Some(default_nested_dict) = default_value.as_dict() else {
275 continue;
276 };
277 default_nested_dict.keys().cloned().collect()
278 };
279
280 for nested_key in nested_dict.keys() {
281 let is_not_allowed = !allowed_keys.contains(nested_key)
282 && !allowed_keys.contains(&nested_key.to_snake_case());
283 if is_not_allowed {
284 out.push(Warning::UnknownSectionKey {
285 key: nested_key.clone(),
286 section: key.clone(),
287 source: source.to_string(),
288 });
289 }
290 }
291 }
292 }
293
294 fn get_array_item_allowed_keys(section_name: &str) -> BTreeSet<String> {
296 match section_name {
297 "compilation_restrictions" => {
298 COMPILATION_RESTRICTIONS_KEYS.iter().map(|s| s.to_string()).collect()
299 }
300 "additional_compiler_profiles" => {
301 SETTINGS_OVERRIDES_KEYS.iter().map(|s| s.to_string()).collect()
302 }
303 _ => BTreeSet::new(),
304 }
305 }
306}
307
308impl<P: Provider> Provider for WarningsProvider<P> {
309 fn metadata(&self) -> Metadata {
310 if let Some(source) = self.provider.metadata().source {
311 Metadata::from("Warnings", source)
312 } else {
313 Metadata::named("Warnings")
314 }
315 }
316
317 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
318 let warnings = self.collect_warnings()?;
319 Ok(Map::from([(
320 self.profile.clone(),
321 Dict::from([(Self::WARNINGS_KEY.to_string(), Value::serialize(warnings)?)]),
322 )]))
323 }
324
325 fn profile(&self) -> Option<Profile> {
326 Some(self.profile.clone())
327 }
328}