diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 261b5b38..90307818 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -3,7 +3,7 @@ use crate::{ cache::SOLIDITY_FILES_CACHE_FILENAME, error::{Result, SolcError, SolcIoError}, remappings::Remapping, - resolver::Graph, + resolver::{Graph, SolImportAlias}, utils, Source, Sources, }; @@ -281,43 +281,69 @@ impl ProjectPathsConfig { let target_node = graph.node(*target_index); let mut imports = target_node.imports().clone(); - imports.sort_by_key(|x| x.loc().0); + imports.sort_by_key(|x| x.loc().start); - let mut content = target_node.content().as_bytes().to_vec(); + let mut content = target_node.content().to_owned(); + + for alias in imports.iter().flat_map(|i| i.data().aliases()) { + let (alias, target) = match alias { + SolImportAlias::Contract(alias, target) => (alias.clone(), target.clone()), + _ => continue, + }; + let name_regex = utils::create_contract_or_lib_name_regex(&alias); + let target_len = target.len() as isize; + let mut replace_offset = 0; + for cap in name_regex.captures_iter(&content.clone()) { + if cap.name("ignore").is_some() { + continue + } + if let Some(name_match) = + vec!["n1", "n2", "n3"].iter().find_map(|name| cap.name(name)) + { + let name_match_range = + utils::range_by_offset(&name_match.range(), replace_offset); + replace_offset += target_len - (name_match_range.len() as isize); + content.replace_range(name_match_range, &target); + } + } + } + + let mut content = content.as_bytes().to_vec(); let mut offset = 0_isize; if strip_license { if let Some(license) = target_node.license() { - let (start, end) = license.loc_by_offset(offset); - content.splice(start..end, std::iter::empty()); - offset -= (end - start) as isize; + let license_range = license.loc_by_offset(offset); + offset -= license_range.len() as isize; + content.splice(license_range, std::iter::empty()); } } if strip_version_pragma { if let Some(version) = target_node.version() { - let (start, end) = version.loc_by_offset(offset); - content.splice(start..end, std::iter::empty()); - offset -= (end - start) as isize; + let version_range = version.loc_by_offset(offset); + offset -= version_range.len() as isize; + content.splice(version_range, std::iter::empty()); } } if strip_experimental_pragma { if let Some(experiment) = target_node.experimental() { - let (start, end) = experiment.loc_by_offset(offset); - content.splice(start..end, std::iter::empty()); - offset -= (end - start) as isize; + let experimental_pragma_range = experiment.loc_by_offset(offset); + offset -= experimental_pragma_range.len() as isize; + content.splice(experimental_pragma_range, std::iter::empty()); } } for import in imports.iter() { - let import_path = self.resolve_import(target_dir, import.data())?; + let import_path = self.resolve_import(target_dir, import.data().path())?; let s = self.flatten_node(&import_path, graph, imported, true, true, true)?; + let import_content = s.as_bytes(); let import_content_len = import_content.len() as isize; - let (start, end) = import.loc_by_offset(offset); - content.splice(start..end, import_content.iter().copied()); - offset += import_content_len - ((end - start) 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()); } let result = String::from_utf8(content).map_err(|err| { diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs index b4c6ed7f..9183fd0b 100644 --- a/ethers-solc/src/resolver/mod.rs +++ b/ethers-solc/src/resolver/mod.rs @@ -52,7 +52,7 @@ use std::{ path::{Path, PathBuf}, }; -use parse::{SolData, SolDataUnit}; +use parse::{SolData, SolDataUnit, SolImport}; use rayon::prelude::*; use semver::VersionReq; @@ -62,6 +62,7 @@ use crate::{error::Result, utils, ProjectPathsConfig, SolcError, Source, Sources mod parse; mod tree; +pub use parse::SolImportAlias; pub use tree::{print, Charset, TreeOptions}; /// The underlying edges of the graph which only contains the raw relationship data. @@ -339,13 +340,14 @@ impl Graph { }; for import in node.data.imports.iter() { - match paths.resolve_import(cwd, import.data()) { + let import_path = import.data().path(); + match paths.resolve_import(cwd, import_path) { Ok(import) => { add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?; } Err(err) => { - if unresolved_paths.insert(import.data().to_path_buf()) { - crate::report::unresolved_import(import.data(), &paths.remappings); + if unresolved_paths.insert(import_path.to_path_buf()) { + crate::report::unresolved_import(import_path, &paths.remappings); } tracing::trace!( "failed to resolve import component \"{:?}\" for {:?}", @@ -772,7 +774,7 @@ impl Node { &self.source.content } - pub fn imports(&self) -> &Vec> { + pub fn imports(&self) -> &Vec> { &self.data.imports } @@ -854,7 +856,7 @@ mod tests { let dapp_test = graph.node(1); assert_eq!(dapp_test.path, paths.sources.join("Dapp.t.sol")); assert_eq!( - dapp_test.data.imports.iter().map(|i| i.data()).collect::>(), + dapp_test.data.imports.iter().map(|i| i.data().path()).collect::>(), vec![&PathBuf::from("ds-test/test.sol"), &PathBuf::from("./Dapp.sol")] ); assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]); diff --git a/ethers-solc/src/resolver/parse.rs b/ethers-solc/src/resolver/parse.rs index 028856f9..4bcd8807 100644 --- a/ethers-solc/src/resolver/parse.rs +++ b/ethers-solc/src/resolver/parse.rs @@ -1,11 +1,13 @@ use crate::{utils, Solc}; -use regex::Match; use semver::VersionReq; use solang_parser::pt::{ ContractPart, ContractTy, FunctionAttribute, FunctionDefinition, Import, Loc, SourceUnitPart, Visibility, }; -use std::path::{Path, PathBuf}; +use std::{ + ops::Range, + path::{Path, PathBuf}, +}; /// Represents various information about a solidity file parsed via [solang_parser] #[derive(Debug)] @@ -14,7 +16,7 @@ pub struct SolData { pub license: Option>, pub version: Option>, pub experimental: Option>, - pub imports: Vec>, + pub imports: Vec>, pub version_req: Option, pub libraries: Vec, pub contracts: Vec, @@ -39,7 +41,7 @@ impl SolData { pub fn parse(content: &str, file: &Path) -> Self { let mut version = None; let mut experimental = None; - let mut imports = Vec::>::new(); + let mut imports = Vec::>::new(); let mut libraries = Vec::new(); let mut contracts = Vec::new(); @@ -50,22 +52,30 @@ impl SolData { SourceUnitPart::PragmaDirective(loc, _, pragma, value) => { if pragma.name == "solidity" { // we're only interested in the solidity version pragma - version = Some(SolDataUnit::new(value.string.clone(), loc.into())); + version = Some(SolDataUnit::from_loc(value.string.clone(), loc)); } if pragma.name == "experimental" { experimental = - Some(SolDataUnit::new(value.string.clone(), loc.into())); + Some(SolDataUnit::from_loc(value.string.clone(), loc)); } } SourceUnitPart::ImportDirective(_, import) => { - let (import, loc) = match import { - Import::Plain(s, l) => (s, l), - Import::GlobalSymbol(s, _, l) => (s, l), - Import::Rename(s, _, l) => (s, l), + let (import, ids, loc) = match import { + Import::Plain(s, l) => (s, vec![], l), + Import::GlobalSymbol(s, i, l) => (s, vec![(i, None)], l), + Import::Rename(s, i, l) => (s, i, l), }; - imports - .push(SolDataUnit::new(PathBuf::from(import.string), loc.into())); + let sol_import = SolImport::new(PathBuf::from(import.string)) + .set_aliases( + ids.into_iter() + .map(|(id, alias)| match alias { + Some(al) => SolImportAlias::Contract(al.name, id.name), + None => SolImportAlias::File(id.name), + }) + .collect(), + ); + imports.push(SolDataUnit::from_loc(sol_import, loc)); } SourceUnitPart::ContractDefinition(def) => { let functions = def @@ -100,16 +110,14 @@ impl SolData { version = capture_outer_and_inner(content, &utils::RE_SOL_PRAGMA_VERSION, &["version"]) .first() - .map(|(cap, name)| { - SolDataUnit::new(name.as_str().to_owned(), cap.to_owned().into()) - }); + .map(|(cap, name)| SolDataUnit::new(name.as_str().to_owned(), cap.range())); imports = capture_imports(content); } }; let license = content.lines().next().and_then(|line| { capture_outer_and_inner(line, &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, &["license"]) .first() - .map(|(cap, l)| SolDataUnit::new(l.as_str().to_owned(), cap.to_owned().into())) + .map(|(cap, l)| SolDataUnit::new(l.as_str().to_owned(), cap.range())) }); let version_req = version.as_ref().and_then(|v| Solc::version_req(v.data()).ok()); @@ -130,6 +138,37 @@ pub struct SolContract { pub functions: Vec, } +#[derive(Debug, Clone)] +pub struct SolImport { + path: PathBuf, + aliases: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum SolImportAlias { + File(String), + Contract(String, String), +} + +impl SolImport { + pub fn new(path: PathBuf) -> Self { + Self { path, aliases: vec![] } + } + + pub fn path(&self) -> &PathBuf { + &self.path + } + + pub fn aliases(&self) -> &Vec { + &self.aliases + } + + fn set_aliases(mut self, aliases: Vec) -> Self { + self.aliases = aliases; + self + } +} + /// Minimal representation of a contract inside a solidity file #[derive(Debug)] pub struct SolLibrary { @@ -164,57 +203,41 @@ impl SolLibrary { /// Represents an item in a solidity file with its location in the file #[derive(Debug, Clone)] pub struct SolDataUnit { - loc: Location, + loc: Range, data: T, } -/// Location in a text file buffer -#[derive(Debug, Clone)] -pub struct Location { - pub start: usize, - pub end: usize, -} - /// Solidity Data Unit decorated with its location within the file impl SolDataUnit { - pub fn new(data: T, loc: Location) -> Self { + pub fn new(data: T, loc: Range) -> Self { Self { data, loc } } + pub fn from_loc(data: T, loc: Loc) -> Self { + Self { + data, + loc: match loc { + Loc::File(_, start, end) => Range { start, end: end + 1 }, + _ => Range { start: 0, end: 0 }, + }, + } + } + /// Returns the underlying data for the unit pub fn data(&self) -> &T { &self.data } /// Returns the location of the given data unit - pub fn loc(&self) -> (usize, usize) { - (self.loc.start, self.loc.end) + pub fn loc(&self) -> Range { + self.loc.clone() } /// Returns the location of the given data unit adjusted by an offset. /// Used to determine new position of the unit within the file after /// content manipulation. - pub fn loc_by_offset(&self, offset: isize) -> (usize, usize) { - ( - offset.saturating_add(self.loc.start as isize) as usize, - // make the end location exclusive - offset.saturating_add(self.loc.end as isize + 1) as usize, - ) - } -} - -impl From> for Location { - fn from(src: Match) -> Self { - Location { start: src.start(), end: src.end() } - } -} - -impl From for Location { - fn from(src: Loc) -> Self { - match src { - Loc::File(_, start, end) => Location { start, end }, - _ => Location { start: 0, end: 0 }, - } + pub fn loc_by_offset(&self, offset: isize) -> Range { + utils::range_by_offset(&self.loc, offset) } } @@ -238,12 +261,31 @@ fn capture_outer_and_inner<'a>( }) .collect() } - -pub fn capture_imports(content: &str) -> Vec> { - capture_outer_and_inner(content, &utils::RE_SOL_IMPORT, &["p1", "p2", "p3", "p4"]) - .iter() - .map(|(cap, m)| SolDataUnit::new(PathBuf::from(m.as_str()), cap.to_owned().into())) - .collect() +/// Capture the import statement information together with aliases +pub fn capture_imports(content: &str) -> Vec> { + let mut imports = vec![]; + for cap in utils::RE_SOL_IMPORT.captures_iter(content) { + if let Some(name_match) = + vec!["p1", "p2", "p3", "p4"].iter().find_map(|name| cap.name(name)) + { + let statement_match = cap.get(0).unwrap(); + let mut aliases = vec![]; + for alias_cap in utils::RE_SOL_IMPORT_ALIAS.captures_iter(statement_match.as_str()) { + if let Some(alias) = alias_cap.name("alias") { + let alias = alias.as_str().to_owned(); + let import_alias = match alias_cap.name("target") { + Some(target) => SolImportAlias::Contract(alias, target.as_str().to_owned()), + None => SolImportAlias::File(alias), + }; + aliases.push(import_alias); + } + } + let sol_import = + SolImport::new(PathBuf::from(name_match.as_str())).set_aliases(aliases); + imports.push(SolDataUnit::new(sol_import, statement_match.range())); + } + } + imports } #[cfg(test)] @@ -259,7 +301,7 @@ import {DsTest} from "ds-test/test.sol"; "#; let captured_imports = - capture_imports(content).into_iter().map(|s| s.data).collect::>(); + capture_imports(content).into_iter().map(|s| s.data.path).collect::>(); let expected = utils::find_import_paths(content).map(|m| m.as_str().into()).collect::>(); @@ -275,4 +317,29 @@ import {DsTest} from "ds-test/test.sol"; ] ); } + + #[test] + fn cap_capture_aliases() { + let content = r#" +import * as T from "./Test.sol"; +import { DsTest as Test } from "ds-test/test.sol"; +import "ds-test/test.sol" as Test; +import { FloatMath as Math, Math as FloatMath } from "./Math.sol"; +"#; + + let caputred_imports = + capture_imports(content).into_iter().map(|s| s.data.aliases).collect::>(); + assert_eq!( + caputred_imports, + vec![ + vec![SolImportAlias::File("T".into())], + vec![SolImportAlias::Contract("Test".into(), "DsTest".into())], + vec![SolImportAlias::File("Test".into())], + vec![ + SolImportAlias::Contract("Math".into(), "FloatMath".into()), + SolImportAlias::Contract("FloatMath".into(), "Math".into()), + ], + ] + ); + } } diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs index bd354228..48e2cb59 100644 --- a/ethers-solc/src/utils.rs +++ b/ethers-solc/src/utils.rs @@ -2,6 +2,7 @@ use std::{ collections::HashSet, + ops::Range, path::{Component, Path, PathBuf}, }; @@ -17,9 +18,13 @@ use walkdir::WalkDir; /// statement with the named groups "path", "id". // Adapted from pub static RE_SOL_IMPORT: Lazy = Lazy::new(|| { - Regex::new(r#"import\s+(?:(?:"(?P[^;]*)"|'(?P[^;]*)')(?:;|\s+as\s+(?P[^;]*);)|.+from\s+(?:"(?P.*)"|'(?P.*)');)"#).unwrap() + Regex::new(r#"import\s+(?:(?:"(?P.*)"|'(?P.*)')(?:\s+as\s+\w+)?|(?:(?:\w+(?:\s+as\s+\w+)?|\*\s+as\s+\w+|\{\s*(?:\w+(?:\s+as\s+\w+)?(?:\s*,\s*)?)+\s*\})\s+from\s+(?:"(?P.*)"|'(?P.*)')))\s*;"#).unwrap() }); +/// A regex that matches an alias within an import statement +pub static RE_SOL_IMPORT_ALIAS: Lazy = + Lazy::new(|| Regex::new(r#"(?:(?P\w+)|\*|'|")\s+as\s+(?P\w+)"#).unwrap()); + /// A regex that matches the version part of a solidity pragma /// as follows: `pragma solidity ^0.5.2;` => `^0.5.2` /// statement with the named group "version". @@ -35,6 +40,19 @@ pub static RE_SOL_SDPX_LICENSE_IDENTIFIER: Lazy = /// A regex used to remove extra lines in flatenned files pub static RE_THREE_OR_MORE_NEWLINES: Lazy = Lazy::new(|| Regex::new("\n{3,}").unwrap()); +/// Create a regex that matches any library or contract name inside a file +pub fn create_contract_or_lib_name_regex(name: &str) -> Regex { + Regex::new(&format!(r#"(?:using\s+(?P{name})\s+|is\s+(?:\w+\s*,\s*)*(?P{name})(?:\s*,\s*\w+)*|(?:(?P(?:function|error|as)\s+|\n[^\n]*(?:"([^"\n]|\\")*|'([^'\n]|\\')*))|\W+)(?P{name})(?:\.|\(| ))"#, name = name)).unwrap() +} + +/// Move a range by a specified offset +pub fn range_by_offset(range: &Range, offset: isize) -> Range { + Range { + start: offset.saturating_add(range.start as isize) as usize, + end: offset.saturating_add(range.end as isize) as usize, + } +} + /// Returns all path parts from any solidity import statement in a string, /// `import "./contracts/Contract.sol";` -> `"./contracts/Contract.sol"`. /// diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs index e10674c0..40f9aaa4 100644 --- a/ethers-solc/tests/project.rs +++ b/ethers-solc/tests/project.rs @@ -721,6 +721,154 @@ contract A { } ); } +#[test] +fn can_flatten_with_alias() { + let project = TempProject::dapptools().unwrap(); + + let f = project + .add_source( + "Contract", + r#"pragma solidity ^0.8.10; +import { ParentContract as Parent } from "./Parent.sol"; +import { AnotherParentContract as AnotherParent } from "./AnotherParent.sol"; +import { PeerContract as Peer } from "./Peer.sol"; +import { MathLibrary as Math } from "./Math.sol"; +import * as Lib from "./SomeLib.sol"; + +contract Contract is Parent, + AnotherParent { + using Math for uint256; + + string public usingString = "using Math for uint256;"; + string public inheritanceString = "\"Contract is Parent {\""; + string public castString = 'Peer(smth) '; + string public methodString = '\' Math.max()'; + + Peer public peer; + + error Peer(); + + constructor(address _peer) { + peer = Peer(_peer); + } + + function Math(uint256 value) external pure returns (uint256) { + return Math.minusOne(Math.max() - value.diffMax()); + } +} +"#, + ) + .unwrap(); + + project + .add_source( + "Parent", + r#"pragma solidity ^0.8.10; +contract ParentContract { } +"#, + ) + .unwrap(); + + project + .add_source( + "AnotherParent", + r#"pragma solidity ^0.8.10; +contract AnotherParentContract { } +"#, + ) + .unwrap(); + + project + .add_source( + "Peer", + r#"pragma solidity ^0.8.10; +contract PeerContract { } +"#, + ) + .unwrap(); + + project + .add_source( + "Math", + r#"pragma solidity ^0.8.10; +library MathLibrary { + function minusOne(uint256 val) internal returns (uint256) { + return val - 1; + } + + function max() internal returns (uint256) { + return type(uint256).max; + } + + function diffMax(uint256 value) internal returns (uint256) { + return type(uint256).max - value; + } +} +"#, + ) + .unwrap(); + + project + .add_source( + "SomeLib", + r#"pragma solidity ^0.8.10; +library SomeLib { } +"#, + ) + .unwrap(); + + let result = project.flatten(&f).unwrap(); + assert_eq!( + result, + r#"pragma solidity ^0.8.10; + +contract ParentContract { } + +contract AnotherParentContract { } + +contract PeerContract { } + +library MathLibrary { + function minusOne(uint256 val) internal returns (uint256) { + return val - 1; + } + + function max() internal returns (uint256) { + return type(uint256).max; + } + + function diffMax(uint256 value) internal returns (uint256) { + return type(uint256).max - value; + } +} + +library SomeLib { } + +contract Contract is ParentContract, + AnotherParentContract { + using MathLibrary for uint256; + + string public usingString = "using Math for uint256;"; + string public inheritanceString = "\"Contract is Parent {\""; + string public castString = 'Peer(smth) '; + string public methodString = '\' Math.max()'; + + PeerContract public peer; + + error Peer(); + + constructor(address _peer) { + peer = PeerContract(_peer); + } + + function Math(uint256 value) external pure returns (uint256) { + return MathLibrary.minusOne(MathLibrary.max() - value.diffMax()); + } +} +"# + ); +} + #[test] fn can_detect_type_error() { let project = TempProject::::dapptools().unwrap();