fix(solc): flatten import aliases (#1192)

* import aliases

* fix solang alias parsing

* fix token replacement

* minor

* remove log

* remove log

* address pr comments

* rollback
This commit is contained in:
Roman Krasiuk 2022-05-04 08:33:25 +03:00 committed by GitHub
parent b34c034bc4
commit a656830790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 339 additions and 78 deletions

View File

@ -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| {

View File

@ -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<SolDataUnit<PathBuf>> {
pub fn imports(&self) -> &Vec<SolDataUnit<SolImport>> {
&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::<Vec<&PathBuf>>(),
dapp_test.data.imports.iter().map(|i| i.data().path()).collect::<Vec<&PathBuf>>(),
vec![&PathBuf::from("ds-test/test.sol"), &PathBuf::from("./Dapp.sol")]
);
assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);

View File

@ -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<SolDataUnit<String>>,
pub version: Option<SolDataUnit<String>>,
pub experimental: Option<SolDataUnit<String>>,
pub imports: Vec<SolDataUnit<PathBuf>>,
pub imports: Vec<SolDataUnit<SolImport>>,
pub version_req: Option<VersionReq>,
pub libraries: Vec<SolLibrary>,
pub contracts: Vec<SolContract>,
@ -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::<SolDataUnit<PathBuf>>::new();
let mut imports = Vec::<SolDataUnit<SolImport>>::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<FunctionDefinition>,
}
#[derive(Debug, Clone)]
pub struct SolImport {
path: PathBuf,
aliases: Vec<SolImportAlias>,
}
#[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<SolImportAlias> {
&self.aliases
}
fn set_aliases(mut self, aliases: Vec<SolImportAlias>) -> 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<T> {
loc: Location,
loc: Range<usize>,
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<T> SolDataUnit<T> {
pub fn new(data: T, loc: Location) -> Self {
pub fn new(data: T, loc: Range<usize>) -> 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<usize> {
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<Match<'_>> for Location {
fn from(src: Match) -> Self {
Location { start: src.start(), end: src.end() }
}
}
impl From<Loc> 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<usize> {
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<SolDataUnit<PathBuf>> {
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<SolDataUnit<SolImport>> {
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::<Vec<_>>();
capture_imports(content).into_iter().map(|s| s.data.path).collect::<Vec<_>>();
let expected =
utils::find_import_paths(content).map(|m| m.as_str().into()).collect::<Vec<PathBuf>>();
@ -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::<Vec<_>>();
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()),
],
]
);
}
}

View File

@ -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 <https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L100>
pub static RE_SOL_IMPORT: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"import\s+(?:(?:"(?P<p1>[^;]*)"|'(?P<p2>[^;]*)')(?:;|\s+as\s+(?P<id>[^;]*);)|.+from\s+(?:"(?P<p3>.*)"|'(?P<p4>.*)');)"#).unwrap()
Regex::new(r#"import\s+(?:(?:"(?P<p1>.*)"|'(?P<p2>.*)')(?:\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<p3>.*)"|'(?P<p4>.*)')))\s*;"#).unwrap()
});
/// A regex that matches an alias within an import statement
pub static RE_SOL_IMPORT_ALIAS: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(?:(?P<target>\w+)|\*|'|")\s+as\s+(?P<alias>\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<Regex> =
/// A regex used to remove extra lines in flatenned files
pub static RE_THREE_OR_MORE_NEWLINES: Lazy<Regex> = 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<n1>{name})\s+|is\s+(?:\w+\s*,\s*)*(?P<n2>{name})(?:\s*,\s*\w+)*|(?:(?P<ignore>(?:function|error|as)\s+|\n[^\n]*(?:"([^"\n]|\\")*|'([^'\n]|\\')*))|\W+)(?P<n3>{name})(?:\.|\(| ))"#, name = name)).unwrap()
}
/// Move a range by a specified offset
pub fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
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"`.
///

View File

@ -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::<ConfigurableArtifacts>::dapptools().unwrap();