1use super::{
2 data::{ContractData, PreprocessorData},
3 span_to_range,
4};
5use foundry_compilers::Updates;
6use itertools::Itertools;
7use solar::sema::{
8 Gcx, Hir,
9 hir::{
10 CallArgs, CallOptions, ContractId, Expr, ExprKind, Function, FunctionKind, StateMutability,
11 Stmt, StmtKind, TypeKind, Visit,
12 },
13 interface::{SourceMap, data_structures::Never, source_map::FileName},
14};
15use std::{
16 collections::{BTreeMap, BTreeSet, HashSet},
17 ops::{ControlFlow, Range},
18 path::{Path, PathBuf},
19};
20
21pub(crate) struct PreprocessorDependencies {
23 pub preprocessed_contracts: BTreeMap<ContractId, Vec<BytecodeDependency>>,
25 pub referenced_contracts: HashSet<ContractId>,
27}
28
29impl PreprocessorDependencies {
30 pub fn new(
31 gcx: Gcx<'_>,
32 paths: &[PathBuf],
33 script_paths: &HashSet<PathBuf>,
34 src_dir: &Path,
35 root_dir: &Path,
36 mocks: &mut HashSet<PathBuf>,
37 ) -> Self {
38 let mut preprocessed_contracts = BTreeMap::new();
39 let mut referenced_contracts = HashSet::new();
40 let mut current_mocks = HashSet::new();
41
42 let candidate_contracts = || {
44 gcx.hir.contract_ids().filter_map(|id| {
45 let contract = gcx.hir.contract(id);
46 let source = gcx.hir.source(contract.source);
47 let FileName::Real(path) = &source.file.name else {
48 return None;
49 };
50
51 if !paths.contains(path) {
52 trace!("{} is not test or script", path.display());
53 return None;
54 }
55
56 Some((id, contract, source, path))
57 })
58 };
59
60 for (_, contract, _, path) in candidate_contracts() {
62 if contract.linearized_bases.iter().any(|base_id| {
63 let base = gcx.hir.contract(*base_id);
64 matches!(
65 &gcx.hir.source(base.source).file.name,
66 FileName::Real(base_path) if base_path.starts_with(src_dir)
67 )
68 }) {
69 let mock_path = root_dir.join(path);
70 trace!("found mock contract {}", mock_path.display());
71 current_mocks.insert(mock_path);
72 }
73 }
74
75 for (contract_id, contract, source, path) in candidate_contracts() {
77 let full_path = root_dir.join(path);
78
79 if current_mocks.contains(&full_path) {
80 trace!("{} is a mock, skipping", path.display());
81 continue;
82 }
83
84 mocks.remove(&full_path);
87
88 let is_script = script_paths.contains(path)
92 || contract
93 .linearized_bases
94 .iter()
95 .skip(1)
96 .any(|base_id| gcx.hir.contract(*base_id).name.as_str() == "Script");
97 let mut deps_collector =
98 BytecodeDependencyCollector::new(gcx, source.file.src.as_str(), src_dir, is_script);
99 let _ = deps_collector.walk_contract(contract);
101 if !deps_collector.dependencies.is_empty() {
103 preprocessed_contracts.insert(contract_id, deps_collector.dependencies);
104 }
105
106 referenced_contracts.extend(deps_collector.referenced_contracts);
108 }
109
110 mocks.extend(current_mocks);
112
113 Self { preprocessed_contracts, referenced_contracts }
114 }
115}
116
117#[derive(Debug)]
119enum BytecodeDependencyKind {
120 CreationCode,
122 New {
124 name: String,
126 args_length: usize,
128 call_args_offset: usize,
130 value: Option<String>,
132 salt: Option<String>,
134 try_stmt: Option<bool>,
136 },
137}
138
139#[derive(Debug)]
141pub(crate) struct BytecodeDependency {
142 kind: BytecodeDependencyKind,
144 loc: Range<usize>,
146 referenced_contract: ContractId,
148}
149
150struct BytecodeDependencyCollector<'gcx, 'src> {
152 gcx: Gcx<'gcx>,
154 src: &'src str,
156 src_dir: &'src Path,
158 is_script: bool,
163 preserve_native_creation_code: bool,
165 dependencies: Vec<BytecodeDependency>,
167 referenced_contracts: HashSet<ContractId>,
169}
170
171impl<'gcx, 'src> BytecodeDependencyCollector<'gcx, 'src> {
172 fn new(gcx: Gcx<'gcx>, src: &'src str, src_dir: &'src Path, is_script: bool) -> Self {
173 Self {
174 gcx,
175 src,
176 src_dir,
177 is_script,
178 preserve_native_creation_code: false,
179 dependencies: vec![],
180 referenced_contracts: HashSet::default(),
181 }
182 }
183
184 fn collect_dependency(&mut self, dependency: BytecodeDependency) {
188 if self.is_script {
190 match &dependency.kind {
191 BytecodeDependencyKind::CreationCode => {
192 trace!("skip creationCode in script");
193 return;
194 }
195 BytecodeDependencyKind::New { .. } => {
196 trace!("skip new-expression in script");
197 return;
198 }
199 }
200 }
201
202 if self.preserve_native_creation_code
205 && matches!(&dependency.kind, BytecodeDependencyKind::CreationCode)
206 {
207 trace!("skip creationCode in native creationCode context");
208 return;
209 }
210
211 let contract = self.gcx.hir.contract(dependency.referenced_contract);
212 let source = self.gcx.hir.source(contract.source);
213 let FileName::Real(path) = &source.file.name else {
214 return;
215 };
216
217 if !path.starts_with(self.src_dir) {
218 let path = path.display();
219 trace!("ignore dependency {path}");
220 return;
221 }
222
223 self.referenced_contracts.insert(dependency.referenced_contract);
224 self.dependencies.push(dependency);
225 }
226}
227
228impl<'gcx> Visit<'gcx> for BytecodeDependencyCollector<'gcx, '_> {
229 type BreakValue = Never;
230
231 fn hir(&self) -> &'gcx Hir<'gcx> {
232 &self.gcx.hir
233 }
234
235 fn visit_function(&mut self, func: &'gcx Function<'gcx>) -> ControlFlow<Self::BreakValue> {
236 let previous = self.preserve_native_creation_code;
237 self.preserve_native_creation_code = previous
238 || func.state_mutability == StateMutability::Pure
239 || matches!(func.kind, FunctionKind::Modifier);
240 self.walk_function(func)?;
241 self.preserve_native_creation_code = previous;
242 ControlFlow::Continue(())
243 }
244
245 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
246 #[allow(clippy::collapsible_match)]
247 match &expr.kind {
248 ExprKind::Call(call_expr, call_args, named_args) => {
249 if let Some(dependency) = handle_call_expr(
250 self.src,
251 self.gcx.sess.source_map(),
252 expr,
253 call_expr,
254 call_args,
255 named_args,
256 ) {
257 self.collect_dependency(dependency);
258 }
259 }
260 ExprKind::Member(member_expr, ident) => {
261 if let ExprKind::TypeCall(ty) = &member_expr.kind
262 && let TypeKind::Custom(contract_id) = &ty.kind
263 && ident.name.as_str() == "creationCode"
264 && let Some(contract_id) = contract_id.as_contract()
265 {
266 self.collect_dependency(BytecodeDependency {
267 kind: BytecodeDependencyKind::CreationCode,
268 loc: span_to_range(self.gcx.sess.source_map(), expr.span),
269 referenced_contract: contract_id,
270 });
271 }
272 }
273 _ => {}
274 }
275 self.walk_expr(expr)
276 }
277
278 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
279 if let StmtKind::Try(stmt_try) = stmt.kind
280 && let ExprKind::Call(call_expr, call_args, named_args) = &stmt_try.expr.kind
281 && let Some(mut dependency) = handle_call_expr(
282 self.src,
283 self.gcx.sess.source_map(),
284 &stmt_try.expr,
285 call_expr,
286 call_args,
287 named_args,
288 )
289 {
290 let has_custom_return = if let Some(clause) = stmt_try.clauses.first()
291 && clause.args.len() == 1
292 && let Some(ret_var) = clause.args.first()
293 && let TypeKind::Custom(_) = self.hir().variable(*ret_var).ty.kind
294 {
295 true
296 } else {
297 false
298 };
299
300 if let BytecodeDependencyKind::New { try_stmt, .. } = &mut dependency.kind {
301 *try_stmt = Some(has_custom_return);
302 }
303 self.collect_dependency(dependency);
304
305 for clause in stmt_try.clauses {
306 for &var in clause.args {
307 self.visit_nested_var(var)?;
308 }
309 for stmt in clause.block.stmts {
310 self.visit_stmt(stmt)?;
311 }
312 }
313 return ControlFlow::Continue(());
314 }
315 self.walk_stmt(stmt)
316 }
317}
318
319fn handle_call_expr(
321 src: &str,
322 source_map: &SourceMap,
323 parent_expr: &Expr<'_>,
324 call_expr: &Expr<'_>,
325 call_args: &CallArgs<'_>,
326 call_options: &Option<&CallOptions<'_>>,
327) -> Option<BytecodeDependency> {
328 if let ExprKind::New(ty_new) = &call_expr.kind
329 && let TypeKind::Custom(item_id) = ty_new.kind
330 && let Some(contract_id) = item_id.as_contract()
331 {
332 let name_loc = span_to_range(source_map, ty_new.span);
333 let name = &src[name_loc];
334
335 let call_args_offset = if call_options.is_some() && !call_args.is_empty() {
339 (call_args.span.lo() - ty_new.span.hi()).to_usize()
340 } else {
341 0
342 };
343
344 let args_len = parent_expr.span.hi() - ty_new.span.hi();
345 return Some(BytecodeDependency {
346 kind: BytecodeDependencyKind::New {
347 name: name.to_string(),
348 args_length: args_len.to_usize(),
349 call_args_offset,
350 value: named_arg(src, call_options, "value", source_map),
351 salt: named_arg(src, call_options, "salt", source_map),
352 try_stmt: None,
353 },
354 loc: span_to_range(source_map, call_expr.span),
355 referenced_contract: contract_id,
356 });
357 }
358 None
359}
360
361fn named_arg(
363 src: &str,
364 call_options: &Option<&CallOptions<'_>>,
365 arg: &str,
366 source_map: &SourceMap,
367) -> Option<String> {
368 call_options
369 .map(|options| options.args)
370 .unwrap_or_default()
371 .iter()
372 .find(|named_arg| named_arg.name.as_str() == arg)
373 .map(|named_arg| {
374 let named_arg_loc = span_to_range(source_map, named_arg.value.span);
375 src[named_arg_loc].to_string()
376 })
377}
378
379pub(crate) fn remove_bytecode_dependencies(
393 gcx: Gcx<'_>,
394 deps: &PreprocessorDependencies,
395 data: &PreprocessorData,
396) -> Updates {
397 let mut updates = Updates::default();
398 for (contract_id, deps) in &deps.preprocessed_contracts {
399 let contract = gcx.hir.contract(*contract_id);
400 let source = gcx.hir.source(contract.source);
401 let FileName::Real(path) = &source.file.name else {
402 continue;
403 };
404
405 let updates = updates.entry(path.clone()).or_default();
406 let mut used_helpers = BTreeSet::new();
407
408 let vm_interface_name = format!("VmContractHelper{}", contract_id.index());
409 let vm = format!("{vm_interface_name}(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)");
411 let mut try_catch_helpers: HashSet<&str> = HashSet::default();
412
413 for dep in deps {
414 let Some(ContractData { artifact, constructor_data, .. }) =
415 data.get(&dep.referenced_contract)
416 else {
417 continue;
418 };
419
420 match &dep.kind {
421 BytecodeDependencyKind::CreationCode => {
422 updates.insert((
424 dep.loc.start,
425 dep.loc.end,
426 format!("{vm}.getCode(\"{artifact}\")"),
427 ));
428 }
429 BytecodeDependencyKind::New {
430 name,
431 args_length,
432 call_args_offset,
433 value,
434 salt,
435 try_stmt,
436 } => {
437 let (mut update, closing_seq) = if let Some(has_ret) = try_stmt {
438 if *has_ret {
439 try_catch_helpers.insert(name);
441 (format!("this.addressTo{name}{id}(", id = contract_id.index()), "}))")
442 } else {
443 (String::new(), "})")
444 }
445 } else {
446 (format!("{name}(payable("), "})))")
447 };
448 update.push_str(&format!("{vm}.deployCode({{"));
449 update.push_str(&format!("_artifact: \"{artifact}\""));
450
451 if let Some(value) = value {
452 update.push_str(", ");
453 update.push_str(&format!("_value: {value}"));
454 }
455
456 if let Some(salt) = salt {
457 update.push_str(", ");
458 update.push_str(&format!("_salt: {salt}"));
459 }
460
461 if constructor_data.is_some() {
462 used_helpers.insert(dep.referenced_contract);
464
465 update.push_str(", ");
466 update.push_str(&format!(
467 "_args: encodeArgs{id}(DeployHelper{id}.FoundryPpConstructorArgs",
468 id = dep.referenced_contract.index()
469 ));
470 updates.insert((dep.loc.start, dep.loc.end + call_args_offset, update));
471
472 updates.insert((
473 dep.loc.end + args_length,
474 dep.loc.end + args_length,
475 format!("){closing_seq}"),
476 ));
477 } else {
478 update.push_str(closing_seq);
479 updates.insert((dep.loc.start, dep.loc.end + args_length, update));
480 }
481 }
482 };
483 }
484
485 if !try_catch_helpers.is_empty()
487 && let Some(last_fn_id) = contract.functions().last()
488 {
489 let last_fn_range =
490 span_to_range(gcx.sess.source_map(), gcx.hir.function(last_fn_id).span);
491 let to_address_fns = try_catch_helpers
492 .iter()
493 .map(|ty| {
494 format!(
495 r#"
496 function addressTo{ty}{id}(address addr) public pure returns ({ty}) {{
497 return {ty}(addr);
498 }}
499 "#,
500 id = contract_id.index()
501 )
502 })
503 .collect::<String>();
504
505 updates.insert((last_fn_range.end, last_fn_range.end, to_address_fns));
506 }
507
508 let helper_imports = used_helpers.into_iter().map(|id| {
509 let id = id.index();
510 format!(
511 "import {{DeployHelper{id}, encodeArgs{id}}} from \"foundry-pp/DeployHelper{id}.sol\";",
512 )
513 }).join("\n");
514 updates.insert((
515 source.file.src.len(),
516 source.file.src.len(),
517 format!(
518 r#"
519{helper_imports}
520
521interface {vm_interface_name} {{
522 function deployCode(string memory _artifact) external returns (address);
523 function deployCode(string memory _artifact, bytes32 _salt) external returns (address);
524 function deployCode(string memory _artifact, bytes memory _args) external returns (address);
525 function deployCode(string memory _artifact, bytes memory _args, bytes32 _salt) external returns (address);
526 function deployCode(string memory _artifact, uint256 _value) external returns (address);
527 function deployCode(string memory _artifact, uint256 _value, bytes32 _salt) external returns (address);
528 function deployCode(string memory _artifact, bytes memory _args, uint256 _value) external returns (address);
529 function deployCode(string memory _artifact, bytes memory _args, uint256 _value, bytes32 _salt) external returns (address);
530 function getCode(string memory _artifact) external view returns (bytes memory);
531}}"#
532 ),
533 ));
534 }
535 updates
536}