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#[derive(Clone, Debug, Default)]
43pub struct DynamicTestLinkingPreprocessor {
44 rewrote_sources: Arc<AtomicBool>,
46}
47
48impl DynamicTestLinkingPreprocessor {
49 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 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 supports_create2: input
99 .input
100 .settings
101 .evm_version
102 .is_some_and(|version| version >= EvmVersion::Constantinople),
103 };
104 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 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 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 pcx.parse();
148 let ControlFlow::Continue(()) = compiler.lower_asts()? else { return Ok(()) };
149 compiler.dcx().has_errors()?;
150 let gcx = compiler.gcx();
151 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 let data = collect_preprocessor_data(
167 gcx,
168 &deps.referenced_contracts,
169 &paths.root,
170 source_units,
171 );
172
173 sources.extend(create_deploy_helpers(gcx, constructor_context, &data));
175
176 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 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 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
291fn 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 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
341fn 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#[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 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 ("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 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}