foundry_common/preprocessor/
mod.rs

1use foundry_compilers::{
2    Compiler, Language, ProjectPathsConfig, apply_updates,
3    artifacts::SolcLanguage,
4    error::Result,
5    multi::{MultiCompiler, MultiCompilerInput, MultiCompilerLanguage},
6    project::Preprocessor,
7    solc::{SolcCompiler, SolcVersionedInput},
8};
9use solar_parse::{
10    ast::Span,
11    interface::{Session, SourceMap},
12};
13use solar_sema::{ParsingContext, thread_local::ThreadLocal};
14use std::{collections::HashSet, ops::Range, path::PathBuf};
15
16mod data;
17use data::{collect_preprocessor_data, create_deploy_helpers};
18
19mod deps;
20use deps::{PreprocessorDependencies, remove_bytecode_dependencies};
21
22/// Returns the range of the given span in the source map.
23#[track_caller]
24fn span_to_range(source_map: &SourceMap, span: Span) -> Range<usize> {
25    source_map.span_to_source(span).unwrap().1
26}
27
28#[derive(Debug)]
29pub struct TestOptimizerPreprocessor;
30
31impl Preprocessor<SolcCompiler> for TestOptimizerPreprocessor {
32    fn preprocess(
33        &self,
34        _solc: &SolcCompiler,
35        input: &mut SolcVersionedInput,
36        paths: &ProjectPathsConfig<SolcLanguage>,
37        mocks: &mut HashSet<PathBuf>,
38    ) -> Result<()> {
39        // Skip if we are not preprocessing any tests or scripts. Avoids unnecessary AST parsing.
40        if !input.input.sources.iter().any(|(path, _)| paths.is_test_or_script(path)) {
41            trace!("no tests or sources to preprocess");
42            return Ok(());
43        }
44
45        let sess = solar_session_from_solc(input);
46        let _ = sess.enter_parallel(|| -> solar_parse::interface::Result {
47            // Set up the parsing context with the project paths.
48            let mut parsing_context = solar_pcx_from_solc_no_sources(&sess, input, paths);
49
50            // Add the sources into the context.
51            // Include all sources in the source map so as to not re-load them from disk, but only
52            // parse and preprocess tests and scripts.
53            let mut preprocessed_paths = vec![];
54            let sources = &mut input.input.sources;
55            for (path, source) in sources.iter() {
56                if let Ok(src_file) =
57                    sess.source_map().new_source_file(path.clone(), source.content.as_str())
58                    && paths.is_test_or_script(path)
59                {
60                    parsing_context.add_file(src_file);
61                    preprocessed_paths.push(path.clone());
62                }
63            }
64
65            // Parse and preprocess.
66            let hir_arena = ThreadLocal::new();
67            if let Some(gcx) = parsing_context.parse_and_lower(&hir_arena)? {
68                let hir = &gcx.get().hir;
69                // Collect tests and scripts dependencies and identify mock contracts.
70                let deps = PreprocessorDependencies::new(
71                    &sess,
72                    hir,
73                    &preprocessed_paths,
74                    &paths.paths_relative().sources,
75                    &paths.root,
76                    mocks,
77                );
78                // Collect data of source contracts referenced in tests and scripts.
79                let data = collect_preprocessor_data(&sess, hir, &deps.referenced_contracts);
80
81                // Extend existing sources with preprocessor deploy helper sources.
82                sources.extend(create_deploy_helpers(&data));
83
84                // Generate and apply preprocessor source updates.
85                apply_updates(sources, remove_bytecode_dependencies(hir, &deps, &data));
86            }
87
88            Ok(())
89        });
90
91        // Warn if any diagnostics emitted during content parsing.
92        if let Err(err) = sess.emitted_errors().unwrap() {
93            warn!("failed preprocessing {err}");
94        }
95
96        Ok(())
97    }
98}
99
100impl Preprocessor<MultiCompiler> for TestOptimizerPreprocessor {
101    fn preprocess(
102        &self,
103        compiler: &MultiCompiler,
104        input: &mut <MultiCompiler as Compiler>::Input,
105        paths: &ProjectPathsConfig<MultiCompilerLanguage>,
106        mocks: &mut HashSet<PathBuf>,
107    ) -> Result<()> {
108        // Preprocess only Solc compilers.
109        let MultiCompilerInput::Solc(input) = input else { return Ok(()) };
110
111        let Some(solc) = &compiler.solc else { return Ok(()) };
112
113        let paths = paths.clone().with_language::<SolcLanguage>();
114        self.preprocess(solc, input, &paths, mocks)
115    }
116}
117
118fn solar_session_from_solc(solc: &SolcVersionedInput) -> Session {
119    use solar_parse::interface::config;
120
121    Session::builder()
122        .with_buffer_emitter(Default::default())
123        .opts(config::Opts {
124            language: match solc.input.language {
125                SolcLanguage::Solidity => config::Language::Solidity,
126                SolcLanguage::Yul => config::Language::Yul,
127                _ => unimplemented!(),
128            },
129
130            // TODO: ...
131            /*
132            evm_version: solc.input.settings.evm_version,
133            */
134            ..Default::default()
135        })
136        .build()
137}
138
139fn solar_pcx_from_solc_no_sources<'sess>(
140    sess: &'sess Session,
141    solc: &SolcVersionedInput,
142    paths: &ProjectPathsConfig<impl Language>,
143) -> ParsingContext<'sess> {
144    let mut pcx = ParsingContext::new(sess);
145    pcx.file_resolver.set_current_dir(solc.cli_settings.base_path.as_ref().unwrap_or(&paths.root));
146    for remapping in &paths.remappings {
147        pcx.file_resolver.add_import_remapping(solar_sema::interface::config::ImportRemapping {
148            context: remapping.context.clone().unwrap_or_default(),
149            prefix: remapping.name.clone(),
150            path: remapping.path.clone(),
151        });
152    }
153    pcx.file_resolver.add_include_paths(solc.cli_settings.include_paths.iter().cloned());
154    pcx
155}