Skip to main content

foundry_common/preprocessor/
deps.rs

1use super::{
2    data::{ContractData, PreprocessorData, deploy_helper_path},
3    span_to_range,
4};
5use crate::fs::{canonicalize_path, normalize_path};
6use foundry_compilers::{
7    ProjectPathsConfig, Updates,
8    artifacts::{SolcLanguage, remappings::Remapping},
9    project::{NativeDependencies, NativeDependencyState, PreprocessorState},
10};
11use itertools::Itertools;
12use path_slash::PathExt;
13use solar::{
14    ast::{ItemKind, UserDefinableOperator},
15    sema::{
16        Gcx, Hir,
17        builtins::Builtin,
18        hir::{
19            CallArgs, CallArgsKind, CallOptions, Contract, ContractId, ContractKind, Expr,
20            ExprKind, Function, FunctionId, FunctionKind, Modifier, Res, SourceId, StateMutability,
21            Stmt, StmtKind, TypeKind, UsingDirective, UsingEntryKind, Variable, VariableId,
22            Visibility, Visit,
23        },
24        interface::{SourceMap, Symbol, data_structures::Never, source_map::FileName},
25    },
26};
27use std::{
28    collections::{BTreeMap, BTreeSet, HashSet},
29    ops::{ControlFlow, Range},
30    path::{Path, PathBuf},
31};
32
33/// Compiler and source context whose validation would be lost by rewriting construction.
34#[derive(Clone, Copy)]
35pub(super) struct ConstructorContext {
36    pub abi_coder_v2: bool,
37    pub supports_create2: bool,
38}
39
40impl ConstructorContext {
41    fn for_source(mut self, gcx: Gcx<'_>, source: SourceId) -> Self {
42        let ast = gcx
43            .sources
44            .get_file(&gcx.hir.source(source).file)
45            .and_then(|(_, source)| source.ast.as_ref());
46        let Some(ast) = ast else {
47            self.abi_coder_v2 = false;
48            return self;
49        };
50        for item in ast.items.iter() {
51            if let ItemKind::Pragma(pragma) = &item.kind
52                && let Some((name, Some(value))) = pragma.tokens.as_name_and_value()
53            {
54                match (name.as_str(), value.as_str()) {
55                    ("abicoder", "v1") => {
56                        self.abi_coder_v2 = false;
57                        return self;
58                    }
59                    ("abicoder", "v2") | ("experimental", "ABIEncoderV2") => {
60                        self.abi_coder_v2 = true;
61                    }
62                    _ => {}
63                }
64            }
65        }
66        self
67    }
68}
69
70/// Holds data about referenced source contracts and bytecode dependencies.
71pub(crate) struct PreprocessorDependencies {
72    // Mapping contract id to preprocess -> contract bytecode dependencies.
73    pub preprocessed_contracts: BTreeMap<ContractId, Vec<BytecodeDependency>>,
74    // Referenced contract ids.
75    pub referenced_contracts: HashSet<ContractId>,
76}
77
78impl PreprocessorDependencies {
79    #[allow(clippy::too_many_arguments)]
80    pub fn new(
81        gcx: Gcx<'_>,
82        constructor_context: ConstructorContext,
83        paths: &[PathBuf],
84        script_paths: &HashSet<PathBuf>,
85        project_paths: &ProjectPathsConfig<SolcLanguage>,
86        source_units: &[PathBuf],
87        mocks: &mut HashSet<PathBuf>,
88        preprocessor_state: &mut PreprocessorState,
89    ) -> Self {
90        let relative_paths = project_paths.paths_relative();
91        let src_dir = &relative_paths.sources;
92        let root_dir = &project_paths.root;
93        let remappings = &project_paths.remappings;
94        let mut preprocessed_contracts = BTreeMap::new();
95        let mut referenced_contracts = HashSet::new();
96        let mut current_mocks = HashSet::new();
97        let mut current_native_dependencies = NativeDependencies::new();
98        let candidate_files =
99            paths.iter().map(|path| normalize_path(&root_dir.join(path))).collect::<HashSet<_>>();
100        let mut conservative_files = HashSet::new();
101        let global_using_dependencies = using_dependency_sources(
102            gcx,
103            gcx.hir
104                .source_ids()
105                .flat_map(|id| gcx.hir.source(id).usings)
106                .filter(|directive| directive.global),
107        );
108
109        // Helper closure for iterating candidate contracts to preprocess (tests and scripts).
110        let candidate_contracts = || {
111            gcx.hir.contract_ids().filter_map(|id| {
112                let contract = gcx.hir.contract(id);
113                let source = gcx.hir.source(contract.source);
114                let FileName::Real(path) = &source.file.name else {
115                    return None;
116                };
117
118                if !paths.contains(path) {
119                    trace!("{} is not test or script", path.display());
120                    return None;
121                }
122
123                Some((id, contract, source, path))
124            })
125        };
126
127        // An internal call can observe return data left by another function. Analyze the whole
128        // owning contract and its reachable helpers before deciding which scopes can be rewritten.
129        let mut native_return_data_contracts = HashSet::new();
130        for (id, _, _, _) in candidate_contracts() {
131            let mut observer = ReturnDataObserver::new(gcx);
132            let _ = observer.visit_nested_contract(id);
133            if observer.observes_return_data {
134                native_return_data_contracts.insert(id);
135                native_return_data_contracts.extend(observer.contracts);
136            }
137        }
138
139        // Collect current mocks.
140        for (_, contract, _, path) in candidate_contracts() {
141            let full_path = normalize_path(&root_dir.join(path));
142            let mut inherited_dependencies = BTreeSet::new();
143            let mut inherits_source_contract = false;
144            for base_id in contract.linearized_bases {
145                let base = gcx.hir.contract(*base_id);
146                if let FileName::Real(base_path) = &gcx.hir.source(base.source).file.name {
147                    let base_path = normalize_path(&root_dir.join(base_path));
148                    let is_source_contract = is_path_in_dir(&base_path, src_dir, root_dir);
149                    inherits_source_contract |= is_source_contract;
150                    if base_path != full_path {
151                        inherited_dependencies.insert(base_path);
152                    }
153                }
154            }
155            if inherits_source_contract {
156                trace!("found mock contract {}", full_path.display());
157                current_mocks.insert(full_path.clone());
158            }
159            if !inherited_dependencies.is_empty() {
160                add_native_dependencies(
161                    &mut current_native_dependencies,
162                    full_path,
163                    inherited_dependencies,
164                );
165            }
166        }
167
168        // Collect dependencies for non-mock test/script contracts.
169        for (contract_id, contract, source, path) in candidate_contracts() {
170            let full_path = normalize_path(&root_dir.join(path));
171
172            if current_mocks.contains(&full_path) {
173                trace!("{} is a mock, skipping", path.display());
174                continue;
175            }
176
177            // Treat the contract as a script when its file lives under the configured script
178            // directory, or when it inherits from a `Script` base (forge-std). The inheritance
179            // check covers atypical layouts where script contracts are placed under `src/`.
180            let is_script = script_paths.contains(path)
181                || contract
182                    .linearized_bases
183                    .iter()
184                    .skip(1)
185                    .any(|base_id| gcx.hir.contract(*base_id).name.as_str() == "Script");
186            let mut deps_collector = BytecodeDependencyCollector::new(
187                gcx,
188                contract_id,
189                path,
190                src_dir,
191                root_dir,
192                is_script,
193                constructor_context.for_source(gcx, contract.source),
194            );
195            deps_collector.preserve_native_bytecode =
196                native_return_data_contracts.contains(&contract_id);
197            let mut using_dependencies = global_using_dependencies.clone();
198            using_dependencies.extend(using_dependency_sources(
199                gcx,
200                source.usings.iter().chain(
201                    contract
202                        .linearized_bases
203                        .iter()
204                        .flat_map(|base_id| gcx.hir.contract(*base_id).usings),
205                ),
206            ));
207            for source_id in using_dependencies {
208                deps_collector.collect_source_dependencies(source_id);
209            }
210            // Analyze current contract.
211            let _ = deps_collector.walk_contract(contract);
212            if deps_collector.has_unresolved_native_dependency {
213                conservative_files.insert(full_path.clone());
214            }
215            if !deps_collector.native_dependencies.is_empty() {
216                add_native_dependencies(
217                    &mut current_native_dependencies,
218                    full_path.clone(),
219                    deps_collector.native_dependencies,
220                );
221            }
222            deps_collector.dependencies.retain(|dependency| {
223                let dependency_id = dependency.referenced_contract;
224                let dependency = gcx.hir.contract(dependency_id);
225                let dependency_source = gcx.hir.source(dependency.source);
226                let FileName::Real(dependency_path) = &dependency_source.file.name else {
227                    conservative_files.insert(full_path.clone());
228                    return false;
229                };
230                let has_constructor_args = dependency
231                    .ctor
232                    .is_some_and(|ctor_id| !gcx.hir.function(ctor_id).parameters.is_empty());
233                if can_rewrite(
234                    dependency_path,
235                    path,
236                    root_dir,
237                    source_units,
238                    remappings,
239                    has_constructor_args,
240                    dependency_id,
241                ) {
242                    true
243                } else {
244                    add_native_dependencies(
245                        &mut current_native_dependencies,
246                        full_path.clone(),
247                        [normalize_path(&root_dir.join(dependency_path))],
248                    );
249                    false
250                }
251            });
252            // Ignore empty test contracts declared in source files with other contracts.
253            if !deps_collector.dependencies.is_empty() {
254                preprocessed_contracts.insert(contract_id, deps_collector.dependencies);
255            }
256        }
257
258        for file in conservative_files {
259            current_native_dependencies.insert(file, NativeDependencyState::Conservative);
260        }
261
262        // Replace classifications only for files examined in this compiler job. This clears stale
263        // mocks after a file is refactored while preserving fallback state across narrower jobs.
264        for file in candidate_files {
265            let state = current_native_dependencies.remove(&file);
266            if preprocessor_state.update(file.clone(), state) {
267                mocks.remove(&file);
268            }
269        }
270        mocks.extend(current_mocks);
271
272        for dependencies in preprocessed_contracts.values() {
273            referenced_contracts.extend(dependencies.iter().map(|dep| dep.referenced_contract));
274        }
275
276        Self { preprocessed_contracts, referenced_contracts }
277    }
278}
279
280/// Adds exact dependency paths unless the source is already classified conservatively.
281fn add_native_dependencies(
282    dependencies: &mut NativeDependencies,
283    file: PathBuf,
284    incoming: impl IntoIterator<Item = PathBuf>,
285) {
286    match dependencies.entry(file) {
287        std::collections::btree_map::Entry::Vacant(entry) => {
288            entry.insert(NativeDependencyState::Known(incoming.into_iter().collect()));
289        }
290        std::collections::btree_map::Entry::Occupied(entry) => {
291            if let NativeDependencyState::Known(dependencies) = entry.into_mut() {
292                dependencies.extend(incoming);
293            }
294        }
295    }
296}
297
298/// Returns sources whose code can be embedded through the given `using for` directives.
299fn using_dependency_sources<'gcx>(
300    gcx: Gcx<'gcx>,
301    directives: impl IntoIterator<Item = &'gcx UsingDirective<'gcx>>,
302) -> HashSet<SourceId> {
303    directives
304        .into_iter()
305        .flat_map(|directive| directive.entries)
306        .flat_map(|entry| match entry.kind {
307            UsingEntryKind::Library(contract_id) => vec![gcx.hir.contract(contract_id).source],
308            UsingEntryKind::Functions(function_ids) => function_ids
309                .iter()
310                .map(|function_id| gcx.hir.function(*function_id).source)
311                .collect(),
312            UsingEntryKind::Err(_) => Vec::new(),
313        })
314        .collect()
315}
316
317/// Represents a bytecode dependency kind.
318#[derive(Debug)]
319enum BytecodeDependencyKind {
320    /// `type(Contract).creationCode`
321    CreationCode,
322    /// `type(Contract).runtimeCode`.
323    RuntimeCode,
324    /// `new Contract`.
325    New {
326        /// Contract name.
327        name: String,
328        /// Constructor args length.
329        args_length: usize,
330        /// Constructor call args offset.
331        call_args_offset: usize,
332        /// `msg.value` (if any) used when creating contract.
333        value: Option<String>,
334        /// `salt` (if any) used when creating contract.
335        salt: Option<String>,
336    },
337}
338
339/// Represents a single bytecode dependency.
340#[derive(Debug)]
341pub(crate) struct BytecodeDependency {
342    /// Dependency kind.
343    kind: BytecodeDependencyKind,
344    /// Source map location of this dependency.
345    loc: Range<usize>,
346    /// HIR id of referenced contract.
347    referenced_contract: ContractId,
348    /// The original expression must reach Solc to preserve its validation.
349    preserve_native: bool,
350}
351
352/// Walks over contract HIR and collects [`BytecodeDependency`]s and referenced contracts.
353struct BytecodeDependencyCollector<'gcx, 'src> {
354    /// Source map, used for determining contract item locations.
355    gcx: Gcx<'gcx>,
356    /// Contract whose lexically owned bytecode references may be rewritten.
357    owner_contract: ContractId,
358    /// Constructor validation context of the source being rewritten.
359    constructor_context: ConstructorContext,
360    /// Source path of the current contract.
361    source_path: PathBuf,
362    /// Project source dir, used to determine if referenced contract is a source contract.
363    src_dir: &'src Path,
364    /// Project root, used to compare relative and absolute source paths.
365    root_dir: &'src Path,
366    /// Whether the contract being analyzed lives in a script file.
367    /// Script bytecode references must not be rewritten: native script CREATE/CREATE2 frames
368    /// are handled by the script execution inspector, and `type(Contract).creationCode` must keep
369    /// its native mutability semantics.
370    is_script: bool,
371    /// Whether `type(Contract).creationCode` should keep native Solidity semantics.
372    preserve_native_creation_code: bool,
373    /// Whether bytecode references are being visited outside the owning contract's rewrite scope.
374    preserve_native_bytecode: bool,
375    /// Dependencies collected for current contract.
376    dependencies: Vec<BytecodeDependency>,
377    /// Dependencies that cannot be rewritten and remain embedded in the importer bytecode.
378    native_dependencies: BTreeSet<PathBuf>,
379    /// Whether a native dependency could not be assigned a stable filesystem identity.
380    has_unresolved_native_dependency: bool,
381    /// Functions followed while finding transitively embedded implementation code.
382    visited_functions: HashSet<FunctionId>,
383    /// Imported sources already classified as native dependencies.
384    visited_sources: HashSet<SourceId>,
385    /// Constants followed while finding embedded initializers, including aliases and cycles.
386    visited_variables: HashSet<VariableId>,
387}
388
389impl<'gcx, 'src> BytecodeDependencyCollector<'gcx, 'src> {
390    fn new(
391        gcx: Gcx<'gcx>,
392        owner_contract: ContractId,
393        source_path: &Path,
394        src_dir: &'src Path,
395        root_dir: &'src Path,
396        is_script: bool,
397        constructor_context: ConstructorContext,
398    ) -> Self {
399        Self {
400            gcx,
401            owner_contract,
402            constructor_context,
403            source_path: normalize_path(&root_dir.join(source_path)),
404            src_dir,
405            root_dir,
406            is_script,
407            preserve_native_creation_code: false,
408            preserve_native_bytecode: false,
409            dependencies: vec![],
410            native_dependencies: BTreeSet::new(),
411            has_unresolved_native_dependency: false,
412            visited_functions: HashSet::new(),
413            visited_sources: HashSet::new(),
414            visited_variables: HashSet::new(),
415        }
416    }
417
418    /// Visits embedded implementation code without allowing edits outside the owning contract.
419    fn collect_function_dependency(&mut self, function_id: FunctionId) {
420        let function = self.gcx.hir.function(function_id);
421        if function.contract == Some(self.owner_contract)
422            || !self.visited_functions.insert(function_id)
423        {
424            return;
425        }
426
427        let source = self.gcx.hir.source(function.source);
428        if let FileName::Real(path) = &source.file.name {
429            let path = normalize_path(&self.root_dir.join(path));
430            if path != self.source_path {
431                self.native_dependencies.insert(path);
432            }
433        } else {
434            self.has_unresolved_native_dependency = true;
435        }
436
437        let previous = self.preserve_native_bytecode;
438        self.preserve_native_bytecode = true;
439        let _ = self.visit_function(function);
440        self.preserve_native_bytecode = previous;
441    }
442
443    /// Visits an expression for dependencies without rewriting within its source range.
444    fn collect_native_expr(&mut self, expr: &'gcx Expr<'gcx>) {
445        let previous = self.preserve_native_bytecode;
446        self.preserve_native_bytecode = true;
447        let _ = self.visit_expr(expr);
448        self.preserve_native_bytecode = previous;
449    }
450
451    /// Records a source containing embedded code and all of its transitive imports.
452    fn collect_source_dependencies(&mut self, source_id: SourceId) {
453        if !self.visited_sources.insert(source_id) {
454            return;
455        }
456        let source = self.gcx.hir.source(source_id);
457        if let FileName::Real(path) = &source.file.name {
458            let path = normalize_path(&self.root_dir.join(path));
459            if path != self.source_path {
460                self.native_dependencies.insert(path);
461            }
462        } else {
463            self.has_unresolved_native_dependency = true;
464        }
465        for &(_, imported_source) in source.imports {
466            self.collect_source_dependencies(imported_source);
467        }
468    }
469
470    /// Classifies a bytecode dependency as rewritable or native.
471    fn collect_dependency(&mut self, dependency: BytecodeDependency) {
472        let contract = self.gcx.hir.contract(dependency.referenced_contract);
473        let source = self.gcx.hir.source(contract.source);
474        let FileName::Real(path) = &source.file.name else {
475            self.has_unresolved_native_dependency = true;
476            return;
477        };
478        let native_path = normalize_path(&self.root_dir.join(path));
479
480        if self.preserve_native_bytecode || dependency.preserve_native {
481            self.native_dependencies.insert(native_path);
482            return;
483        }
484
485        if matches!(&dependency.kind, BytecodeDependencyKind::RuntimeCode) {
486            self.native_dependencies.insert(native_path);
487            return;
488        }
489
490        // Script bytecode references must not be rewritten. See field doc on `is_script`.
491        if self.is_script {
492            match &dependency.kind {
493                BytecodeDependencyKind::CreationCode | BytecodeDependencyKind::RuntimeCode => {
494                    trace!("skip creationCode in script");
495                    self.native_dependencies.insert(native_path);
496                    return;
497                }
498                BytecodeDependencyKind::New { .. } => {
499                    trace!("skip new-expression in script");
500                    self.native_dependencies.insert(native_path);
501                    return;
502                }
503            }
504        }
505
506        // `type(Contract).creationCode` has native `pure` semantics. Rewriting it to a `view`
507        // cheatcode call would make valid pure functions fail to compile.
508        if self.preserve_native_creation_code
509            && matches!(&dependency.kind, BytecodeDependencyKind::CreationCode)
510        {
511            trace!("skip creationCode in native creationCode context");
512            self.native_dependencies.insert(native_path);
513            return;
514        }
515
516        let has_constructor_args = contract
517            .ctor
518            .is_some_and(|ctor_id| !self.gcx.hir.function(ctor_id).parameters.is_empty());
519        // Solidity only permits a custom layout on the most-derived contract, so the generated
520        // constructor helper cannot inherit a target that declares one; keep this dependency
521        // native.
522        if contract.layout.is_some() && has_constructor_args {
523            trace!("skip dependency on custom-layout contract");
524            self.native_dependencies.insert(native_path);
525            return;
526        }
527
528        // Constructor parameter types are copied into a derived helper contract. Private
529        // constants used as array dimensions are not accessible in that scope.
530        if constructor_uses_private_constants(self.gcx, contract) {
531            self.native_dependencies.insert(native_path);
532            return;
533        }
534
535        // Remapped imports can have absolute or symlinked paths, while compiler input paths are
536        // relative and configured source directories can be canonicalized.
537        if !is_path_in_dir(path, self.src_dir, self.root_dir) {
538            let path = path.display();
539            trace!("keep external dependency {path} native");
540            self.native_dependencies.insert(native_path);
541            return;
542        }
543
544        self.dependencies.push(dependency);
545    }
546
547    /// Follows constants whose initializer can embed code from another source.
548    fn collect_variable_dependency(&mut self, id: VariableId) {
549        let variable = self.gcx.hir.variable(id);
550        if !variable.is_constant() || !self.visited_variables.insert(id) {
551            return;
552        }
553        if let FileName::Real(path) = &self.gcx.hir.source(variable.source).file.name {
554            let path = normalize_path(&self.root_dir.join(path));
555            if path != self.source_path {
556                self.native_dependencies.insert(path);
557            }
558        } else {
559            self.has_unresolved_native_dependency = true;
560        }
561        if let Some(initializer) = variable.initializer {
562            self.collect_native_expr(initializer);
563        }
564    }
565}
566
567/// Returns whether constructor parameter types reference private constants.
568fn constructor_uses_private_constants(gcx: Gcx<'_>, contract: &Contract<'_>) -> bool {
569    contract.ctor.is_some_and(|ctor| {
570        gcx.hir.function(ctor).parameters.iter().any(|&param| {
571            gcx.hir
572                .variable(param)
573                .ty
574                .visit(&gcx.hir, &mut |ty| {
575                    if let TypeKind::Array(array) = &ty.kind
576                        && let Some(size) = array.size
577                    {
578                        size.visit(&mut |expr| {
579                            if gcx.resolved_variable(expr).is_some_and(|var| {
580                                gcx.hir.variable(var).visibility == Some(Visibility::Private)
581                            }) {
582                                return ControlFlow::Break(());
583                            }
584                            ControlFlow::Continue(())
585                        })?;
586                    }
587                    ControlFlow::Continue(())
588                })
589                .is_break()
590        })
591    })
592}
593
594/// Returns whether generated helper and artifact references preserve the source-unit identity.
595fn can_rewrite(
596    path: &Path,
597    source_path: &Path,
598    root_dir: &Path,
599    source_units: &[PathBuf],
600    remappings: &[Remapping],
601    has_constructor_args: bool,
602    contract_id: ContractId,
603) -> bool {
604    let generated_path = path.strip_prefix(root_dir).unwrap_or(path);
605    if !source_units.iter().any(|source_unit| source_unit == generated_path)
606        || source_units.iter().filter(|source_unit| source_unit.ends_with(generated_path)).count()
607            != 1
608    {
609        return false;
610    }
611
612    // Runtime artifact lookup uses the running test's context, which can differ from the source
613    // containing an inherited helper. Any remapping matching the generated path is therefore
614    // unsafe unless every possible runtime context is known.
615    if remappings.iter().any(|remapping| remapping_matches_path(remapping, generated_path)) {
616        return false;
617    }
618
619    if !has_constructor_args {
620        return true;
621    }
622
623    let helper_path = deploy_helper_path(contract_id.index(), source_units);
624    !remappings.iter().any(|remapping| {
625        // The test imports the generated helper, which in turn imports the dependency.
626        remapping_applies(remapping, &helper_path, source_path, root_dir)
627            || remapping_applies(remapping, generated_path, &helper_path, root_dir)
628    })
629}
630
631/// Returns whether `path` resolves within `dir`, accepting relative, absolute, and symlinked paths.
632fn is_path_in_dir(path: &Path, dir: &Path, root_dir: &Path) -> bool {
633    let path = normalize_path(&root_dir.join(path));
634    let dir = normalize_path(&root_dir.join(dir));
635    path.starts_with(&dir)
636        || canonicalize_path(path)
637            .is_ok_and(|path| canonicalize_path(dir).is_ok_and(|dir| path.starts_with(dir)))
638}
639
640/// Returns whether a generated import would be redirected by `remapping`.
641fn remapping_applies(
642    remapping: &Remapping,
643    import_path: &Path,
644    source_unit: &Path,
645    root_dir: &Path,
646) -> bool {
647    let source_unit = source_unit.strip_prefix(root_dir).unwrap_or(source_unit).to_slash_lossy();
648    remapping
649        .context
650        .as_ref()
651        .is_none_or(|context| source_unit.starts_with(Path::new(context).to_slash_lossy().as_ref()))
652        && remapping_matches_path(remapping, import_path)
653}
654
655/// Returns whether `path` has the string prefix selected by `remapping`.
656fn remapping_matches_path(remapping: &Remapping, path: &Path) -> bool {
657    path.to_slash_lossy().starts_with(&remapping.name)
658}
659
660impl<'gcx> Visit<'gcx> for BytecodeDependencyCollector<'gcx, '_> {
661    type BreakValue = Never;
662
663    fn hir(&self) -> &'gcx Hir<'gcx> {
664        &self.gcx.hir
665    }
666
667    fn visit_function(&mut self, func: &'gcx Function<'gcx>) -> ControlFlow<Self::BreakValue> {
668        let previous = self.preserve_native_creation_code;
669        self.preserve_native_creation_code = previous
670            || func.state_mutability == StateMutability::Pure
671            || matches!(func.kind, FunctionKind::Modifier);
672        self.walk_function(func)?;
673        self.preserve_native_creation_code = previous;
674        ControlFlow::Continue(())
675    }
676
677    fn visit_var(&mut self, var: &'gcx Variable<'gcx>) -> ControlFlow<Self::BreakValue> {
678        let previous = self.preserve_native_creation_code;
679        self.preserve_native_creation_code |= var.is_constant();
680        self.walk_var(var)?;
681        self.preserve_native_creation_code = previous;
682        ControlFlow::Continue(())
683    }
684
685    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
686        #[allow(clippy::collapsible_match)]
687        match &expr.kind {
688            ExprKind::Ident(resolutions) => {
689                for &resolution in *resolutions {
690                    match resolution {
691                        Res::Namespace(source_id) => {
692                            self.collect_source_dependencies(source_id);
693                        }
694                        Res::Item(item) => {
695                            if let Some(function_id) = item.as_function() {
696                                self.collect_function_dependency(function_id);
697                            } else if let Some(variable_id) = item.as_variable() {
698                                self.collect_variable_dependency(variable_id);
699                            }
700                        }
701                        _ => {}
702                    }
703                }
704            }
705            ExprKind::Call(callee, call_args) => {
706                let (call_expr, named_args) = callee.split_call_options();
707                if let Some(dependency) = handle_call_expr(
708                    self.gcx,
709                    self.constructor_context,
710                    expr,
711                    call_expr,
712                    call_args,
713                    &named_args,
714                ) {
715                    self.collect_dependency(dependency);
716                    // Call options are copied into the replacement expression. Keep their
717                    // bytecode references native so edits cannot overlap the outer replacement.
718                    self.visit_expr(call_expr)?;
719                    if let Some(call_options) = named_args {
720                        for arg in call_options.args {
721                            self.collect_native_expr(&arg.value);
722                        }
723                    }
724                    self.visit_call_args(call_args)?;
725                    return ControlFlow::Continue(());
726                }
727                if let Some(function_id) = self.gcx.resolved_function(call_expr) {
728                    self.collect_function_dependency(function_id);
729                }
730            }
731            ExprKind::Member(member_expr, ident) => {
732                // Solar does not resolve `Library.function` member expressions as functions. The
733                // internal library implementation is embedded in the caller, so classify the
734                // library source and its imports as native dependencies directly.
735                if let ExprKind::Ident(resolutions) = member_expr.kind {
736                    for resolution in resolutions {
737                        if let Res::Item(item) = resolution
738                            && let Some(contract_id) = item.as_contract()
739                        {
740                            let contract = self.gcx.hir.contract(contract_id);
741                            if contract.kind == ContractKind::Library {
742                                self.collect_source_dependencies(contract.source);
743                            }
744                        }
745                    }
746                }
747                if let ExprKind::TypeCall(ty) = &member_expr.kind
748                    && let TypeKind::Custom(contract_id) = &ty.kind
749                    && let Some(contract_id) = contract_id.as_contract()
750                    && let kind = match ident.name.as_str() {
751                        "creationCode" => BytecodeDependencyKind::CreationCode,
752                        "runtimeCode" => BytecodeDependencyKind::RuntimeCode,
753                        _ => return self.walk_expr(expr),
754                    }
755                {
756                    self.collect_dependency(BytecodeDependency {
757                        kind,
758                        loc: span_to_range(self.gcx.sess.source_map(), expr.span),
759                        referenced_contract: contract_id,
760                        preserve_native: false,
761                    });
762                }
763            }
764            _ => {}
765        }
766        self.walk_expr(expr)
767    }
768
769    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
770        if let StmtKind::Try(stmt_try) = stmt.kind
771            && let ExprKind::Call(callee, ..) = &stmt_try.expr.kind
772            && let (call_expr, _) = callee.split_call_options()
773            && matches!(call_expr.kind, ExprKind::New(_))
774        {
775            // Keep try deployments native: a static-context violation halts the current frame,
776            // whereas a deployment cheatcode revert could be caught by an untyped try. Typed
777            // returns also require native creation to preserve the constructor catch boundary.
778            self.collect_native_expr(&stmt_try.expr);
779
780            for clause in stmt_try.clauses {
781                for &var in clause.args {
782                    self.visit_nested_var(var)?;
783                }
784                for stmt in clause.block.stmts {
785                    self.visit_stmt(stmt)?;
786                }
787            }
788            return ControlFlow::Continue(());
789        }
790        self.walk_stmt(stmt)
791    }
792}
793
794/// Helper function to analyze and extract bytecode dependency from a given call expression.
795fn handle_call_expr(
796    gcx: Gcx<'_>,
797    context: ConstructorContext,
798    parent_expr: &Expr<'_>,
799    call_expr: &Expr<'_>,
800    call_args: &CallArgs<'_>,
801    call_options: &Option<&CallOptions<'_>>,
802) -> Option<BytecodeDependency> {
803    if let ExprKind::New(ty_new) = &call_expr.kind
804        && let TypeKind::Custom(item_id) = ty_new.kind
805        && let Some(contract_id) = item_id.as_contract()
806    {
807        let source_map = gcx.sess.source_map();
808        let name = source_map.span_to_snippet(ty_new.span).ok()?;
809
810        // Calculate the offset to remove call options and parentheses between the new type and
811        // constructor arguments. For example, in `new Counter {value: 333} (address(this))`, the
812        // offset is used to replace `{value: 333} (` with `(`. This also removes closing
813        // parentheses around the callee when no call options are present.
814        let call_args_offset = if call_args.is_empty() {
815            0
816        } else {
817            (call_args.span.lo() - ty_new.span.hi()).to_usize()
818        };
819
820        let args_len = parent_expr.span.hi() - ty_new.span.hi();
821        return Some(BytecodeDependency {
822            kind: BytecodeDependencyKind::New {
823                name,
824                args_length: args_len.to_usize(),
825                call_args_offset,
826                value: named_arg(call_options, "value", source_map),
827                salt: named_arg(call_options, "salt", source_map),
828            },
829            // The HIR callee excludes parentheses, so start at the full call expression.
830            loc: span_to_range(source_map, parent_expr.span.with_hi(call_expr.span.hi())),
831            referenced_contract: contract_id,
832            preserve_native: !valid_constructor_call(
833                gcx,
834                context,
835                contract_id,
836                call_args,
837                call_options,
838            ),
839        });
840    }
841    None
842}
843
844/// Helper function to extract value of a given named arg.
845fn named_arg(
846    call_options: &Option<&CallOptions<'_>>,
847    arg: &str,
848    source_map: &SourceMap,
849) -> Option<String> {
850    call_options
851        .map(|options| options.args)
852        .unwrap_or_default()
853        .iter()
854        .find(|named_arg| named_arg.name.as_str() == arg)
855        .and_then(|named_arg| source_map.span_to_snippet(named_arg.value.span).ok())
856}
857
858/// Goes over all test/script files and replaces bytecode dependencies with cheatcode
859/// invocations.
860///
861/// Try deployments remain native to preserve their constructor failure boundaries.
862pub(crate) fn remove_bytecode_dependencies(
863    gcx: Gcx<'_>,
864    deps: &PreprocessorDependencies,
865    data: &PreprocessorData,
866) -> Updates {
867    let mut updates = Updates::default();
868    let reserved_identifiers = gcx
869        .hir
870        .source_ids()
871        .map(|source_id| gcx.hir.source(source_id).file.src.as_str())
872        .collect::<Vec<_>>()
873        .join("\n");
874    for (contract_id, deps) in &deps.preprocessed_contracts {
875        let contract = gcx.hir.contract(*contract_id);
876        let source = gcx.hir.source(contract.source);
877        let FileName::Real(path) = &source.file.name else {
878            continue;
879        };
880
881        let updates = updates.entry(path.clone()).or_default();
882        let mut used_helpers = BTreeSet::new();
883
884        let vm_interface_name = unique_identifier(
885            &reserved_identifiers,
886            format!("VmContractHelper{}", contract_id.index()),
887        );
888        // `address(uint160(uint256(keccak256("hevm cheat code"))))`
889        let vm = format!("{vm_interface_name}(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)");
890        for dep in deps {
891            let Some(ContractData { artifact, constructor_data, .. }) =
892                data.get(&dep.referenced_contract)
893            else {
894                continue;
895            };
896
897            match &dep.kind {
898                BytecodeDependencyKind::CreationCode => {
899                    // for creation code we need to just call getCode
900                    updates.insert((
901                        dep.loc.start,
902                        dep.loc.end,
903                        format!("{vm}.getCode(\"{artifact}\")"),
904                    ));
905                }
906                BytecodeDependencyKind::RuntimeCode => {
907                    unreachable!("runtimeCode is never rewritten")
908                }
909                BytecodeDependencyKind::New {
910                    name,
911                    args_length,
912                    call_args_offset,
913                    value,
914                    salt,
915                } => {
916                    let mut update = format!("{name}(payable(");
917                    let closing_seq = "})))";
918                    update.push_str(&format!("{vm}.deployCode({{"));
919                    update.push_str(&format!("_artifact: \"{artifact}\""));
920
921                    if let Some(value) = value {
922                        update.push_str(", ");
923                        update.push_str(&format!("_value: {value}"));
924                    }
925
926                    if let Some(salt) = salt {
927                        update.push_str(", ");
928                        update.push_str(&format!("_salt: {salt}"));
929                    }
930
931                    if let Some(constructor_data) = constructor_data {
932                        // Insert our helper.
933                        used_helpers.insert(dep.referenced_contract);
934                        let helper_contract = unique_identifier(
935                            &reserved_identifiers,
936                            constructor_data.helper_contract.clone(),
937                        );
938                        let encode_function = unique_identifier(
939                            &reserved_identifiers,
940                            constructor_data.encode_function.clone(),
941                        );
942
943                        update.push_str(", ");
944                        update.push_str(&format!(
945                            "_args: {}({}.{}",
946                            encode_function, helper_contract, constructor_data.args_struct,
947                        ));
948                        updates.insert((dep.loc.start, dep.loc.end + call_args_offset, update));
949
950                        updates.insert((
951                            dep.loc.end + args_length,
952                            dep.loc.end + args_length,
953                            format!("){closing_seq}"),
954                        ));
955                    } else {
956                        update.push_str(closing_seq);
957                        updates.insert((dep.loc.start, dep.loc.end + args_length, update));
958                    }
959                }
960            };
961        }
962
963        let helper_imports = used_helpers
964            .into_iter()
965            .map(|id| {
966                let constructor = data[&id].constructor_data.as_ref().unwrap();
967                let helper_contract = &constructor.helper_contract;
968                let encode_function = &constructor.encode_function;
969                let local_helper =
970                    unique_identifier(&reserved_identifiers, helper_contract.clone());
971                let local_encoder =
972                    unique_identifier(&reserved_identifiers, encode_function.clone());
973                let helper_import = import_alias(helper_contract, &local_helper);
974                let encoder_import = import_alias(encode_function, &local_encoder);
975                let helper_path = constructor.helper_path.to_slash_lossy();
976                format!("import {{{helper_import}, {encoder_import}}} from \"{helper_path}\";",)
977            })
978            .join("\n");
979        updates.insert((
980            source.file.src.len(),
981            source.file.src.len(),
982            format!(
983                r#"
984{helper_imports}
985
986interface {vm_interface_name} {{
987    function deployCode(string memory _artifact) external returns (address);
988    function deployCode(string memory _artifact, bytes32 _salt) external returns (address);
989    function deployCode(string memory _artifact, bytes memory _args) external returns (address);
990    function deployCode(string memory _artifact, bytes memory _args, bytes32 _salt) external returns (address);
991    function deployCode(string memory _artifact, uint256 _value) external returns (address);
992    function deployCode(string memory _artifact, uint256 _value, bytes32 _salt) external returns (address);
993    function deployCode(string memory _artifact, bytes memory _args, uint256 _value) external returns (address);
994    function deployCode(string memory _artifact, bytes memory _args, uint256 _value, bytes32 _salt) external returns (address);
995    function getCode(string memory _artifact) external view returns (bytes memory);
996}}"#
997            ),
998        ));
999    }
1000    updates
1001}
1002
1003/// Returns an identifier that cannot collide with text in the original source.
1004fn unique_identifier(source: &str, mut identifier: String) -> String {
1005    while source.contains(&identifier) {
1006        identifier.push('_');
1007    }
1008    identifier
1009}
1010
1011fn import_alias(identifier: &str, local: &str) -> String {
1012    if identifier == local { identifier.to_string() } else { format!("{identifier} as {local}") }
1013}
1014
1015/// Checks constraints that disappear when `new` is replaced by a cheatcode call.
1016/// The generated argument struct retains type checks, but not source ABI-coder restrictions.
1017/// Keep parameterized ABI-coder-v1 calls native rather than duplicating Solc ABI validation.
1018fn valid_constructor_call(
1019    gcx: Gcx<'_>,
1020    context: ConstructorContext,
1021    id: ContractId,
1022    args: &CallArgs<'_>,
1023    options: &Option<&CallOptions<'_>>,
1024) -> bool {
1025    let contract = gcx.hir.contract(id);
1026    if contract.kind != ContractKind::Contract {
1027        return false;
1028    }
1029    let constructor = contract.ctor.map(|id| gcx.hir.function(id));
1030    let parameters = constructor.map_or(&[][..], |ctor| ctor.parameters);
1031    if args.len() != parameters.len() || (!parameters.is_empty() && !context.abi_coder_v2) {
1032        return false;
1033    }
1034    if let CallArgsKind::Named(args) = args.kind {
1035        let mut names = HashSet::new();
1036        if args.iter().any(|arg| {
1037            !names.insert(arg.name.name)
1038                || !parameters.iter().any(|id| {
1039                    gcx.hir.variable(*id).name.is_some_and(|name| name.name == arg.name.name)
1040                })
1041        }) {
1042            return false;
1043        }
1044    }
1045    if let Some(options) = options {
1046        let mut names = HashSet::new();
1047        if options.args.iter().any(|arg| {
1048            !names.insert(arg.name.name)
1049                || match arg.name.as_str() {
1050                    "salt" => !context.supports_create2,
1051                    "value" => constructor
1052                        .is_none_or(|ctor| ctor.state_mutability != StateMutability::Payable),
1053                    _ => true,
1054                }
1055        }) {
1056            return false;
1057        }
1058    }
1059    true
1060}
1061
1062/// Finds return-buffer observations in a contract and the helpers it can call internally.
1063struct ReturnDataObserver<'gcx> {
1064    gcx: Gcx<'gcx>,
1065    observes_return_data: bool,
1066    functions: HashSet<FunctionId>,
1067    contracts: HashSet<ContractId>,
1068    visited_contracts: HashSet<ContractId>,
1069    member_functions: HashSet<(FunctionId, Symbol)>,
1070    sources: HashSet<SourceId>,
1071    operator_functions: HashSet<(FunctionId, UserDefinableOperator)>,
1072}
1073
1074impl<'gcx> ReturnDataObserver<'gcx> {
1075    fn new(gcx: Gcx<'gcx>) -> Self {
1076        Self {
1077            gcx,
1078            observes_return_data: false,
1079            functions: HashSet::new(),
1080            contracts: HashSet::new(),
1081            visited_contracts: HashSet::new(),
1082            member_functions: HashSet::new(),
1083            sources: HashSet::new(),
1084            operator_functions: HashSet::new(),
1085        }
1086    }
1087
1088    fn collect_member_functions(&mut self, source: SourceId, contract: Option<ContractId>) {
1089        let bases = contract
1090            .into_iter()
1091            .flat_map(|id| self.gcx.hir.contract(id).linearized_bases)
1092            .copied()
1093            .collect::<Vec<_>>();
1094        for &id in &bases {
1095            self.member_functions.extend(
1096                self.gcx
1097                    .hir
1098                    .contract(id)
1099                    .functions()
1100                    .filter_map(|id| self.gcx.hir.function(id).name.map(|name| (id, name.name))),
1101            );
1102        }
1103        let directives = self
1104            .gcx
1105            .hir
1106            .source(source)
1107            .usings
1108            .iter()
1109            .chain(bases.iter().flat_map(|&id| self.gcx.hir.contract(id).usings))
1110            .chain(
1111                self.gcx
1112                    .hir
1113                    .source_ids()
1114                    .flat_map(|id| self.gcx.hir.source(id).usings)
1115                    .filter(|directive| directive.global),
1116            );
1117        for directive in directives {
1118            for entry in directive.entries {
1119                match entry.kind {
1120                    UsingEntryKind::Library(id) => self.member_functions.extend(
1121                        self.gcx.hir.contract(id).functions().filter_map(|id| {
1122                            self.gcx.hir.function(id).name.map(|name| (id, name.name))
1123                        }),
1124                    ),
1125                    UsingEntryKind::Functions(ids) => {
1126                        if let Some(operator) = entry.operator {
1127                            self.operator_functions.extend(ids.iter().map(|&id| (id, operator)));
1128                        }
1129                        self.member_functions.extend(ids.iter().copied().filter_map(|id| {
1130                            entry
1131                                .name
1132                                .or_else(|| self.gcx.hir.function(id).name.map(|name| name.name))
1133                                .map(|name| (id, name))
1134                        }))
1135                    }
1136                    UsingEntryKind::Err(_) => {}
1137                }
1138            }
1139        }
1140    }
1141}
1142
1143impl<'gcx> Visit<'gcx> for ReturnDataObserver<'gcx> {
1144    type BreakValue = Never;
1145
1146    fn hir(&self) -> &'gcx Hir<'gcx> {
1147        &self.gcx.hir
1148    }
1149
1150    fn visit_nested_source(&mut self, id: SourceId) -> ControlFlow<Self::BreakValue> {
1151        if self.sources.insert(id) {
1152            let source = self.gcx.hir.source(id);
1153            self.walk_nested_source(id)?;
1154            for &(_, id) in source.imports {
1155                self.visit_nested_source(id)?;
1156            }
1157        }
1158        ControlFlow::Continue(())
1159    }
1160
1161    fn visit_nested_contract(&mut self, id: ContractId) -> ControlFlow<Self::BreakValue> {
1162        if self.visited_contracts.insert(id) {
1163            let contract = self.gcx.hir.contract(id);
1164            self.contracts.insert(id);
1165            // State initializers run before functions are visited, but can already call methods
1166            // supplied by contract-scoped using directives.
1167            self.collect_member_functions(contract.source, Some(id));
1168            // An inherited observer can call a derived override that produces return data. Treat
1169            // the complete inheritance hierarchy as one execution scope before rewriting it.
1170            for &base in contract.linearized_bases {
1171                if base != id {
1172                    self.visit_nested_contract(base)?;
1173                }
1174            }
1175            self.walk_contract(contract)?;
1176        }
1177        ControlFlow::Continue(())
1178    }
1179
1180    fn visit_nested_function(&mut self, id: FunctionId) -> ControlFlow<Self::BreakValue> {
1181        if self.functions.insert(id) {
1182            let function = self.gcx.hir.function(id);
1183            self.collect_member_functions(function.source, function.contract);
1184            if let Some(id) = function.contract {
1185                self.contracts.insert(id);
1186            }
1187            self.walk_function(function)?;
1188        }
1189        ControlFlow::Continue(())
1190    }
1191
1192    fn visit_modifier(&mut self, modifier: &'gcx Modifier<'gcx>) -> ControlFlow<Self::BreakValue> {
1193        if let Some(id) = modifier.id.as_function() {
1194            self.visit_nested_function(id)?;
1195        }
1196        self.walk_modifier(modifier)
1197    }
1198
1199    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
1200        let operator = match &expr.kind {
1201            ExprKind::Unary(op, _) => UserDefinableOperator::from_unop(op.kind),
1202            ExprKind::Binary(_, op, _) => UserDefinableOperator::from_binop(op.kind),
1203            _ => None,
1204        };
1205        if let Some(operator) = operator {
1206            // Type checking has not selected an overload yet, so visit every visible binding.
1207            let functions = self
1208                .operator_functions
1209                .iter()
1210                .filter_map(|&(id, bound)| (bound == operator).then_some(id))
1211                .collect::<Vec<_>>();
1212            for id in functions {
1213                self.visit_nested_function(id)?;
1214            }
1215        }
1216        match &expr.kind {
1217            ExprKind::Ident(resolutions) => {
1218                for resolution in *resolutions {
1219                    match resolution {
1220                        Res::Builtin(Builtin::YulReturndatasize | Builtin::YulReturndatacopy) => {
1221                            self.observes_return_data = true;
1222                        }
1223                        Res::Item(item) => {
1224                            if let Some(id) = item.as_function() {
1225                                self.visit_nested_function(id)?;
1226                            }
1227                        }
1228                        _ => {}
1229                    }
1230                }
1231            }
1232            ExprKind::Call(callee, _) => {
1233                let (callee, _) = callee.split_call_options();
1234                if let Some(id) = self.gcx.resolved_function(callee) {
1235                    self.visit_nested_function(id)?;
1236                }
1237            }
1238            ExprKind::Member(member, name) => {
1239                // Resolve the complete namespace path, including renamed re-exports and library
1240                // members, without visiting unrelated declarations in the imported sources.
1241                let mut names = vec![*name];
1242                let mut root = member.peel_parens();
1243                while let ExprKind::Member(parent, name) = &root.kind {
1244                    names.push(*name);
1245                    root = parent.peel_parens();
1246                }
1247                if let ExprKind::Ident(resolutions) = &root.kind {
1248                    names.reverse();
1249                    for resolution in *resolutions {
1250                        if let Res::Namespace(source) = resolution {
1251                            if let Some(resolutions) =
1252                                self.gcx.source_path_resolutions(&names, *source, None)
1253                                && let Some(targets) = resolutions.last()
1254                            {
1255                                for target in targets {
1256                                    if let Res::Item(item) = target {
1257                                        if let Some(id) = item.as_function() {
1258                                            self.visit_nested_function(id)?;
1259                                        } else if let Some(id) = item.as_variable() {
1260                                            self.visit_nested_var(id)?;
1261                                        }
1262                                    }
1263                                }
1264                            } else {
1265                                // Retain the conservative fallback when name resolution is
1266                                // incomplete.
1267                                self.visit_nested_source(*source)?;
1268                            }
1269                        }
1270                    }
1271                }
1272                // Type checking has not run, so include every visible overload of an inherited
1273                // or using-for method with this name.
1274                let functions = self
1275                    .member_functions
1276                    .iter()
1277                    .copied()
1278                    .filter_map(|(id, attached_name)| (attached_name == name.name).then_some(id))
1279                    .collect::<Vec<_>>();
1280                for id in functions {
1281                    self.visit_nested_function(id)?;
1282                }
1283                if let ExprKind::Ident(resolutions) = member.peel_parens().kind {
1284                    for resolution in resolutions {
1285                        if let Res::Item(item) = resolution
1286                            && let Some(id) = item.as_contract()
1287                            && self.gcx.hir.contract(id).kind == ContractKind::Library
1288                        {
1289                            for id in self.gcx.hir.contract(id).functions() {
1290                                if self
1291                                    .gcx
1292                                    .hir
1293                                    .function(id)
1294                                    .name
1295                                    .is_some_and(|ident| ident.name == name.name)
1296                                {
1297                                    self.visit_nested_function(id)?;
1298                                }
1299                            }
1300                        }
1301                    }
1302                }
1303            }
1304            _ => {}
1305        }
1306        self.walk_expr(expr)
1307    }
1308}