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