From 1e1aba19b17e647f69864aef9cc1b907c281336b Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Thu, 17 Mar 2022 11:48:01 +0100 Subject: [PATCH] fix(solc): only modify files that are required to compile the project (#1050) --- ethers-solc/src/project_util/mock.rs | 61 +++++++++++++++++++++++++++- ethers-solc/tests/mocked.rs | 30 ++++++++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/ethers-solc/src/project_util/mock.rs b/ethers-solc/src/project_util/mock.rs index 78fb81a9..8576a520 100644 --- a/ethers-solc/src/project_util/mock.rs +++ b/ethers-solc/src/project_util/mock.rs @@ -12,7 +12,7 @@ use rand::{ }; use serde::{Deserialize, Serialize}; use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeSet, HashMap, HashSet, VecDeque}, path::{Path, PathBuf}, }; @@ -25,6 +25,13 @@ pub struct MockProjectSkeleton { pub libraries: Vec, } +impl MockProjectSkeleton { + /// Returns a list of file ids the given file id imports. + pub fn imported_nodes(&self, from: usize) -> impl Iterator + '_ { + self.files[from].imports.iter().map(|i| i.file_id()) + } +} + /// Represents a virtual project #[derive(Serialize)] pub struct MockProjectGenerator { @@ -272,11 +279,27 @@ impl MockProjectGenerator { } } + /// Returns the file for the given id + pub fn get_file(&self, id: usize) -> &MockFile { + &self.inner.files[id] + } + /// All file ids pub fn file_ids(&self) -> impl Iterator + '_ { self.inner.files.iter().map(|f| f.id) } + /// Returns an iterator over all file ids that are source files or imported by source files + /// + /// In other words, all files that are relevant in order to compile the project's source files. + pub fn used_file_ids(&self) -> impl Iterator + '_ { + let mut file_ids = BTreeSet::new(); + for file in self.internal_file_ids() { + file_ids.extend(NodesIter::new(file, &self.inner)) + } + file_ids.into_iter() + } + /// All ids of internal files pub fn internal_file_ids(&self) -> impl Iterator + '_ { self.inner.files.iter().filter(|f| !f.is_external()).map(|f| f.id) @@ -469,6 +492,15 @@ pub enum MockImport { External(usize, usize), } +impl MockImport { + pub fn file_id(&self) -> usize { + *match self { + MockImport::Internal(id) => id, + MockImport::External(_, id) => id, + } + } +} + /// Container of a mock lib #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockLib { @@ -552,6 +584,33 @@ impl Default for MockProjectSettings { } } +/// An iterator over a node and its dependencies +struct NodesIter<'a> { + /// stack of nodes + stack: VecDeque, + visited: HashSet, + skeleton: &'a MockProjectSkeleton, +} + +impl<'a> NodesIter<'a> { + fn new(start: usize, skeleton: &'a MockProjectSkeleton) -> Self { + Self { stack: VecDeque::from([start]), visited: HashSet::new(), skeleton } + } +} + +impl<'a> Iterator for NodesIter<'a> { + type Item = usize; + fn next(&mut self) -> Option { + let file = self.stack.pop_front()?; + + if self.visited.insert(file) { + // push the file's direct imports to the stack if we haven't visited it already + self.stack.extend(self.skeleton.imported_nodes(file)); + } + Some(file) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ethers-solc/tests/mocked.rs b/ethers-solc/tests/mocked.rs index 4bd717b2..3fdffb97 100644 --- a/ethers-solc/tests/mocked.rs +++ b/ethers-solc/tests/mocked.rs @@ -3,7 +3,7 @@ use ethers_solc::{ error::Result, project_util::{ - mock::{MockProjectGenerator, MockProjectSettings}, + mock::{MockProjectGenerator, MockProjectSettings, MockProjectSkeleton}, TempProject, }, }; @@ -85,7 +85,7 @@ fn can_compile_mocked_modified() { run_mock(MockProjectSettings::random(), |project, gen| { project.ensure_no_errors_recompile_unchanged()?; // modify a random file - gen.modify_file(gen.file_ids().count() / 2, project.paths(), DEFAULT_VERSION)?; + gen.modify_file(gen.used_file_ids().count() / 2, project.paths(), DEFAULT_VERSION)?; project.ensure_changed()?; project.artifacts_snapshot()?.assert_artifacts_essentials_present(); Ok(()) @@ -97,7 +97,7 @@ fn can_compile_mocked_modified_all() { run_mock(MockProjectSettings::random(), |project, gen| { project.ensure_no_errors_recompile_unchanged()?; // modify a random file - for id in gen.file_ids() { + for id in gen.used_file_ids() { gen.modify_file(id, project.paths(), DEFAULT_VERSION)?; project.ensure_changed()?; project.artifacts_snapshot()?.assert_artifacts_essentials_present(); @@ -105,3 +105,27 @@ fn can_compile_mocked_modified_all() { Ok(()) }); } + +// a test useful to manually debug a serialized skeleton +#[test] +fn can_compile_skeleton() { + let mut project = TempProject::dapptools().unwrap(); + let s = r#"{"files":[{"id":0,"name":"SourceFile0","imports":[{"External":[0,1]},{"External":[3,4]}],"lib_id":null,"emit_artifacts":true},{"id":1,"name":"SourceFile1","imports":[],"lib_id":0,"emit_artifacts":true},{"id":2,"name":"SourceFile2","imports":[],"lib_id":1,"emit_artifacts":true},{"id":3,"name":"SourceFile3","imports":[],"lib_id":2,"emit_artifacts":true},{"id":4,"name":"SourceFile4","imports":[],"lib_id":3,"emit_artifacts":true}],"libraries":[{"name":"Lib0","id":0,"offset":1,"num_files":1},{"name":"Lib1","id":1,"offset":2,"num_files":1},{"name":"Lib2","id":2,"offset":3,"num_files":1},{"name":"Lib3","id":3,"offset":4,"num_files":1}]}"#; + let gen: MockProjectGenerator = serde_json::from_str::(s).unwrap().into(); + let remappings = gen.remappings_at(project.root()); + project.paths_mut().remappings.extend(remappings); + project.mock(&gen, DEFAULT_VERSION).unwrap(); + + // mattsse: helper to show what's being generated + // gen.write_to(ðers_solc::ProjectPathsConfig::dapptools("./skeleton").unwrap(), + // DEFAULT_VERSION).unwrap(); + + let compiled = project.compile().unwrap(); + assert!(!compiled.has_compiler_errors()); + assert!(!compiled.is_unchanged()); + for id in gen.used_file_ids() { + gen.modify_file(id, project.paths(), DEFAULT_VERSION).unwrap(); + project.ensure_changed().unwrap(); + project.artifacts_snapshot().unwrap().assert_artifacts_essentials_present(); + } +}