Skip to main content

foundry_common/preprocessor/
deps.rs

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
21/// Holds data about referenced source contracts and bytecode dependencies.
22pub(crate) struct PreprocessorDependencies {
23    // Mapping contract id to preprocess -> contract bytecode dependencies.
24    pub preprocessed_contracts: BTreeMap<ContractId, Vec<BytecodeDependency>>,
25    // Referenced contract ids.
26    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        // Helper closure for iterating candidate contracts to preprocess (tests and scripts).
43        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        // Collect current mocks.
61        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        // Collect dependencies for non-mock test/script contracts.
76        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            // Make sure current contract is not in list of mocks (could happen when a contract
85            // which used to be a mock is refactored to a non-mock implementation).
86            mocks.remove(&full_path);
87
88            // Treat the contract as a script when its file lives under the configured script
89            // directory, or when it inherits from a `Script` base (forge-std). The inheritance
90            // check covers atypical layouts where script contracts are placed under `src/`.
91            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            // Analyze current contract.
100            let _ = deps_collector.walk_contract(contract);
101            // Ignore empty test contracts declared in source files with other contracts.
102            if !deps_collector.dependencies.is_empty() {
103                preprocessed_contracts.insert(contract_id, deps_collector.dependencies);
104            }
105
106            // Record collected referenced contract ids.
107            referenced_contracts.extend(deps_collector.referenced_contracts);
108        }
109
110        // Add current mocks.
111        mocks.extend(current_mocks);
112
113        Self { preprocessed_contracts, referenced_contracts }
114    }
115}
116
117/// Represents a bytecode dependency kind.
118#[derive(Debug)]
119enum BytecodeDependencyKind {
120    /// `type(Contract).creationCode`
121    CreationCode,
122    /// `new Contract`.
123    New {
124        /// Contract name.
125        name: String,
126        /// Constructor args length.
127        args_length: usize,
128        /// Constructor call args offset.
129        call_args_offset: usize,
130        /// `msg.value` (if any) used when creating contract.
131        value: Option<String>,
132        /// `salt` (if any) used when creating contract.
133        salt: Option<String>,
134        /// Whether it's a try contract creation statement, with custom return.
135        try_stmt: Option<bool>,
136    },
137}
138
139/// Represents a single bytecode dependency.
140#[derive(Debug)]
141pub(crate) struct BytecodeDependency {
142    /// Dependency kind.
143    kind: BytecodeDependencyKind,
144    /// Source map location of this dependency.
145    loc: Range<usize>,
146    /// HIR id of referenced contract.
147    referenced_contract: ContractId,
148}
149
150/// Walks over contract HIR and collects [`BytecodeDependency`]s and referenced contracts.
151struct BytecodeDependencyCollector<'gcx, 'src> {
152    /// Source map, used for determining contract item locations.
153    gcx: Gcx<'gcx>,
154    /// Source content of current contract.
155    src: &'src str,
156    /// Project source dir, used to determine if referenced contract is a source contract.
157    src_dir: &'src Path,
158    /// Whether the contract being analyzed lives in a script file.
159    /// Script bytecode references must not be rewritten: native script CREATE/CREATE2 frames
160    /// are handled by the script execution inspector, and `type(Contract).creationCode` must keep
161    /// its native mutability semantics.
162    is_script: bool,
163    /// Whether `type(Contract).creationCode` should keep native Solidity semantics.
164    preserve_native_creation_code: bool,
165    /// Dependencies collected for current contract.
166    dependencies: Vec<BytecodeDependency>,
167    /// Unique HIR ids of contracts referenced from current contract.
168    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    /// Collects reference identified as bytecode dependency of analyzed contract.
185    /// Discards any reference that is not in project src directory (e.g. external
186    /// libraries or mock contracts that extend source contracts).
187    fn collect_dependency(&mut self, dependency: BytecodeDependency) {
188        // Script bytecode references must not be rewritten. See field doc on `is_script`.
189        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        // `type(Contract).creationCode` has native `pure` semantics. Rewriting it to a `view`
203        // cheatcode call would make valid pure functions fail to compile.
204        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
319/// Helper function to analyze and extract bytecode dependency from a given call expression.
320fn 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        // Calculate offset to remove named args, e.g. for an expression like
336        // `new Counter {value: 333} (  address(this))`
337        // the offset will be used to replace `{value: 333} (  ` with `(`
338        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
361/// Helper function to extract value of a given named arg.
362fn 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
379/// Goes over all test/script files and replaces bytecode dependencies with cheatcode
380/// invocations.
381///
382/// Special handling of try/catch statements with custom returns, where the try statement becomes
383/// ```solidity
384/// try this.addressToCounter() returns (Counter c)
385/// ```
386/// and helper to cast address is appended
387/// ```solidity
388/// function addressToCounter(address addr) returns (Counter) {
389///     return Counter(addr);
390/// }
391/// ```
392pub(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        // `address(uint160(uint256(keccak256("hevm cheat code"))))`
410        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                    // for creation code we need to just call getCode
423                    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 this.addressToCounter1() returns (Counter c)
440                            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                        // Insert our helper
463                        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        // Add try catch statements after last function of the test contract.
486        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}