Skip to content

Commit 719d4e8

Browse files
committed
Autodetect and recompile mocks
1 parent c354a7c commit 719d4e8

File tree

4 files changed

+120
-55
lines changed

4 files changed

+120
-55
lines changed

crates/compilers/src/cache.rs

+26-5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub struct CompilerCache<S = Settings> {
4747
pub builds: BTreeSet<String>,
4848
pub profiles: BTreeMap<String, S>,
4949
pub preprocessed: bool,
50+
pub mocks: HashSet<PathBuf>,
5051
}
5152

5253
impl<S> CompilerCache<S> {
@@ -58,6 +59,7 @@ impl<S> CompilerCache<S> {
5859
builds: Default::default(),
5960
profiles: Default::default(),
6061
preprocessed,
62+
mocks: Default::default(),
6163
}
6264
}
6365
}
@@ -381,6 +383,7 @@ impl<S> Default for CompilerCache<S> {
381383
paths: Default::default(),
382384
profiles: Default::default(),
383385
preprocessed: false,
386+
mocks: Default::default(),
384387
}
385388
}
386389
}
@@ -792,7 +795,7 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
792795
self.missing_extra_files()
793796
}
794797

795-
// Walks over all cache entires, detects dirty files and removes them from cache.
798+
// Walks over all cache entries, detects dirty files and removes them from cache.
796799
fn find_and_remove_dirty(&mut self) {
797800
fn populate_dirty_files<D>(
798801
file: &Path,
@@ -892,13 +895,21 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
892895
self.dirty_sources.insert(file.clone());
893896
break;
894897
// For non-src files we mark them as dirty only if they import dirty
895-
// non-src file or src file for which
896-
// interface representation changed.
898+
// non-src file or src file for which interface representation changed.
899+
// For identified mock contracts (non-src contracts that extends contracts
900+
// from src file) we mark edges as dirty.
897901
} else if !is_src
898902
&& self.dirty_sources.contains(import)
899-
&& (!self.is_source_file(import) || self.is_dirty_impl(import, true))
903+
&& (!self.is_source_file(import)
904+
|| self.is_dirty_impl(import, true)
905+
|| self.cache.mocks.contains(file))
900906
{
901-
self.dirty_sources.insert(file.clone());
907+
if self.cache.mocks.contains(file) {
908+
// Mark all mock edges as dirty.
909+
populate_dirty_files(file, &mut self.dirty_sources, &edges);
910+
} else {
911+
self.dirty_sources.insert(file.clone());
912+
}
902913
}
903914
}
904915
}
@@ -1122,6 +1133,16 @@ impl<'a, T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
11221133
}
11231134
}
11241135

