Skip to main content

foundry_common/preprocessor/
mod.rs

1use crate::errors::convert_solar_errors;
2use foundry_compilers::{
3    Compiler, ProjectPathsConfig, SourceParser, apply_updates,
4    artifacts::SolcLanguage,
5    cache::CompilerCache,
6    error::Result,
7    multi::{MultiCompiler, MultiCompilerInput, MultiCompilerLanguage, MultiCompilerSettings},
8    project::Preprocessor,
9    solc::{SolcCompiler, SolcSettings, SolcVersionedInput},
10};
11use solar::parse::{ast::Span, interface::SourceMap};
12use std::{
13    collections::HashSet,
14    ops::{ControlFlow, Range},
15    path::PathBuf,
16};
17
18mod data;
19use data::{collect_preprocessor_data, create_deploy_helpers};
20
21mod deps;
22use deps::{PreprocessorDependencies, remove_bytecode_dependencies};
23
24/// Preprocessor that replaces static bytecode linking in tests and scripts (`new Contract`) with
25/// dynamic linkage through (`Vm.create*`).
26///
27/// This allows for more efficient caching when iterating on tests.
28///
29/// See <https://github.com/foundry-rs/foundry/pull/10010>.
30#[derive(Debug)]
31pub struct DynamicTestLinkingPreprocessor;
32
33impl Preprocessor<SolcCompiler> for DynamicTestLinkingPreprocessor {
34    #[instrument(name = "DynamicTestLinkingPreprocessor::preprocess", skip_all)]
35    fn preprocess(
36        &self,
37        _solc: &SolcCompiler,
38        input: &mut SolcVersionedInput,
39        paths: &ProjectPathsConfig<SolcLanguage>,
40        mocks: &mut HashSet<PathBuf>,
41    ) -> Result<()> {
42        // Skip if we are not preprocessing any tests or scripts. Avoids unnecessary AST parsing.
43        if !input.input.sources.iter().any(|(path, _)| paths.is_test_or_script(path)) {
44            trace!("no tests or scripts to preprocess");
45            return Ok(());
46        }
47
48        let mut compiler =
49            foundry_compilers::resolver::parse::SolParser::new(paths.with_language_ref())
50                .into_compiler();
51        let _ = compiler.enter_mut(|compiler| -> solar::interface::Result {
52            let mut pcx = compiler.parse();
53
54            // Add the sources into the context.
55            // Include all sources in the source map so as to not re-load them from disk, but only
56            // parse and preprocess tests and scripts.
57            let mut preprocessed_paths = vec![];
58            let mut script_paths = HashSet::new();
59            let sources = &mut input.input.sources;
60            for (path, source) in sources.iter() {
61                if let Ok(src_file) = compiler
62                    .sess()
63                    .source_map()
64                    .new_source_file(path.clone(), source.content.as_str())
65                    && paths.is_test_or_script(path)
66                {
67                    pcx.add_file(src_file);
68                    if paths.is_script(path) {
69                        script_paths.insert(path.clone());
70                    }
71                    preprocessed_paths.push(path.clone());
72                }
73            }
74
75            // Parse and preprocess.
76            pcx.parse();
77            let ControlFlow::Continue(()) = compiler.lower_asts()? else { return Ok(()) };
78            let gcx = compiler.gcx();
79            let mut source_units = sources.keys().cloned().collect::<Vec<_>>();
80            // Cache data is optional, including on the first compilation. Avoid the cache
81            // reader diagnostics when probing for either supported settings format.
82            let cache_files = crate::fs::read_to_string(&paths.cache).ok().and_then(|cache| {
83                serde_json::from_str::<CompilerCache<MultiCompilerSettings>>(&cache)
84                    .map(|cache| cache.files)
85                    .or_else(|_| {
86                        serde_json::from_str::<CompilerCache<SolcSettings>>(&cache)
87                            .map(|cache| cache.files)
88                    })
89                    .ok()
90            });
91            if let Some(files) = cache_files {
92                source_units.extend(
93                    files
94                        .into_keys()
95                        .map(|path| path.strip_prefix(&paths.root).unwrap_or(&path).to_path_buf()),
96                );
97            }
98            source_units.sort_unstable();
99            source_units.dedup();
100            // Collect tests and scripts dependencies and identify mock contracts.
101            // Script paths are passed separately so salted new-expressions are left untouched
102            // (Foundry's broadcast redirects native CREATE2 through the deterministic factory,
103            // but vm.deployCode runs at a deeper depth and bypasses that redirect).
104            let deps = PreprocessorDependencies::new(
105                gcx,
106                &preprocessed_paths,
107                &script_paths,
108                paths,
109                &source_units,
110                mocks,
111            );
112            // Collect data of source contracts referenced in tests and scripts.
113            let data = collect_preprocessor_data(gcx, &deps.referenced_contracts, &paths.root);
114
115            // Extend existing sources with preprocessor deploy helper sources.
116            sources.extend(create_deploy_helpers(&data));
117
118            // Generate and apply preprocessor source updates.
119            apply_updates(sources, remove_bytecode_dependencies(gcx, &deps, &data));
120
121            Ok(())
122        });
123
124        // Warn if any diagnostics emitted during content parsing.
125        if let Err(err) = convert_solar_errors(compiler.dcx()) {
126            warn!(%err, "failed preprocessing");
127        }
128
129        Ok(())
130    }
131}
132
133impl Preprocessor<MultiCompiler> for DynamicTestLinkingPreprocessor {
134    fn preprocess(
135        &self,
136        compiler: &MultiCompiler,
137        input: &mut <MultiCompiler as Compiler>::Input,
138        paths: &ProjectPathsConfig<MultiCompilerLanguage>,
139        mocks: &mut HashSet<PathBuf>,
140    ) -> Result<()> {
141        // Preprocess only Solc compilers.
142        let MultiCompilerInput::Solc(input) = input else { return Ok(()) };
143
144        let Some(solc) = &compiler.solc else { return Ok(()) };
145
146        let paths = paths.clone().with_language::<SolcLanguage>();
147        self.preprocess(solc, input, &paths, mocks)
148    }
149}
150
151/// Returns the range of the given span in the source map.
152#[track_caller]
153fn span_to_range(source_map: &SourceMap, span: Span) -> Range<usize> {
154    source_map.span_to_range(span).unwrap()
155}