Skip to main content

foundry_common/preprocessor/
mod.rs

1use crate::{errors::convert_solar_errors, fs::normalize_path};
2use foundry_compilers::{
3    ProjectPathsConfig, SourceParser, apply_updates,
4    artifacts::{EvmVersion, SolcLanguage},
5    error::Result,
6    multi::{MultiCompiler, MultiCompilerInput, MultiCompilerLanguage},
7    project::{
8        NativeDependencies, NativeDependencyState, Preprocessor, PreprocessorState,
9        merge_native_dependencies,
10    },
11    resolver::{
12        Graph,
13        parse::{SolData, SolParser},
14    },
15    solc::{SolcCompiler, SolcVersionedInput},
16};
17use solar::parse::{ast::Span, interface::SourceMap};
18use std::{
19    collections::{BTreeSet, HashSet},
20    ops::{ControlFlow, Range},
21    path::{Path, PathBuf},
22    sync::{
23        Arc,
24        atomic::{AtomicBool, Ordering},
25    },
26};
27
28mod data;
29use data::{collect_preprocessor_data, create_deploy_helpers};
30
31pub use data::is_deploy_helper_path;
32
33mod deps;
34use deps::{ConstructorContext, PreprocessorDependencies, remove_bytecode_dependencies};
35
36/// Preprocessor that replaces static bytecode linking in tests and scripts (`new Contract`) with
37/// dynamic linkage through (`Vm.create*`).
38///
39/// This allows for more efficient caching when iterating on tests.
40///
41/// See <https://github.com/foundry-rs/foundry/pull/10010>.
42#[derive(Clone, Debug, Default)]
43pub struct DynamicTestLinkingPreprocessor {
44    /// Set when preprocessing changes any compiler input. Shared by clones.
45    rewrote_sources: Arc<AtomicBool>,
46}
47
48impl DynamicTestLinkingPreprocessor {
49    /// Returns whether preprocessing changed any compiler input.
50    pub fn rewrote_sources(&self) -> bool {
51        self.rewrote_sources.load(Ordering::Relaxed)
52    }
53}
54
55impl Preprocessor<SolcCompiler> for DynamicTestLinkingPreprocessor {
56    fn cache_version(&self) -> u64 {
57        13
58    }
59
60    fn preprocess(
61        &self,
62        solc: &SolcCompiler,
63        input: &mut SolcVersionedInput,
64        paths: &ProjectPathsConfig<SolcLanguage>,
65        mocks: &mut HashSet<PathBuf>,
66    ) -> Result<()> {
67        let source_units = input.input.sources.keys().cloned().collect::<Vec<_>>();
68        self.preprocess_with_dependencies(
69            solc,
70            input,
71            paths,
72            mocks,
73            &mut PreprocessorState::untracked(),
74            &source_units,
75        )
76    }
77
78    #[instrument(name = "DynamicTestLinkingPreprocessor::preprocess", skip_all)]
79    fn preprocess_with_dependencies(
80        &self,
81        solc: &SolcCompiler,
82        input: &mut SolcVersionedInput,
83        paths: &ProjectPathsConfig<SolcLanguage>,
84        mocks: &mut HashSet<PathBuf>,
85        preprocessor_state: &mut PreprocessorState,
86        source_units: &[PathBuf],
87    ) -> Result<()> {
88        // Skip if we are not preprocessing any tests or scripts. Avoids unnecessary AST parsing.
89        if !input.input.sources.iter().any(|(path, _)| paths.is_test_or_script(path)) {
90            trace!("no tests or scripts to preprocess");
91            return Ok(());
92        }
93
94        let constructor_context = ConstructorContext {
95            abi_coder_v2: input.version >= semver::Version::new(0, 8, 0),
96            // Without an explicit target, preserve native validation rather than guessing the
97            // selected compiler's default EVM version.
98            supports_create2: input
99                .input
100                .settings
101                .evm_version
102                .is_some_and(|version| version >= EvmVersion::Constantinople),
103        };
104        // Avoid the via-IR address-return stack error with uint160, retaining canonical ABI checks.
105        // Keep this compatibility workaround scoped to the verified compiler releases.
106        // Specific compiler selection normalizes away prerelease metadata in the input version.
107        let use_uint160_return = input.input.settings.via_ir == Some(true)
108            && input.version.pre.is_empty()
109            && !matches!(solc, SolcCompiler::Specific(solc) if !solc.version.pre.is_empty())
110            && matches!(
111                (input.version.major, input.version.minor, input.version.patch),
112                (0, 8, 15..=25)
113            );
114        let original_sources = input.input.sources.clone();
115        let mut parser_paths = paths.clone();
116        parser_paths.include_paths.extend(input.cli_settings.include_paths.iter().cloned());
117        let mut compiler =
118            foundry_compilers::resolver::parse::SolParser::new(parser_paths.with_language_ref())
119                .into_compiler();
120        // Solc validates the input version; Solar only analyzes deployment dependencies.
121        compiler.dcx().set_allowed_diagnostic_codes(["5333".into()]);
122        let result = compiler.enter_mut(|compiler| -> solar::interface::Result {
123            let mut pcx = compiler.parse();
124
125            // Add the sources into the context.
126            // Include all sources in the source map so as to not re-load them from disk, but only
127            // parse and preprocess tests and scripts.
128            let mut preprocessed_paths = vec![];
129            let mut script_paths = HashSet::new();
130            let sources = &mut input.input.sources;
131            for (path, source) in sources.iter() {
132                if let Ok(src_file) = compiler
133                    .sess()
134                    .source_map()
135                    .new_source_file(path.clone(), source.content.as_str())
136                    && paths.is_test_or_script(path)
137                {
138                    pcx.add_file(src_file);
139                    if paths.is_script(path) {
140                        script_paths.insert(path.clone());
141                    }
142                    preprocessed_paths.push(path.clone());
143                }
144            }
145
146            // Parse and preprocess.
147            pcx.parse();
148            let ControlFlow::Continue(()) = compiler.lower_asts()? else { return Ok(()) };
149            compiler.dcx().has_errors()?;
150            let gcx = compiler.gcx();
151            // Collect tests and scripts dependencies and identify mock contracts.
152            // Script paths are passed separately so salted new-expressions are left untouched
153            // (Foundry's broadcast redirects native CREATE2 through the deterministic factory,
154            // but vm.deployCode runs at a deeper depth and bypasses that redirect).
155            let deps = PreprocessorDependencies::new(
156                gcx,
157                constructor_context,
158                &preprocessed_paths,
159                &script_paths,
160                paths,
161                source_units,
162                mocks,
163                preprocessor_state,
164            );
165            // Collect data of source contracts referenced in tests and scripts.
166            let data = collect_preprocessor_data(
167                gcx,
168                &deps.referenced_contracts,
169                &paths.root,
170                source_units,
171            );
172
173            // Extend existing sources with preprocessor deploy helper sources.
174            sources.extend(create_deploy_helpers(gcx, constructor_context, &data));
175
176            // Generate and apply preprocessor source updates.
177            let updates =
178                remove_bytecode_dependencies(gcx, &deps, &data, &paths.root, use_uint160_return);
179            apply_updates(sources, updates);
180
181            Ok(())
182        });
183
184        let diagnostics = convert_solar_errors(compiler.dcx());
185        if result.is_err() || diagnostics.is_err() {
186            let reason = diagnostics.err().map(|err| err.to_string());
187            if let Some(err) = &reason {
188                warn!(%err, "dynamic test linking analysis failed; using native bytecode");
189            } else {
190                warn!("dynamic test linking analysis failed; using native bytecode");
191            }
192            input.input.sources = original_sources;
193            let dependencies = native_import_dependencies(&parser_paths, &input.input.sources);
194            let affected = dependencies.len();
195            for (path, state) in dependencies {
196                if preprocessor_state.update(path.clone(), Some(state)) {
197                    mocks.remove(&path);
198                }
199            }
200            if affected > 0 {
201                let reason = reason
202                    .as_deref()
203                    .and_then(solar_error_summary)
204                    .unwrap_or_else(|| "Solar analysis failed".to_string());
205                let _ = sh_warn!("dynamic test linking disabled for {affected} files: {reason}");
206            }
207        } else if input.input.sources != original_sources {
208            self.rewrote_sources.store(true, Ordering::Relaxed);
209        }
210
211        Ok(())
212    }
213
214    fn is_source_context_sensitive(
215        &self,
216        source: &Path,
217        paths: &ProjectPathsConfig<SolcLanguage>,
218    ) -> bool {
219        // Context changes can alter rewriting eligibility even for previously native deployments.
220        paths.is_test_or_script(source)
221    }
222}
223
224impl Preprocessor<MultiCompiler> for DynamicTestLinkingPreprocessor {
225    fn cache_version(&self) -> u64 {
226        <Self as Preprocessor<SolcCompiler>>::cache_version(self)
227    }
228
229    fn preprocess(
230        &self,
231        compiler: &MultiCompiler,
232        input: &mut MultiCompilerInput,
233        paths: &ProjectPathsConfig<MultiCompilerLanguage>,
234        mocks: &mut HashSet<PathBuf>,
235    ) -> Result<()> {
236        let source_units = match input {
237            MultiCompilerInput::Solc(input) => {
238                input.input.sources.keys().cloned().collect::<Vec<_>>()
239            }
240            _ => Vec::new(),
241        };
242        self.preprocess_with_dependencies(
243            compiler,
244            input,
245            paths,
246            mocks,
247            &mut PreprocessorState::untracked(),
248            &source_units,
249        )
250    }
251
252    fn preprocess_with_dependencies(
253        &self,
254        compiler: &MultiCompiler,
255        input: &mut MultiCompilerInput,
256        paths: &ProjectPathsConfig<MultiCompilerLanguage>,
257        mocks: &mut HashSet<PathBuf>,
258        preprocessor_state: &mut PreprocessorState,
259        source_units: &[PathBuf],
260    ) -> Result<()> {
261        // Preprocess only Solc compilers.
262        let MultiCompilerInput::Solc(input) = input else { return Ok(()) };
263
264        let Some(solc) = &compiler.solc else { return Ok(()) };
265
266        let paths = paths.clone().with_language::<SolcLanguage>();
267        <Self as Preprocessor<SolcCompiler>>::preprocess_with_dependencies(
268            self,
269            solc,
270            input,
271            &paths,
272            mocks,
273            preprocessor_state,
274            source_units,
275        )
276    }
277
278    fn is_source_context_sensitive(
279        &self,
280        source: &Path,
281        paths: &ProjectPathsConfig<MultiCompilerLanguage>,
282    ) -> bool {
283        <Self as Preprocessor<SolcCompiler>>::is_source_context_sensitive(
284            self,
285            source,
286            paths.with_language_ref(),
287        )
288    }
289}
290
291/// Tracks transitive imports of unmodified tests and scripts after analysis fails.
292fn native_import_dependencies(
293    paths: &ProjectPathsConfig<SolcLanguage>,
294    sources: &foundry_compilers::artifacts::Sources,
295) -> NativeDependencies {
296    let Ok(graph) = Graph::<SolParser>::resolve_sources(paths, sources.clone()) else {
297        return sources
298            .keys()
299            .filter(|path| paths.is_test_or_script(path))
300            .map(|path| {
301                (normalize_path(&paths.root.join(path)), NativeDependencyState::Conservative)
302            })
303            .collect();
304    };
305
306    // Keep conservative invalidation only where import extraction itself is incomplete.
307    let mut incomplete = HashSet::new();
308    if graph.nodes.iter().any(|node| node.data.parse_result.is_err()) {
309        incomplete.extend(
310            graph
311                .nodes
312                .iter()
313                .filter(|node| SolData::parse(node.content(), node.path()).parse_result.is_err())
314                .map(|node| normalize_path(node.path())),
315        );
316    }
317    let graph_paths = graph.files().keys().cloned().collect::<HashSet<_>>();
318    let (_, edges) = graph.into_sources();
319    incomplete.extend(edges.unresolved_imports().iter().map(|(_, path)| normalize_path(path)));
320
321    let mut dependencies = NativeDependencies::new();
322    for path in sources.keys().filter(|path| paths.is_test_or_script(path)) {
323        let graph_path = paths.root.join(path);
324        let path = normalize_path(&graph_path);
325        let state = if graph_paths.contains(&graph_path) {
326            let imports =
327                edges.imports(&graph_path).into_iter().map(normalize_path).collect::<BTreeSet<_>>();
328            if incomplete.contains(&path) || incomplete.iter().any(|path| imports.contains(path)) {
329                NativeDependencyState::Conservative
330            } else {
331                NativeDependencyState::Known(imports)
332            }
333        } else {
334            NativeDependencyState::Conservative
335        };
336        merge_native_dependencies(&mut dependencies, NativeDependencies::from([(path, state)]));
337    }
338    dependencies
339}
340
341/// Extracts the first rendered Solar error header without ANSI codes.
342fn solar_error_summary(reason: &str) -> Option<String> {
343    anstream::adapter::strip_str(reason)
344        .to_string()
345        .lines()
346        .map(str::trim)
347        .find(|line| line.starts_with("error:") || line.starts_with("error["))
348        .map(str::to_string)
349}
350
351/// Returns the range of the given span in the source map.
352#[track_caller]
353fn span_to_range(source_map: &SourceMap, span: Span) -> Range<usize> {
354    source_map.span_to_range(span).unwrap()
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use alloy_primitives::keccak256;
361    use foundry_compilers::{
362        Compiler, CompilerInput,
363        artifacts::Source,
364        solc::{Solc, SolcSettings},
365    };
366    use semver::Version;
367
368    fn input() -> (tempfile::TempDir, ProjectPathsConfig<SolcLanguage>, SolcVersionedInput) {
369        let root = tempfile::tempdir().unwrap();
370        let paths = ProjectPathsConfig::builder().root(root.path()).build().unwrap();
371        let sources = [
372            ("src/Dep.sol", "contract Dep {}"),
373            ("test/Mock.sol", "import '../src/Dep.sol'; contract Mock is Dep {}"),
374            (
375                "test/Deploy.sol",
376                "import '../src/Dep.sol'; contract Deploy { bytes constant CODE = type(Dep).creationCode; bytes32 constant CODE_HASH = keccak256(type(Dep).creationCode); bytes32 immutable codeHash = keccak256(type(Dep).creationCode); function deploy() public returns (Dep) { return new Dep(); } function native() public pure returns (bytes memory) { return type(Dep).creationCode; } }",
377            ),
378        ]
379        .into_iter()
380        .map(|(path, content)| (PathBuf::from(path), Source::new(content)))
381        .collect();
382        let input = SolcVersionedInput::build(
383            sources,
384            SolcSettings::default(),
385            SolcLanguage::Solidity,
386            Version::new(0, 8, 30),
387        );
388        (root, paths, input)
389    }
390
391    fn assert_preprocessed(
392        paths: &ProjectPathsConfig<SolcLanguage>,
393        input: &SolcVersionedInput,
394        mocks: &HashSet<PathBuf>,
395    ) {
396        let source = &input.input.sources[&PathBuf::from("test/Deploy.sol")].content;
397        assert!(!source.contains("return new Dep();"), "eligible deployment was not rewritten");
398        assert!(source.contains("type(Dep).creationCode"), "native dependency was lost");
399        assert!(
400            source.contains("bytes constant CODE = type(Dep).creationCode"),
401            "constant creation code was rewritten"
402        );
403        assert!(
404            source.contains("bytes32 constant CODE_HASH = keccak256(type(Dep).creationCode)"),
405            "constant creation code hash was rewritten"
406        );
407        assert!(
408            !source.contains("immutable codeHash = keccak256(type(Dep).creationCode)"),
409            "constant preservation leaked into the immutable initializer"
410        );
411        assert!(
412            source.contains("immutable codeHash = keccak256(VmContractHelper"),
413            "immutable creation code was not rewritten"
414        );
415        assert!(mocks.contains(&paths.root.join("test/Mock.sol")));
416    }
417
418    #[test]
419    fn helper_sources_are_stable_across_compiler_jobs() {
420        let (_root, paths, mut input) = input();
421        input.input.sources = [
422            ("src/Dep.sol", "contract Dep { constructor(uint256 value) {} }".to_string()),
423            ("test/Deploy.sol", "import '../src/Dep.sol'; contract Deploy { function deploy() public { new Dep(7); } }".to_string()),
424        ].into_iter().map(|(path, source)| (PathBuf::from(path), Source::new(source))).collect();
425        let mut wider = input.clone();
426        // These declarations would also affect collision avoidance if it inspected unrelated files.
427        let dep_id = format!("{:x}", keccak256("src/Dep.sol:Dep"));
428        let test_id = format!("{:x}", keccak256("test/Deploy.sol:Deploy"));
429        wider.input.sources.insert(PathBuf::from("test/AAA.sol"), Source::new(format!(
430            "contract Extra {{}} interface VmContractHelper{test_id} {{}} contract DeployHelper{dep_id} {{}} function encodeArgs{dep_id}() pure {{}}"
431        )));
432        for input in [&mut input, &mut wider] {
433            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
434                &DynamicTestLinkingPreprocessor::default(),
435                &SolcCompiler::default(),
436                input,
437                &paths,
438                &mut HashSet::new(),
439            )
440            .unwrap();
441        }
442        for (path, source) in &input.input.sources {
443            assert_eq!(source.content, wider.input.sources[path].content, "{path:?}");
444        }
445    }
446
447    #[test]
448    fn direct_solc_preprocess_tracks_mocks_without_cache_context() {
449        let (_root, paths, mut input) = input();
450        let mut mocks = HashSet::new();
451        <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
452            &DynamicTestLinkingPreprocessor::default(),
453            &SolcCompiler::default(),
454            &mut input,
455            &paths,
456            &mut mocks,
457        )
458        .unwrap();
459        assert_preprocessed(&paths, &input, &mocks);
460    }
461
462    #[test]
463    fn direct_multi_preprocess_tracks_mocks_without_cache_context() {
464        let (_root, paths, input) = input();
465        let mut input = MultiCompilerInput::Solc(Box::new(input));
466        let mut mocks = HashSet::new();
467        <DynamicTestLinkingPreprocessor as Preprocessor<MultiCompiler>>::preprocess(
468            &DynamicTestLinkingPreprocessor::default(),
469            &MultiCompiler { solc: Some(SolcCompiler::default()), vyper: None },
470            &mut input,
471            paths.with_language_ref(),
472            &mut mocks,
473        )
474        .unwrap();
475        let MultiCompilerInput::Solc(input) = input else { unreachable!() };
476        assert_preprocessed(&paths, &input, &mocks);
477    }
478
479    #[test]
480    fn constructor_abi_context_preserves_source_pragmas_and_version_defaults() {
481        for (minor, pragma, native) in [
482            (7, "", true),
483            (8, "", false),
484            (8, "pragma abicoder /* comment */ v1;", true),
485            (7, "pragma abicoder v2;", false),
486            (7, "pragma experimental ABIEncoderV2;", false),
487        ] {
488            let (_root, paths, mut input) = input();
489            input.version = Version::new(0, minor, 6);
490            input.input.sources = [
491                ("src/Dep.sol", "contract Dep { constructor(uint256 n) {} }".to_string()),
492                ("test/Deploy.sol", format!("{pragma} import '../src/Dep.sol'; contract Deploy {{ function deploy() public {{ new Dep(7); }} }}")),
493                // A pragma in another source must not change this source's ABI context.
494                ("test/Other.sol", "pragma abicoder v2; import '../src/Dep.sol'; contract Other { function deploy() public { new Dep(7); } }".to_string()),
495            ].into_iter().map(|(path, source)| (PathBuf::from(path), Source::new(source))).collect();
496            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
497                &DynamicTestLinkingPreprocessor::default(),
498                &SolcCompiler::default(),
499                &mut input,
500                &paths,
501                &mut HashSet::new(),
502            )
503            .unwrap();
504            let source = &input.input.sources[&PathBuf::from("test/Deploy.sol")].content;
505            assert_eq!(source.contains("new Dep(7)"), native, "0.{minor}.6 {pragma}");
506            let other = &input.input.sources[&PathBuf::from("test/Other.sol")].content;
507            assert!(!other.contains("new Dep(7)"), "unrelated v2 source was not rewritten");
508        }
509    }
510
511    #[test]
512    fn constructor_salt_uses_compiler_evm_target() {
513        for (evm_version, native) in [
514            (None, true),
515            (Some(EvmVersion::Byzantium), true),
516            (Some(EvmVersion::Constantinople), false),
517            (Some(EvmVersion::Prague), false),
518        ] {
519            let (_root, paths, mut input) = input();
520            input.input.settings.evm_version = evm_version;
521            input.input.sources.insert(
522                PathBuf::from("test/Deploy.sol"),
523                Source::new("pragma abicoder v1; import '../src/Dep.sol'; contract Deploy { function deploy() public { new Dep{salt: bytes32(0)}(); } function plain() public { new Dep(); } }"),
524            );
525            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
526                &DynamicTestLinkingPreprocessor::default(),
527                &SolcCompiler::default(),
528                &mut input,
529                &paths,
530                &mut HashSet::new(),
531            )
532            .unwrap();
533            let source = &input.input.sources[&PathBuf::from("test/Deploy.sol")].content;
534            assert_eq!(source.contains("new Dep{salt:"), native, "{evm_version:?}");
535            assert!(
536                !source.contains("new Dep();"),
537                "ordinary parameterless CREATE was not rewritten"
538            );
539        }
540    }
541
542    #[test]
543    fn constructor_call_options_are_preserved() {
544        let (_root, paths, mut input) = input();
545        input.input.sources.insert(
546            PathBuf::from("src/Dep.sol"),
547            Source::new("contract Dep { constructor() payable {} }"),
548        );
549        input.input.sources.insert(
550            PathBuf::from("test/Deploy.sol"),
551            Source::new("import '../src/Dep.sol'; contract Deploy { function deploy() public { new Dep{value: 1, salt: bytes32(7)}(); } }"),
552        );
553        <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
554            &DynamicTestLinkingPreprocessor::default(),
555            &SolcCompiler::default(),
556            &mut input,
557            &paths,
558            &mut HashSet::new(),
559        )
560        .unwrap();
561        let source = &input.input.sources[&PathBuf::from("test/Deploy.sol")].content;
562        assert!(!source.contains("new Dep{value:"), "deployment was not rewritten");
563        assert!(source.contains("_value: 1"), "value option was not preserved");
564        assert!(source.contains("_salt: bytes32(7)"), "salt option was not preserved");
565    }
566
567    #[test]
568    fn return_data_fallback_follows_helpers_without_disabling_unrelated_contracts() {
569        for (helper, declarations, expression) in [
570            (
571                "function size(uint256) view returns (uint256 n) { assembly { n := returndatasize() } }",
572                "import * as R from '../src/Read.sol';",
573                "R.size(0)",
574            ),
575            (
576                "function size(uint256) view returns (uint256 n) { assembly { n := returndatasize() } }",
577                "import {size} from '../src/Read.sol'; using {size} for uint256;",
578                "uint256(0).size()",
579            ),
580            (
581                "function size(uint256) view returns (uint256 n) { assembly { n := returndatasize() } }",
582                "import {size as readSize} from '../src/Read.sol'; using {readSize} for uint256;",
583                "uint256(0).readSize()",
584            ),
585            (
586                "library R { function size(uint256) internal view returns (uint256 n) { assembly { n := returndatasize() } } }",
587                "import {R} from '../src/Read.sol'; using R for uint256;",
588                "uint256(0).size()",
589            ),
590            (
591                "library R { function size(uint256) internal view returns (uint256 n) { assembly { n := returndatasize() } } }",
592                "import {R} from '../src/Read.sol';",
593                "R.size(0)",
594            ),
595            (
596                "library R { function size(uint256) internal pure returns (uint256 n) { assembly { n := returndatasize() } } }",
597                "import {R} from '../src/Read.sol';",
598                "(R).size(0)",
599            ),
600            (
601                "type Word is uint256; using {size as -} for Word global; function size(Word) pure returns (Word) { uint256 n; assembly { n := returndatasize() } return Word.wrap(n); }",
602                "import {Word} from '../src/Read.sol';",
603                "Word.unwrap(-Word.wrap(0))",
604            ),
605            (
606                "type Word is uint256; using {size as +} for Word global; function size(Word, Word) pure returns (Word) { uint256 n; assembly { n := returndatasize() } return Word.wrap(n); }",
607                "import {Word} from '../src/Read.sol';",
608                "Word.unwrap(Word.wrap(0) + Word.wrap(0))",
609            ),
610        ] {
611            let (_root, paths, mut input) = input();
612            input.input.sources.insert(PathBuf::from("src/Read.sol"), Source::new(helper));
613            let observer = format!(
614                "{declarations} import '../src/Dep.sol'; contract Observer {{ function observe() public {{ new Dep(); require({expression} == 0); }} }}"
615            );
616            let safe = "import '../src/Read.sol'; import '../src/Dep.sol'; contract Safe { function deploy() public { new Dep(); } }";
617            input.input.sources.insert(PathBuf::from("test/Observer.sol"), Source::new(&observer));
618            input.input.sources.insert(PathBuf::from("test/Safe.sol"), Source::new(safe));
619            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
620                &DynamicTestLinkingPreprocessor::default(),
621                &SolcCompiler::default(),
622                &mut input,
623                &paths,
624                &mut HashSet::new(),
625            )
626            .unwrap();
627            assert_eq!(
628                input.input.sources[&PathBuf::from("test/Observer.sol")].content.as_str(),
629                observer
630            );
631            assert_ne!(input.input.sources[&PathBuf::from("test/Safe.sol")].content.as_str(), safe);
632        }
633    }
634
635    #[test]
636    fn return_data_fallback_includes_inherited_observers() {
637        assert_return_data_scope_native(
638            "abstract contract Base { function make() internal virtual; function observe() public { make(); uint256 n; assembly { n := returndatasize() } require(n == 0); } } contract Derived is Base { function make() internal override { new Dep(); } }",
639        );
640    }
641
642    #[test]
643    fn return_data_fallback_collects_using_before_initializers() {
644        assert_return_data_scope_native(
645            "library Reader { function size(uint256) internal view returns (uint256 n) { assembly { n := returndatasize() } } } contract Initializer { using Reader for uint256; Dep target = new Dep(); uint256 observed = uint256(0).size(); function observe() public view { require(observed == 0); } }",
646        );
647    }
648
649    fn assert_return_data_scope_native(declarations: &str) {
650        let (_root, paths, mut input) = input();
651        let observer = format!("import '../src/Dep.sol'; {declarations}");
652        let safe =
653            "import '../src/Dep.sol'; contract Safe { function deploy() public { new Dep(); } }";
654        input.input.sources.insert(PathBuf::from("test/Observer.sol"), Source::new(&observer));
655        input.input.sources.insert(PathBuf::from("test/Safe.sol"), Source::new(safe));
656        <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
657            &DynamicTestLinkingPreprocessor::default(),
658            &SolcCompiler::default(),
659            &mut input,
660            &paths,
661            &mut HashSet::new(),
662        )
663        .unwrap();
664        assert_eq!(
665            input.input.sources[&PathBuf::from("test/Observer.sol")].content.as_str(),
666            observer
667        );
668        assert_ne!(input.input.sources[&PathBuf::from("test/Safe.sol")].content.as_str(), safe);
669    }
670
671    #[test]
672    fn return_data_namespace_resolution_selects_exports() {
673        for (namespace, expression, native) in [
674            ("Read", "R.identity(7)", false),
675            ("Read", "R.size(0)", true),
676            ("Export", "R.identity(7)", false),
677            ("Export", "R.observe(0)", true),
678            ("Nested", "R.Inner.identity(7)", false),
679            ("Nested", "R.Inner.observe(0)", true),
680            ("Read", "R.Reader.identity(7)", false),
681            ("Read", "R.Reader.size(0)", true),
682            ("Export", "R.Renamed.identity(7)", false),
683            ("Export", "R.Renamed.size(0)", true),
684        ] {
685            let (_root, paths, mut input) = input();
686            for (path, source) in [
687                (
688                    "src/Read.sol",
689                    "function identity(uint256 n) pure returns (uint256) { return n; } function size(uint256) view returns (uint256 n) { assembly { n := returndatasize() } } library Reader { function identity(uint256 n) internal pure returns (uint256) { return n; } function size(uint256) internal view returns (uint256 n) { assembly { n := returndatasize() } } }",
690                ),
691                (
692                    "src/Export.sol",
693                    "import {size as observe, identity, Reader as Renamed} from './Read.sol';",
694                ),
695                ("src/Nested.sol", "import * as Inner from './Export.sol';"),
696            ] {
697                input.input.sources.insert(PathBuf::from(path), Source::new(source));
698            }
699            let source = format!(
700                "import '../src/Dep.sol'; import * as R from '../src/{namespace}.sol'; contract Case {{ function run() public {{ new Dep(); {expression}; }} }}"
701            );
702            input.input.sources.insert(PathBuf::from("test/Case.sol"), Source::new(&source));
703            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
704                &DynamicTestLinkingPreprocessor::default(),
705                &SolcCompiler::default(),
706                &mut input,
707                &paths,
708                &mut HashSet::new(),
709            )
710            .unwrap();
711            let actual = &input.input.sources[&PathBuf::from("test/Case.sol")].content;
712            assert_eq!(actual.contains("new Dep();"), native, "{namespace}: {expression}");
713        }
714    }
715
716    #[test]
717    fn analysis_fallback_tracks_transitive_imports() {
718        let (_root, mut paths, mut input) = input();
719        paths.remappings.push("dep/=lib/dep/".parse().unwrap());
720        input.input.sources = [
721            ("lib/dep/Leaf.sol", "contract Leaf {}"),
722            ("src/Middle.sol", "import 'dep/Leaf.sol'; contract Middle {}"),
723            ("src/Unrelated.sol", "contract Unrelated {}"),
724            ("test/Fallback.sol", "import '../src/Middle.sol'; contract Fallback is Missing {}"),
725            ("test/Independent.sol", "contract Independent {}"),
726            ("script/Deploy.sol", "import '../src/Middle.sol'; contract Deploy {}"),
727        ]
728        .into_iter()
729        .map(|(path, source)| (PathBuf::from(path), Source::new(source)))
730        .collect();
731        for (path, source) in &input.input.sources {
732            let path = paths.root.join(path);
733            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
734            std::fs::write(path, source.content.as_str()).unwrap();
735        }
736        let original = input.input.sources.clone();
737        let mut mocks = HashSet::from([paths.root.join("test/Fallback.sol")]);
738        <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
739            &DynamicTestLinkingPreprocessor::default(),
740            &SolcCompiler::default(),
741            &mut input,
742            &paths,
743            &mut mocks,
744        )
745        .unwrap();
746        assert_eq!(input.input.sources, original);
747        assert!(mocks.is_empty());
748        let dependencies = native_import_dependencies(&paths, &input.input.sources);
749        let imports = BTreeSet::from([
750            paths.root.join("src/Middle.sol"),
751            paths.root.join("lib/dep/Leaf.sol"),
752        ]);
753        assert_eq!(
754            dependencies,
755            NativeDependencies::from([
756                (
757                    paths.root.join("test/Fallback.sol"),
758                    NativeDependencyState::Known(imports.clone())
759                ),
760                (paths.root.join("script/Deploy.sol"), NativeDependencyState::Known(imports)),
761                (
762                    paths.root.join("test/Independent.sol"),
763                    NativeDependencyState::Known(BTreeSet::new())
764                ),
765            ])
766        );
767    }
768
769    #[test]
770    fn analysis_fallback_limits_incomplete_imports_to_consumers() {
771        let (_root, paths, mut input) = input();
772        input.input.sources = [
773            ("src/AHealthy.sol", "contract AHealthy {}"),
774            (
775                "test/Fallback.sol",
776                "import /* Solar's regex fallback cannot recover this */ '../src/AHealthy.sol'; contract Fallback { function fail() public { throw; } }",
777            ),
778            (
779                "test/Healthy.sol",
780                "import '../src/AHealthy.sol'; contract Healthy is AHealthy {}",
781            ),
782            ("test/Independent.sol", "contract Independent {}"),
783        ]
784        .into_iter()
785        .map(|(path, source)| (PathBuf::from(path), Source::new(source)))
786        .collect();
787        for (path, source) in &input.input.sources {
788            let path = paths.root.join(path);
789            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
790            std::fs::write(path, source.content.as_str()).unwrap();
791        }
792        let dependencies = native_import_dependencies(&paths, &input.input.sources);
793        assert_eq!(
794            dependencies,
795            NativeDependencies::from([
796                (paths.root.join("test/Fallback.sol"), NativeDependencyState::Conservative),
797                (
798                    paths.root.join("test/Healthy.sol"),
799                    NativeDependencyState::Known(BTreeSet::from([paths
800                        .root
801                        .join("src/AHealthy.sol")]))
802                ),
803                (
804                    paths.root.join("test/Independent.sol"),
805                    NativeDependencyState::Known(BTreeSet::new())
806                ),
807            ])
808        );
809    }
810
811    #[test]
812    fn analysis_fallback_merges_normalized_graph_paths() {
813        let (_root, mut paths, mut input) = input();
814        paths.remappings.push("test/z/:src/=lib/alternate/".parse().unwrap());
815        input.input.sources = [
816            ("src/Dep.sol", "contract Dep {}"),
817            ("lib/alternate/Dep.sol", "contract Dep {}"),
818            ("test/A.sol", "import '../src/Dep.sol'; contract A is Dep {}"),
819            ("test/z/../A.sol", "import '../src/Dep.sol'; contract A is Dep {}"),
820        ]
821        .into_iter()
822        .map(|(path, source)| (PathBuf::from(path), Source::new(source)))
823        .collect();
824        for (path, source) in &input.input.sources {
825            let path = paths.root.join(path);
826            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
827            std::fs::write(path, source.content.as_str()).unwrap();
828        }
829        let dependencies = native_import_dependencies(&paths, &input.input.sources);
830        assert_eq!(
831            dependencies,
832            NativeDependencies::from([(
833                paths.root.join("test/A.sol"),
834                NativeDependencyState::Known(BTreeSet::from([
835                    paths.root.join("src/Dep.sol"),
836                    paths.root.join("lib/alternate/Dep.sol")
837                ]))
838            )])
839        );
840    }
841
842    #[test]
843    fn solar_error_summary_accepts_coded_and_colored_errors() {
844        assert_eq!(
845            solar_error_summary(
846                "solar reported errors:\n\n\u{1b}[1;91merror\u{1b}[0m\u{1b}[1m: boom\u{1b}[0m"
847            ),
848            Some("error: boom".to_string())
849        );
850        assert_eq!(
851            solar_error_summary("solar reported errors:\n\n\u{1b}[31merror[1234]: boom\u{1b}[0m"),
852            Some("error[1234]: boom".to_string())
853        );
854        assert_eq!(
855            solar_error_summary("solar reported errors:\n\nerror: boom"),
856            Some("error: boom".to_string())
857        );
858        assert_eq!(solar_error_summary("solar reported 1 error"), None);
859    }
860
861    #[test]
862    fn deploy_helper_preserves_target_abi_coder() {
863        for (minor, patch, pragma, needs_v2) in [
864            (7, 6, "", false),
865            (7, 6, "pragma abicoder v2;", true),
866            (7, 6, "pragma experimental ABIEncoderV2;", true),
867            (8, 0, "", false),
868            (8, 37, "", false),
869            (8, 37, "pragma abicoder v2;", false),
870            (8, 37, "pragma experimental ABIEncoderV2;", false),
871            (8, 37, "pragma abicoder v1;", false),
872        ] {
873            let (_root, paths, mut input) = input();
874            input.version = Version::new(0, minor, patch);
875            input.input.sources = [
876                ("src/Dep.sol", format!("{pragma} contract Dep {{ constructor(uint256 n) {{}} }}")),
877                ("test/Deploy.sol", "pragma experimental ABIEncoderV2; import '../src/Dep.sol'; contract Deploy { function deploy() public { new Dep(7); } }".to_string()),
878            ].into_iter().map(|(path, source)| (PathBuf::from(path), Source::new(source))).collect();
879            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
880                &DynamicTestLinkingPreprocessor::default(),
881                &SolcCompiler::default(),
882                &mut input,
883                &paths,
884                &mut HashSet::new(),
885            )
886            .unwrap();
887            let helpers = input
888                .input
889                .sources
890                .iter()
891                .filter(|(path, _)| is_deploy_helper_path(path))
892                .collect::<Vec<_>>();
893            assert_eq!(helpers.len(), 1);
894            assert_eq!(
895                helpers[0].1.content.matches("pragma experimental ABIEncoderV2;").count(),
896                usize::from(needs_v2),
897                "0.{minor}.{patch} {pragma}"
898            );
899            let source = &input.input.sources[&PathBuf::from("test/Deploy.sol")].content;
900            assert_eq!(source.matches("new Dep(7)").count(), 0);
901            assert_eq!(source.matches(".deployCode({").count(), 1);
902        }
903    }
904
905    #[test]
906    fn preprocessor_cache_versions_match() {
907        assert_eq!(
908            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::cache_version(
909                &DynamicTestLinkingPreprocessor::default()
910            ),
911            <DynamicTestLinkingPreprocessor as Preprocessor<MultiCompiler>>::cache_version(
912                &DynamicTestLinkingPreprocessor::default()
913            )
914        );
915    }
916
917    #[test]
918    fn uint160_deployment_returns_follow_compiler_settings() {
919        for (version, via_ir, specific, workaround) in [
920            ("0.8.14", Some(true), false, false),
921            ("0.8.15", Some(true), false, true),
922            ("0.8.24+commit.e11b9ed9", Some(true), false, true),
923            ("0.8.25", Some(true), false, true),
924            ("0.8.26", Some(true), false, false),
925            ("0.8.24", Some(false), false, false),
926            ("0.8.24", None, false, false),
927            ("0.8.24-dev", Some(true), false, false),
928            ("0.8.24-dev", Some(true), true, false),
929            ("0.8.24+commit.e11b9ed9", Some(true), true, true),
930        ] {
931            let (_root, paths, mut input) = input();
932            input.version = version.parse().unwrap();
933            input.input.settings.via_ir = via_ir;
934            let compiler = if specific {
935                let compiler = SolcCompiler::Specific(Solc::new_with_version(
936                    paths.root.join("absent-solc"),
937                    input.version.clone(),
938                ));
939                // Compiler selection strips metadata before constructing the versioned input.
940                let selected = compiler.available_versions(&SolcLanguage::Solidity).pop().unwrap();
941                assert!(selected.as_ref().pre.is_empty());
942                let mut settings = SolcSettings::default();
943                settings.settings.via_ir = via_ir;
944                input = SolcVersionedInput::build(
945                    input.input.sources,
946                    settings,
947                    SolcLanguage::Solidity,
948                    selected.as_ref().clone(),
949                );
950                compiler
951            } else {
952                SolcCompiler::default()
953            };
954            <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::preprocess(
955                &DynamicTestLinkingPreprocessor::default(),
956                &compiler,
957                &mut input,
958                &paths,
959                &mut HashSet::new(),
960            )
961            .unwrap();
962            let source = &input.input.sources[&PathBuf::from("test/Deploy.sol")].content;
963            assert_eq!(
964                source.matches("external returns (uint160);").count(),
965                if workaround { 8 } else { 0 },
966                "{version}, via_ir={via_ir:?}, specific={specific}"
967            );
968            assert_eq!(
969                source.matches("external returns (address);").count(),
970                if workaround { 0 } else { 8 }
971            );
972            assert_eq!(source.matches("payable(address(").count(), usize::from(workaround));
973        }
974    }
975
976    #[test]
977    fn source_context_sensitivity_uses_configured_directories() {
978        let root = tempfile::tempdir().unwrap();
979        let mut paths = ProjectPathsConfig::builder().root(root.path()).build().unwrap();
980        paths.tests = paths.root.join("src/spec");
981        paths.scripts = paths.root.join("ops");
982        let multi_paths = paths.clone().with_language::<MultiCompilerLanguage>();
983        let preprocessor = DynamicTestLinkingPreprocessor::default();
984        for (source, expected) in [
985            ("src/spec/Helper.sol", true),
986            ("ops/Utility.sol", true),
987            ("test/Default.t.sol", false),
988            ("src/LooksLikeTest.t.sol", false),
989            ("lib/Spec.t.sol", false),
990        ] {
991            let source = paths.root.join(source);
992            assert_eq!(
993                <DynamicTestLinkingPreprocessor as Preprocessor<SolcCompiler>>::is_source_context_sensitive(
994                    &preprocessor, &source, &paths,
995                ),
996                expected,
997                "{}", source.display(),
998            );
999            assert_eq!(
1000                <DynamicTestLinkingPreprocessor as Preprocessor<MultiCompiler>>::is_source_context_sensitive(
1001                    &preprocessor, &source, &multi_paths,
1002                ),
1003                expected,
1004                "{}", source.display(),
1005            );
1006        }
1007    }
1008}