1136+
/// Adds the file's hashes to the set if not set yet
1137+
pub fn add_mocks(&mut self, mocks: Option<HashSet<PathBuf>>) {
1138+
if let Some(mocks) = mocks {
1139+
match self {
1140+
ArtifactsCache::Ephemeral(..) => {}
1141+
ArtifactsCache::Cached(cache) => cache.cache.mocks.extend(mocks),
1142+
}
1143+
}
1144+
}
1145+
11251146
/// Filters out those sources that don't need to be compiled
11261147
pub fn filter(&mut self, sources: &mut Sources, version: &Version, profile: &str) {
11271148
match self {

crates/compilers/src/compile/project.rs

+13-3
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,25 @@ use crate::{
115115
use foundry_compilers_core::error::Result;
116116
use rayon::prelude::*;
117117
use semver::Version;
118-
use std::{collections::HashMap, fmt::Debug, path::PathBuf, time::Instant};
118+
use std::{
119+
collections::{HashMap, HashSet},
120+
fmt::Debug,
121+
path::PathBuf,
122+
time::Instant,
123+
};
119124

120125
/// A set of different Solc installations with their version and the sources to be compiled
121126
pub(crate) type VersionedSources<'a, L, S> = HashMap<L, Vec<(Version, Sources, (&'a str, &'a S))>>;
122127

123128
/// Invoked before the actual compiler invocation and can override the input.
129+
/// Returns preprocessed compiler input and identified mocks (if any) to be stored in cache.
124130
pub trait Preprocessor<C: Compiler>: Debug {
125131
fn preprocess(
126132
&self,
127133
compiler: &C,
128134
input: C::Input,
129135
paths: &ProjectPathsConfig<C::Language>,
130-
) -> Result<C::Input>;
136+
) -> Result<(C::Input, Option<HashSet<PathBuf>>)>;
131137
}
132138

133139
#[derive(Debug)]
@@ -469,6 +475,7 @@ impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {
469475
include_paths.extend(graph.include_paths().clone());
470476

471477
let mut jobs = Vec::new();
478+
let mut mocks = None;
472479
for (language, versioned_sources) in self.sources {
473480
for (version, sources, (profile, opt_settings)) in versioned_sources {
474481
let mut opt_settings = opt_settings.clone();
@@ -503,13 +510,16 @@ impl<L: Language, S: CompilerSettings> CompilerSources<'_, L, S> {
503510
input.strip_prefix(project.paths.root.as_path());
504511

505512
if let Some(preprocessor) = preprocessor.as_ref() {
506-
input = preprocessor.preprocess(&project.compiler, input, &project.paths)?;
513+
(input, mocks) =
514+
preprocessor.preprocess(&project.compiler, input, &project.paths)?;
507515
}
508516

509517
jobs.push((input, profile, actually_dirty));
510518
}
511519
}
512520

521+
cache.add_mocks(mocks);
522+
513523
let results = if let Some(num_jobs) = jobs_cnt {
514524
compile_parallel(&project.compiler, jobs, num_jobs)
515525
} else {

crates/compilers/src/preprocessor/deps.rs

+30-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use solar_sema::{
1515
use std::{
1616
collections::{BTreeMap, BTreeSet, HashSet},
1717
ops::ControlFlow,
18-
path::PathBuf,
18+
path::{Path, PathBuf},
1919
};
2020

2121
/// Holds data about referenced source contracts and bytecode dependencies.
@@ -24,12 +24,21 @@ pub(crate) struct PreprocessorDependencies {
2424
pub preprocessed_contracts: BTreeMap<ContractId, Vec<BytecodeDependency>>,
2525
// Referenced contract ids.
2626
pub referenced_contracts: HashSet<ContractId>,
27+
// Mock contract paths (with a base contract from src dir).
28+
pub mocks: HashSet<PathBuf>,
2729
}
2830

2931
impl PreprocessorDependencies {
30-
pub fn new(sess: &Session, hir: &Hir<'_>, paths: &[PathBuf], src_dir: &PathBuf) -> Self {
32+
pub fn new(
33+
sess: &Session,
34+
hir: &Hir<'_>,
35+
paths: &[PathBuf],
36+
src_dir: &PathBuf,
37+
root_dir: &Path,
38+
) -> Self {
3139
let mut preprocessed_contracts = BTreeMap::new();
3240
let mut referenced_contracts = HashSet::new();
41+
let mut mocks = HashSet::new();
3342
for contract_id in hir.contract_ids() {
3443
let contract = hir.contract(contract_id);
3544
let source = hir.source(contract.source);
@@ -40,6 +49,24 @@ impl PreprocessorDependencies {
4049

4150
// Collect dependencies only for tests and scripts.
4251
if !paths.contains(path) {
52+
let path = path.display();
53+
trace!("{path} is not test or script");
54+
continue;
55+
}
56+
57+
// Do not collect dependencies for mock contracts. Walk through base contracts and
58+
// check if they're from src dir.
59+
if contract.linearized_bases.iter().any(|base_contract_id| {
60+
let base_contract = hir.contract(*base_contract_id);
61+
let FileName::Real(path) = &hir.source(base_contract.source).file.name else {
62+
return false;
63+
};
64+
path.starts_with(src_dir)
65+
}) {
66+
// Record mock contracts to be evicted from preprocessed cache.
67+
mocks.insert(root_dir.join(path));
68+
let path = path.display();
69+
trace!("found mock contract {path}");
4370
continue;
4471
}
4572

@@ -58,7 +85,7 @@ impl PreprocessorDependencies {
5885
// Record collected referenced contract ids.
5986
referenced_contracts.extend(deps_collector.referenced_contracts);
6087
}
61-
Self { preprocessed_contracts, referenced_contracts }
88+
Self { preprocessed_contracts, referenced_contracts, mocks }
6289
}
6390
}
6491

crates/compilers/src/preprocessor/mod.rs

+51-44
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ use solar_parse::{
2323
Parser,
2424
};
2525
use solar_sema::{hir::Arena, ParsingContext};
26-
use std::path::{Path, PathBuf};
26+
use std::{
27+
collections::HashSet,
28+
path::{Path, PathBuf},
29+
};
2730

2831
mod data;
2932
mod deps;
@@ -60,62 +63,66 @@ impl Preprocessor<SolcCompiler> for TestOptimizerPreprocessor {
6063
_solc: &SolcCompiler,
6164
mut input: SolcVersionedInput,
6265
paths: &ProjectPathsConfig<SolcLanguage>,
63-
) -> Result<SolcVersionedInput> {
66+
) -> Result<(SolcVersionedInput, Option<HashSet<PathBuf>>)> {
6467
let sources = &mut input.input.sources;
6568
// Skip if we are not preprocessing any tests or scripts. Avoids unnecessary AST parsing.
6669
if sources.iter().all(|(path, _)| !is_test_or_script(path, paths)) {
6770
trace!("no tests or sources to preprocess");
68-
return Ok(input);
71+
return Ok((input, None));
6972
}
7073

7174
let sess = Session::builder().with_buffer_emitter(Default::default()).build();
72-
let _ = sess.enter_parallel(|| -> solar_parse::interface::Result<()> {
73-
let hir_arena = Arena::new();
74-
let mut parsing_context = ParsingContext::new(&sess);
75-
// Set remappings into HIR parsing context.
76-
for remapping in &paths.remappings {
77-
parsing_context
78-
.file_resolver
79-
.add_import_map(PathBuf::from(&remapping.name), PathBuf::from(&remapping.path));
80-
}
81-
// Load and parse test and script contracts only (dependencies are automatically
82-
// resolved).
83-
let preprocessed_paths = sources
84-
.into_iter()
85-
.filter(|(path, source)| {
86-
is_test_or_script(path, paths) && !source.content.is_empty()
87-
})
88-
.map(|(path, _)| path.clone())
89-
.collect_vec();
90-
parsing_context.load_files(&preprocessed_paths)?;
75+
let mocks = sess
76+
.enter_parallel(|| -> solar_parse::interface::Result<Option<HashSet<PathBuf>>> {
77+
let hir_arena = Arena::new();
78+
let mut parsing_context = ParsingContext::new(&sess);
79+
// Set remappings into HIR parsing context.
80+
for remapping in &paths.remappings {
81+
parsing_context.file_resolver.add_import_map(
82+
PathBuf::from(&remapping.name),
83+
PathBuf::from(&remapping.path),
84+
);
85+
}
86+
// Load and parse test and script contracts only (dependencies are automatically
87+
// resolved).
88+
let preprocessed_paths = sources
89+
.into_iter()
90+
.filter(|(path, source)| {
91+
is_test_or_script(path, paths) && !source.content.is_empty()
92+
})
93+
.map(|(path, _)| path.clone())
94+
.collect_vec();
95+
parsing_context.load_files(&preprocessed_paths)?;
9196

92-
let hir = &parsing_context.parse_and_lower_to_hir(&hir_arena)?;
93-
// Collect tests and scripts dependencies.
94-
let deps = PreprocessorDependencies::new(
95-
&sess,
96-
hir,
97-
&preprocessed_paths,
98-
&paths.paths_relative().sources,
99-
);
100-
// Collect data of source contracts referenced in tests and scripts.
101-
let data = collect_preprocessor_data(&sess, hir, &deps.referenced_contracts);
97+
let hir = &parsing_context.parse_and_lower_to_hir(&hir_arena)?;
98+
// Collect tests and scripts dependencies and identify mock contracts.
99+
let deps = PreprocessorDependencies::new(
100+
&sess,
101+
hir,
102+
&preprocessed_paths,
103+
&paths.paths_relative().sources,
104+
&paths.root,
105+
);
106+
// Collect data of source contracts referenced in tests and scripts.
107+
let data = collect_preprocessor_data(&sess, hir, &deps.referenced_contracts);
102108

103-
// Extend existing sources with preprocessor deploy helper sources.
104-
sources.extend(create_deploy_helpers(&data));
109+
// Extend existing sources with preprocessor deploy helper sources.
110+
sources.extend(create_deploy_helpers(&data));
105111

106-
// Generate and apply preprocessor source updates.
107-
apply_updates(sources, remove_bytecode_dependencies(hir, &deps, &data));
112+
// Generate and apply preprocessor source updates.
113+
apply_updates(sources, remove_bytecode_dependencies(hir, &deps, &data));
108114

109-
Ok(())
110-
});
115+
Ok(Some(deps.mocks))
116+
})
117+
.unwrap_or_default();
111118

112119
// Return if any diagnostics emitted during content parsing.
113120
if let Err(err) = sess.emitted_errors().unwrap() {
114121
trace!("failed preprocessing {err}");
115122
return Err(SolcError::Message(err.to_string()));
116123
}
117124

118-
Ok(input)
125+
Ok((input, mocks))
119126
}
120127
}
121128

@@ -125,18 +132,18 @@ impl Preprocessor<MultiCompiler> for TestOptimizerPreprocessor {
125132
compiler: &MultiCompiler,
126133
input: <MultiCompiler as Compiler>::Input,
127134
paths: &ProjectPathsConfig<MultiCompilerLanguage>,
128-
) -> Result<<MultiCompiler as Compiler>::Input> {
135+
) -> Result<(<MultiCompiler as Compiler>::Input, Option<HashSet<PathBuf>>)> {
129136
match input {
130137
MultiCompilerInput::Solc(input) => {
131138
if let Some(solc) = &compiler.solc {
132139
let paths = paths.clone().with_language::<SolcLanguage>();
133-
let input = self.preprocess(solc, input, &paths)?;
134-
Ok(MultiCompilerInput::Solc(input))
140+
let (input, mocks) = self.preprocess(solc, input, &paths)?;
141+
Ok((MultiCompilerInput::Solc(input), mocks))
135142
} else {
136-
Ok(MultiCompilerInput::Solc(input))
143+
Ok((MultiCompilerInput::Solc(input), None))
137144
}
138145
}
139-
MultiCompilerInput::Vyper(input) => Ok(MultiCompilerInput::Vyper(input)),
146+
MultiCompilerInput::Vyper(input) => Ok((MultiCompilerInput::Vyper(input), None)),
140147
}
141148
}
142149
}

0 commit comments

Comments
 (0)