Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(solc): experimental recording in flatten #2645

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 85 additions & 14 deletions ethers-solc/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,17 +408,45 @@ impl ProjectPathsConfig {

let sources = Source::read_all_files(input_files)?;
let graph = Graph::resolve_sources(self, sources)?;
self.flatten_node(target, &graph, &mut Default::default(), false, false, false).map(|x| {
format!("{}\n", utils::RE_THREE_OR_MORE_NEWLINES.replace_all(&x, "\n\n").trim())
})
let mut ctx = FlattenContext::default();
let mut content =
self.flatten_node(target, &graph, &mut ctx, false, false, false).map(|x| {
format!("{}\n", utils::RE_THREE_OR_MORE_NEWLINES.replace_all(&x, "\n\n").trim())
})?;

// need to do some cleanup here
// Note: this is a bit horrible but it's an easy hack to inject experimentals after fully
// traversing the path since we only keep the experimentals for the target node
let index = graph.files().get(target).expect("exists");
let target_node = graph.node(*index);

if target_node.experimental().is_none() && !ctx.unique_experimentals.is_empty() {
// the tree contains experimental pragmas but the target file does not have one
// append it to the version pragma
if let Some(version) = target_node.version() {
let version_pragma = format!("pragma solidity {};", version.data());
let s = format!("{}\n{}", version_pragma, ctx.experimentals());
content = content.replacen(version_pragma.as_str(), s.as_str(), 1);
} else {
content = format!("{}\n{}", ctx.experimentals(), content);
}
} else if target_node.experimental().is_some() && ctx.unique_experimentals.len() > 1 {
// the target file has an experimental pragma but the tree contains more than one
let experimental = target_node.experimental().as_ref().unwrap();
let experimental_pragma = format!("pragma experimental {};", experimental.data());
content =
content.replacen(experimental_pragma.as_str(), ctx.experimentals().as_str(), 1);
}

Ok(content)
}

/// Flattens a single node from the dependency graph
fn flatten_node(
&self,
target: &Path,
graph: &Graph,
imported: &mut HashSet<usize>,
ctx: &mut FlattenContext,
strip_version_pragma: bool,
strip_experimental_pragma: bool,
strip_license: bool,
Expand All @@ -430,11 +458,11 @@ impl ProjectPathsConfig {
SolcError::msg(format!("cannot resolve file at {:?}", target.display()))
})?;

if imported.contains(target_index) {
if ctx.imported.contains(target_index) {
// short circuit nodes that were already imported, if both A.sol and B.sol import C.sol
return Ok(String::new())
}
imported.insert(*target_index);
ctx.imported.insert(*target_index);

let target_node = graph.node(*target_index);

Expand Down Expand Up @@ -466,42 +494,53 @@ impl ProjectPathsConfig {
}

let mut content = content.as_bytes().to_vec();
let mut offset = 0_isize;
let mut offset = 0isize;

let mut statements = [
(target_node.license(), strip_license),
(target_node.version(), strip_version_pragma),
(target_node.experimental(), strip_experimental_pragma),
]
.iter()
.filter_map(|(data, condition)| if *condition { data.to_owned().as_ref() } else { None })
.filter_map(|(data, strip)| if *strip { data.to_owned().as_ref() } else { None })
.collect::<Vec<_>>();
statements.sort_by_key(|x| x.loc().start);

let (mut imports, mut statements) =
(imports.iter().peekable(), statements.iter().peekable());
while imports.peek().is_some() || statements.peek().is_some() {
let (next_import_start, next_statement_start) = (
imports.peek().map_or(usize::max_value(), |x| x.loc().start),
statements.peek().map_or(usize::max_value(), |x| x.loc().start),
imports.peek().map_or(usize::MAX, |x| x.loc().start),
statements.peek().map_or(usize::MAX, |x| x.loc().start),
);
if next_statement_start < next_import_start {
let repl_range = statements.next().unwrap().loc_by_offset(offset);
// remove the statements if they are to be stripped
let repl_range = statements.next().expect("has next; qed").loc_by_offset(offset);
offset -= repl_range.len() as isize;
content.splice(repl_range, std::iter::empty());
} else {
let import = imports.next().unwrap();
// find the next node to import
let import = imports.next().expect("has next; qed");
let import_path = self.resolve_import(target_dir, import.data().path())?;
let s = self.flatten_node(&import_path, graph, imported, true, true, true)?;
let flattened_import =
self.flatten_node(&import_path, graph, ctx, true, true, true)?;

let import_content = s.as_bytes();
let import_content = flattened_import.as_bytes();
let import_content_len = import_content.len() as isize;
let import_range = import.loc_by_offset(offset);
offset += import_content_len - (import_range.len() as isize);
content.splice(import_range, import_content.iter().copied());
}
}

// record global statements
if let Some(experimental) = target_node.experimental() {
ctx.insert_experimental(experimental.data().trim());
}
if let Some(license) = target_node.license() {
ctx.insert_license(license.data().trim());
}

let result = String::from_utf8(content).map_err(|err| {
SolcError::msg(format!("failed to convert extended bytes to string: {err}"))
})?;
Expand Down Expand Up @@ -921,6 +960,38 @@ impl<T: Into<PathBuf>> From<Vec<T>> for AllowedLibPaths {
}
}

#[derive(Default)]
struct FlattenContext {
imported: HashSet<usize>,
unique_experimentals: Vec<String>,
unique_licenses: Vec<String>,
}

impl FlattenContext {
/// Returns all experimentals that are used in the tree
fn experimentals(&self) -> String {
self.unique_experimentals
.iter()
.map(|exp| format!("pragma experimental {exp};"))
.collect::<Vec<_>>()
.join("\n")
}

fn insert_experimental(&mut self, experimental: &str) {
if self.unique_experimentals.iter().any(|s| s == experimental) {
return
}
self.unique_experimentals.push(experimental.to_string())
}

fn insert_license(&mut self, license: &str) {
if self.unique_licenses.iter().any(|s| s == license) {
return
}
self.unique_licenses.push(license.to_string())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
66 changes: 66 additions & 0 deletions ethers-solc/tests/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2605,3 +2605,69 @@ contract ContractTest {
compiled.assert_success();
assert!(compiled.find_first("Contract").is_some());
}

#[test]
fn can_flatten_with_experimental() {
let project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
let target = project
.add_source(
"Foo",
r#"pragma solidity ^0.8.10;
import "./Bar.sol";
contract Foo {}"#,
)
.unwrap();

project
.add_source(
"Bar.sol",
r"pragma solidity ^0.8.10;
pragma experimental ABIEncoderV2;
contract Bar {}",
)
.unwrap();

let content = project.flatten(&target).unwrap();

let expected = r#"pragma solidity ^0.8.10;
pragma experimental ABIEncoderV2;

contract Bar {}
contract Foo {}
"#;

assert_eq!(content, expected);
}
#[test]
fn can_flatten_with_two_experimental() {
let project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
let target = project
.add_source(
"Foo",
r#"pragma solidity ^0.8.10;
pragma experimental ABIEncoderV2;
import "./Bar.sol";
contract Foo {}"#,
)
.unwrap();

project
.add_source(
"Bar.sol",
r"pragma solidity ^0.8.10;
pragma experimental ABIEncoderV2;
contract Bar {}",
)
.unwrap();

let content = project.flatten(&target).unwrap();

let expected = r#"pragma solidity ^0.8.10;
pragma experimental ABIEncoderV2;

contract Bar {}
contract Foo {}
"#;

assert_eq!(content, expected);
}
Loading