1use super::{
2 data::{ContractData, PreprocessorData},
3 span_to_range,
4};
5use crate::fs::normalize_path;
6use foundry_compilers::{
7 ProjectPathsConfig, Updates,
8 artifacts::{SolcLanguage, remappings::Remapping},
9};
10use itertools::Itertools;
11use path_slash::PathExt;
12use solar::sema::{
13 Gcx, Hir,
14 hir::{
15 CallArgs, CallOptions, ContractId, Expr, ExprKind, Function, FunctionKind, StateMutability,
16 Stmt, StmtKind, TypeKind, Visit,
17 },
18 interface::{SourceMap, data_structures::Never, source_map::FileName},
19};
20use std::{
21 collections::{BTreeMap, BTreeSet, HashSet},
22 ops::{ControlFlow, Range},
23 path::{Path, PathBuf},
24};
25
26pub(crate) struct PreprocessorDependencies {
28 pub preprocessed_contracts: BTreeMap<ContractId, Vec<BytecodeDependency>>,
30 pub referenced_contracts: HashSet<ContractId>,
32}
33
34impl PreprocessorDependencies {
35 pub fn new(
36 gcx: Gcx<'_>,
37 paths: &[PathBuf],
38 script_paths: &HashSet<PathBuf>,
39 project_paths: &ProjectPathsConfig<SolcLanguage>,
40 source_units: &[PathBuf],
41 mocks: &mut HashSet<PathBuf>,
42 ) -> Self {
43 let relative_paths = project_paths.paths_relative();
44 let src_dir = &relative_paths.sources;
45 let root_dir = &project_paths.root;
46 let remappings = &project_paths.remappings;
47 let mut preprocessed_contracts = BTreeMap::new();
48 let mut referenced_contracts = HashSet::new();
49 let mut current_mocks = HashSet::new();
50 let mut candidate_files = HashSet::new();
51
52 let candidate_contracts = || {
54 gcx.hir.contract_ids().filter_map(|id| {
55 let contract = gcx.hir.contract(id);
56 let source = gcx.hir.source(contract.source);
57 let FileName::Real(path) = &source.file.name else {
58 return None;
59 };
60
61 if !paths.contains(path) {
62 trace!("{} is not test or script", path.display());
63 return None;
64 }
65
66 Some((id, contract, source, path))
67 })
68 };
69
70 for (_, contract, _, path) in candidate_contracts() {
72 if contract.linearized_bases.iter().any(|base_id| {
73 let base = gcx.hir.contract(*base_id);
74 matches!(
75 &gcx.hir.source(base.source).file.name,
76 FileName::Real(base_path)
77 if is_path_in_dir(base_path, src_dir, root_dir)
78 )
79 }) {
80 let mock_path = root_dir.join(path);
81 trace!("found mock contract {}", mock_path.display());
82 current_mocks.insert(mock_path);
83 }
84 }
85
86 for (contract_id, contract, source, path) in candidate_contracts() {
88 let full_path = root_dir.join(path);
89 candidate_files.insert(full_path.clone());
90
91 if current_mocks.contains(&full_path) {
92 trace!("{} is a mock, skipping", path.display());
93 continue;
94 }
95
96 let is_script = script_paths.contains(path)
100 || contract
101 .linearized_bases
102 .iter()
103 .skip(1)
104 .any(|base_id| gcx.hir.contract(*base_id).name.as_str() == "Script");
105 let mut deps_collector = BytecodeDependencyCollector::new(
106 gcx,
107 source.file.src.as_str(),
108 src_dir,
109 root_dir,
110 is_script,
111 );
112 let _ = deps_collector.walk_contract(contract);
114 let keep_native = (!deps_collector.dependencies.is_empty()
115 && mocks.contains(&full_path))
116 || deps_collector.dependencies.iter().any(|dependency| {
117 let dependency_id = dependency.referenced_contract;
118 let dependency = gcx.hir.contract(dependency_id);
119 let dependency_source = gcx.hir.source(dependency.source);
120 let FileName::Real(dependency_path) = &dependency_source.file.name else {
121 return true;
122 };
123 let has_constructor_args = dependency
124 .ctor
125 .is_some_and(|ctor_id| !gcx.hir.function(ctor_id).parameters.is_empty());
126 !can_rewrite(
127 dependency_path,
128 path,
129 root_dir,
130 source_units,
131 remappings,
132 has_constructor_args,
133 dependency_id,
134 )
135 });
136 if keep_native {
137 trace!("{} has an unsafe bytecode dependency, keeping it native", path.display());
138 current_mocks.insert(full_path.clone());
139 preprocessed_contracts.retain(|contract_id, _| {
140 let source = gcx.hir.source(gcx.hir.contract(*contract_id).source);
141 !matches!(&source.file.name, FileName::Real(path) if root_dir.join(path) == full_path)
142 });
143 continue;
144 }
145 if !deps_collector.dependencies.is_empty() {
147 preprocessed_contracts.insert(contract_id, deps_collector.dependencies);
148 }
149 }
150
151 for file in candidate_files {
154 mocks.remove(&file);
155 }
156 mocks.extend(current_mocks);
157
158 for dependencies in preprocessed_contracts.values() {
159 referenced_contracts.extend(dependencies.iter().map(|dep| dep.referenced_contract));
160 }
161
162 Self { preprocessed_contracts, referenced_contracts }
163 }
164}
165
166#[derive(Debug)]
168enum BytecodeDependencyKind {
169 CreationCode,
171 New {
173 name: String,
175 args_length: usize,
177 call_args_offset: usize,
179 value: Option<String>,
181 salt: Option<String>,
183 try_stmt: Option<bool>,
185 },
186}
187
188#[derive(Debug)]
190pub(crate) struct BytecodeDependency {
191 kind: BytecodeDependencyKind,
193 loc: Range<usize>,
195 referenced_contract: ContractId,
197}
198
199struct BytecodeDependencyCollector<'gcx, 'src> {
201 gcx: Gcx<'gcx>,
203 src: &'src str,
205 src_dir: &'src Path,
207 root_dir: &'src Path,
209 is_script: bool,
214 preserve_native_creation_code: bool,
216 dependencies: Vec<BytecodeDependency>,
218}
219
220impl<'gcx, 'src> BytecodeDependencyCollector<'gcx, 'src> {
221 const fn new(
222 gcx: Gcx<'gcx>,
223 src: &'src str,
224 src_dir: &'src Path,
225 root_dir: &'src Path,
226 is_script: bool,
227 ) -> Self {
228 Self {
229 gcx,
230 src,
231 src_dir,
232 root_dir,
233 is_script,
234 preserve_native_creation_code: false,
235 dependencies: vec![],
236 }
237 }
238
239 fn collect_dependency(&mut self, dependency: BytecodeDependency) {
243 if self.is_script {
245 match &dependency.kind {
246 BytecodeDependencyKind::CreationCode => {
247 trace!("skip creationCode in script");
248 return;
249 }
250 BytecodeDependencyKind::New { .. } => {
251 trace!("skip new-expression in script");
252 return;
253 }
254 }
255 }
256
257 if self.preserve_native_creation_code
260 && matches!(&dependency.kind, BytecodeDependencyKind::CreationCode)
261 {
262 trace!("skip creationCode in native creationCode context");
263 return;
264 }
265
266 let contract = self.gcx.hir.contract(dependency.referenced_contract);
267 let has_constructor_args = contract
268 .ctor
269 .is_some_and(|ctor_id| !self.gcx.hir.function(ctor_id).parameters.is_empty());
270 if contract.layout.is_some() && has_constructor_args {
274 trace!("skip dependency on custom-layout contract");
275 return;
276 }
277
278 let source = self.gcx.hir.source(contract.source);
279 let FileName::Real(path) = &source.file.name else {
280 return;
281 };
282
283 if !is_path_in_dir(path, self.src_dir, self.root_dir) {
286 let path = path.display();
287 trace!("ignore dependency {path}");
288 return;
289 }
290
291 self.dependencies.push(dependency);
292 }
293}
294
295fn can_rewrite(
297 path: &Path,
298 source_path: &Path,
299 root_dir: &Path,
300 source_units: &[PathBuf],
301 remappings: &[Remapping],
302 has_constructor_args: bool,
303 contract_id: ContractId,
304) -> bool {
305 let generated_path = path.strip_prefix(root_dir).unwrap_or(path);
306 if !source_units.iter().any(|source_unit| source_unit == generated_path)
307 || source_units.iter().filter(|source_unit| source_unit.ends_with(generated_path)).count()
308 != 1
309 {
310 return false;
311 }
312
313 if remappings.iter().any(|remapping| remapping_matches_path(remapping, generated_path)) {
317 return false;
318 }
319
320 if !has_constructor_args {
321 return true;
322 }
323
324 let helper_path = PathBuf::from(format!("foundry-pp/DeployHelper{}.sol", contract_id.index()));
325 !remappings.iter().any(|remapping| {
326 remapping_applies(remapping, &helper_path, source_path, root_dir)
328 || remapping_applies(remapping, generated_path, &helper_path, root_dir)
329 })
330}
331
332fn is_path_in_dir(path: &Path, dir: &Path, root_dir: &Path) -> bool {
334 let path = normalize_path(&root_dir.join(path));
335 let dir = normalize_path(&root_dir.join(dir));
336 path.starts_with(&dir)
337 || dunce::canonicalize(path)
338 .is_ok_and(|path| dunce::canonicalize(dir).is_ok_and(|dir| path.starts_with(dir)))
339}
340
341fn remapping_applies(
343 remapping: &Remapping,
344 import_path: &Path,
345 source_unit: &Path,
346 root_dir: &Path,
347) -> bool {
348 let source_unit = source_unit.strip_prefix(root_dir).unwrap_or(source_unit).to_slash_lossy();
349 remapping
350 .context
351 .as_ref()
352 .is_none_or(|context| source_unit.starts_with(Path::new(context).to_slash_lossy().as_ref()))
353 && remapping_matches_path(remapping, import_path)
354}
355
356fn remapping_matches_path(remapping: &Remapping, path: &Path) -> bool {
358 path.to_slash_lossy().starts_with(&remapping.name)
359}
360
361impl<'gcx> Visit<'gcx> for BytecodeDependencyCollector<'gcx, '_> {
362 type BreakValue = Never;
363
364 fn hir(&self) -> &'gcx Hir<'gcx> {
365 &self.gcx.hir
366 }
367
368 fn visit_function(&mut self, func: &'gcx Function<'gcx>) -> ControlFlow<Self::BreakValue> {
369 let previous = self.preserve_native_creation_code;
370 self.preserve_native_creation_code = previous
371 || func.state_mutability == StateMutability::Pure
372 || matches!(func.kind, FunctionKind::Modifier);
373 self.walk_function(func)?;
374 self.preserve_native_creation_code = previous;
375 ControlFlow::Continue(())
376 }
377
378 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
379 #[allow(clippy::collapsible_match)]
380 match &expr.kind {
381 ExprKind::Call(call_expr, call_args, named_args) => {
382 if let Some(dependency) = handle_call_expr(
383 self.src,
384 self.gcx.sess.source_map(),
385 expr,
386 call_expr,
387 call_args,
388 named_args,
389 ) {
390 self.collect_dependency(dependency);
391 }
392 }
393 ExprKind::Member(member_expr, ident) => {
394 if let ExprKind::TypeCall(ty) = &member_expr.kind
395 && let TypeKind::Custom(contract_id) = &ty.kind
396 && ident.name.as_str() == "creationCode"
397 && let Some(contract_id) = contract_id.as_contract()
398 {
399 self.collect_dependency(BytecodeDependency {
400 kind: BytecodeDependencyKind::CreationCode,
401 loc: span_to_range(self.gcx.sess.source_map(), expr.span),
402 referenced_contract: contract_id,
403 });
404 }
405 }
406 _ => {}
407 }
408 self.walk_expr(expr)
409 }
410
411 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
412 if let StmtKind::Try(stmt_try) = stmt.kind
413 && let ExprKind::Call(call_expr, call_args, named_args) = &stmt_try.expr.kind
414 && let Some(mut dependency) = handle_call_expr(
415 self.src,
416 self.gcx.sess.source_map(),
417 &stmt_try.expr,
418 call_expr,
419 call_args,
420 named_args,
421 )
422 {
423 let has_custom_return = if let Some(clause) = stmt_try.clauses.first()
424 && clause.args.len() == 1
425 && let Some(ret_var) = clause.args.first()
426 && let TypeKind::Custom(_) = self.hir().variable(*ret_var).ty.kind
427 {
428 true
429 } else {
430 false
431 };
432
433 if let BytecodeDependencyKind::New { try_stmt, .. } = &mut dependency.kind {
434 *try_stmt = Some(has_custom_return);
435 }
436 self.collect_dependency(dependency);
437
438 for clause in stmt_try.clauses {
439 for &var in clause.args {
440 self.visit_nested_var(var)?;
441 }
442 for stmt in clause.block.stmts {
443 self.visit_stmt(stmt)?;
444 }
445 }
446 return ControlFlow::Continue(());
447 }
448 self.walk_stmt(stmt)
449 }
450}
451
452fn handle_call_expr(
454 src: &str,
455 source_map: &SourceMap,
456 parent_expr: &Expr<'_>,
457 call_expr: &Expr<'_>,
458 call_args: &CallArgs<'_>,
459 call_options: &Option<&CallOptions<'_>>,
460) -> Option<BytecodeDependency> {
461 if let ExprKind::New(ty_new) = &call_expr.kind
462 && let TypeKind::Custom(item_id) = ty_new.kind
463 && let Some(contract_id) = item_id.as_contract()
464 {
465 let name_loc = span_to_range(source_map, ty_new.span);
466 let name = &src[name_loc];
467
468 let call_args_offset = if call_args.is_empty() {
473 0
474 } else {
475 (call_args.span.lo() - ty_new.span.hi()).to_usize()
476 };
477
478 let args_len = parent_expr.span.hi() - ty_new.span.hi();
479 return Some(BytecodeDependency {
480 kind: BytecodeDependencyKind::New {
481 name: name.to_string(),
482 args_length: args_len.to_usize(),
483 call_args_offset,
484 value: named_arg(src, call_options, "value", source_map),
485 salt: named_arg(src, call_options, "salt", source_map),
486 try_stmt: None,
487 },
488 loc: span_to_range(source_map, parent_expr.span.with_hi(call_expr.span.hi())),
490 referenced_contract: contract_id,
491 });
492 }
493 None
494}
495
496fn named_arg(
498 src: &str,
499 call_options: &Option<&CallOptions<'_>>,
500 arg: &str,
501 source_map: &SourceMap,
502) -> Option<String> {
503 call_options
504 .map(|options| options.args)
505 .unwrap_or_default()
506 .iter()
507 .find(|named_arg| named_arg.name.as_str() == arg)
508 .map(|named_arg| {
509 let named_arg_loc = span_to_range(source_map, named_arg.value.span);
510 src[named_arg_loc].to_string()
511 })
512}
513
514pub(crate) fn remove_bytecode_dependencies(
528 gcx: Gcx<'_>,
529 deps: &PreprocessorDependencies,
530 data: &PreprocessorData,
531) -> Updates {
532 let mut updates = Updates::default();
533 for (contract_id, deps) in &deps.preprocessed_contracts {
534 let contract = gcx.hir.contract(*contract_id);
535 let source = gcx.hir.source(contract.source);
536 let FileName::Real(path) = &source.file.name else {
537 continue;
538 };
539
540 let updates = updates.entry(path.clone()).or_default();
541 let mut used_helpers = BTreeSet::new();
542
543 let vm_interface_name = format!("VmContractHelper{}", contract_id.index());
544 let vm = format!("{vm_interface_name}(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)");
546 let mut try_catch_helpers: HashSet<&str> = HashSet::default();
547
548 for dep in deps {
549 let Some(ContractData { artifact, constructor_data, .. }) =
550 data.get(&dep.referenced_contract)
551 else {
552 continue;
553 };
554
555 match &dep.kind {
556 BytecodeDependencyKind::CreationCode => {
557 updates.insert((
559 dep.loc.start,
560 dep.loc.end,
561 format!("{vm}.getCode(\"{artifact}\")"),
562 ));
563 }
564 BytecodeDependencyKind::New {
565 name,
566 args_length,
567 call_args_offset,
568 value,
569 salt,
570 try_stmt,
571 } => {
572 let (mut update, closing_seq) = if let Some(has_ret) = try_stmt {
573 if *has_ret {
574 try_catch_helpers.insert(name);
576 (format!("this.addressTo{name}{id}(", id = contract_id.index()), "}))")
577 } else {
578 (String::new(), "})")
579 }
580 } else {
581 (format!("{name}(payable("), "})))")
582 };
583 update.push_str(&format!("{vm}.deployCode({{"));
584 update.push_str(&format!("_artifact: \"{artifact}\""));
585
586 if let Some(value) = value {
587 update.push_str(", ");
588 update.push_str(&format!("_value: {value}"));
589 }
590
591 if let Some(salt) = salt {
592 update.push_str(", ");
593 update.push_str(&format!("_salt: {salt}"));
594 }
595
596 if constructor_data.is_some() {
597 used_helpers.insert(dep.referenced_contract);
599
600 update.push_str(", ");
601 update.push_str(&format!(
602 "_args: encodeArgs{id}(DeployHelper{id}.FoundryPpConstructorArgs",
603 id = dep.referenced_contract.index()
604 ));
605 updates.insert((dep.loc.start, dep.loc.end + call_args_offset, update));
606
607 updates.insert((
608 dep.loc.end + args_length,
609 dep.loc.end + args_length,
610 format!("){closing_seq}"),
611 ));
612 } else {
613 update.push_str(closing_seq);
614 updates.insert((dep.loc.start, dep.loc.end + args_length, update));
615 }
616 }
617 };
618 }
619
620 if !try_catch_helpers.is_empty()
622 && let Some(last_fn_id) = contract.functions().last()
623 {
624 let last_fn_range =
625 span_to_range(gcx.sess.source_map(), gcx.hir.function(last_fn_id).span);
626 let to_address_fns = try_catch_helpers
627 .iter()
628 .map(|ty| {
629 format!(
630 r#"
631 function addressTo{ty}{id}(address addr) public pure returns ({ty}) {{
632 return {ty}(addr);
633 }}
634 "#,
635 id = contract_id.index()
636 )
637 })
638 .collect::<String>();
639
640 updates.insert((last_fn_range.end, last_fn_range.end, to_address_fns));
641 }
642
643 let helper_imports = used_helpers.into_iter().map(|id| {
644 let id = id.index();
645 format!(
646 "import {{DeployHelper{id}, encodeArgs{id}}} from \"foundry-pp/DeployHelper{id}.sol\";",
647 )
648 }).join("\n");
649 updates.insert((
650 source.file.src.len(),
651 source.file.src.len(),
652 format!(
653 r#"
654{helper_imports}
655
656interface {vm_interface_name} {{
657 function deployCode(string memory _artifact) external returns (address);
658 function deployCode(string memory _artifact, bytes32 _salt) external returns (address);
659 function deployCode(string memory _artifact, bytes memory _args) external returns (address);
660 function deployCode(string memory _artifact, bytes memory _args, bytes32 _salt) external returns (address);
661 function deployCode(string memory _artifact, uint256 _value) external returns (address);
662 function deployCode(string memory _artifact, uint256 _value, bytes32 _salt) external returns (address);
663 function deployCode(string memory _artifact, bytes memory _args, uint256 _value) external returns (address);
664 function deployCode(string memory _artifact, bytes memory _args, uint256 _value, bytes32 _salt) external returns (address);
665 function getCode(string memory _artifact) external view returns (bytes memory);
666}}"#
667 ),
668 ));
669 }
670 updates
671}