Skip to main content

foundry_common/preprocessor/
deps.rs

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
26/// Holds data about referenced source contracts and bytecode dependencies.
27pub(crate) struct PreprocessorDependencies {
28    // Mapping contract id to preprocess -> contract bytecode dependencies.
29    pub preprocessed_contracts: BTreeMap<ContractId, Vec<BytecodeDependency>>,
30    // Referenced contract ids.
31    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        // Helper closure for iterating candidate contracts to preprocess (tests and scripts).
53        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        // Collect current mocks.
71        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        // Collect dependencies for non-mock test/script contracts.
87        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            // Treat the contract as a script when its file lives under the configured script
97            // directory, or when it inherits from a `Script` base (forge-std). The inheritance
98            // check covers atypical layouts where script contracts are placed under `src/`.
99            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            // Analyze current contract.
113            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            // Ignore empty test contracts declared in source files with other contracts.
146            if !deps_collector.dependencies.is_empty() {
147                preprocessed_contracts.insert(contract_id, deps_collector.dependencies);
148            }
149        }
150
151        // Replace classifications only for files examined in this compiler job. This clears stale
152        // mocks after a file is refactored while preserving fallback state across narrower jobs.
153        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/// Represents a bytecode dependency kind.
167#[derive(Debug)]
168enum BytecodeDependencyKind {
169    /// `type(Contract).creationCode`
170    CreationCode,
171    /// `new Contract`.
172    New {
173        /// Contract name.
174        name: String,
175        /// Constructor args length.
176        args_length: usize,
177        /// Constructor call args offset.
178        call_args_offset: usize,
179        /// `msg.value` (if any) used when creating contract.
180        value: Option<String>,
181        /// `salt` (if any) used when creating contract.
182        salt: Option<String>,
183        /// Whether it's a try contract creation statement, with custom return.
184        try_stmt: Option<bool>,
185    },
186}
187
188/// Represents a single bytecode dependency.
189#[derive(Debug)]
190pub(crate) struct BytecodeDependency {
191    /// Dependency kind.
192    kind: BytecodeDependencyKind,
193    /// Source map location of this dependency.
194    loc: Range<usize>,
195    /// HIR id of referenced contract.
196    referenced_contract: ContractId,
197}
198
199/// Walks over contract HIR and collects [`BytecodeDependency`]s and referenced contracts.
200struct BytecodeDependencyCollector<'gcx, 'src> {
201    /// Source map, used for determining contract item locations.
202    gcx: Gcx<'gcx>,
203    /// Source content of current contract.
204    src: &'src str,
205    /// Project source dir, used to determine if referenced contract is a source contract.
206    src_dir: &'src Path,
207    /// Project root, used to compare relative and absolute source paths.
208    root_dir: &'src Path,
209    /// Whether the contract being analyzed lives in a script file.
210    /// Script bytecode references must not be rewritten: native script CREATE/CREATE2 frames
211    /// are handled by the script execution inspector, and `type(Contract).creationCode` must keep
212    /// its native mutability semantics.
213    is_script: bool,
214    /// Whether `type(Contract).creationCode` should keep native Solidity semantics.
215    preserve_native_creation_code: bool,
216    /// Dependencies collected for current contract.
217    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    /// Collects reference identified as bytecode dependency of analyzed contract.
240    /// Discards any reference that is not in project src directory (e.g. external
241    /// libraries or mock contracts that extend source contracts).
242    fn collect_dependency(&mut self, dependency: BytecodeDependency) {
243        // Script bytecode references must not be rewritten. See field doc on `is_script`.
244        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        // `type(Contract).creationCode` has native `pure` semantics. Rewriting it to a `view`
258        // cheatcode call would make valid pure functions fail to compile.
259        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        // Solidity only permits a custom layout on the most-derived contract, so the generated
271        // constructor helper cannot inherit a target that declares one; keep this dependency
272        // native.
273        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        // Remapped imports can have absolute or symlinked paths, while compiler input paths are
284        // relative and configured source directories can be canonicalized.
285        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
295/// Returns whether generated helper and artifact references preserve the source-unit identity.
296fn 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    // Runtime artifact lookup uses the running test's context, which can differ from the source
314    // containing an inherited helper. Any remapping matching the generated path is therefore
315    // unsafe unless every possible runtime context is known.
316    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        // The test imports the generated helper, which in turn imports the dependency.
327        remapping_applies(remapping, &helper_path, source_path, root_dir)
328            || remapping_applies(remapping, generated_path, &helper_path, root_dir)
329    })
330}
331
332/// Returns whether `path` resolves within `dir`, accepting relative, absolute, and symlinked paths.
333fn 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
341/// Returns whether a generated import would be redirected by `remapping`.
342fn 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
356/// Returns whether `path` has the string prefix selected by `remapping`.
357fn 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
452/// Helper function to analyze and extract bytecode dependency from a given call expression.
453fn 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        // Calculate the offset to remove call options and parentheses between the new type and
469        // constructor arguments. For example, in `new Counter {value: 333} (address(this))`, the
470        // offset is used to replace `{value: 333} (` with `(`. This also removes closing
471        // parentheses around the callee when no call options are present.
472        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            // The HIR callee excludes parentheses, so start at the full call expression.
489            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
496/// Helper function to extract value of a given named arg.
497fn 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
514/// Goes over all test/script files and replaces bytecode dependencies with cheatcode
515/// invocations.
516///
517/// Special handling of try/catch statements with custom returns, where the try statement becomes
518/// ```solidity
519/// try this.addressToCounter() returns (Counter c)
520/// ```
521/// and helper to cast address is appended
522/// ```solidity
523/// function addressToCounter(address addr) returns (Counter) {
524///     return Counter(addr);
525/// }
526/// ```
527pub(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        // `address(uint160(uint256(keccak256("hevm cheat code"))))`
545        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                    // for creation code we need to just call getCode
558                    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 this.addressToCounter1() returns (Counter c)
575                            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                        // Insert our helper.
598                        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        // Add try catch statements after last function of the test contract.
621        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}