Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat(solc): flatten #774

Merged
merged 24 commits into from
Jan 17, 2022
Merged
Show file tree
Hide file tree
Changes from 14 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@

### Unreleased

- Add ability to flatten file imports
[#774](https://github.com/gakonst/ethers-rs/pull/774)
- Add dependency graph and resolve all imported libraryfiles
[#750](https://github.com/gakonst/ethers-rs/pull/750)
- `Remapping::find_many` does not return a `Result` anymore
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ethers-solc/src/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ impl Source {

/// Returns all import statements of the file
pub fn parse_imports(&self) -> Vec<&str> {
utils::find_import_paths(self.as_ref())
utils::find_import_paths(self.as_ref()).map(|m| m.as_str()).collect()
}
}

Expand Down
4 changes: 2 additions & 2 deletions ethers-solc/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,10 @@ impl SolFilesCacheBuilder {
.map_err(|err| SolcError::solc(err.to_string()))?
.as_millis() as u64;
let imports =
utils::find_import_paths(source.as_ref()).into_iter().map(str::to_string).collect();
utils::find_import_paths(source.as_ref()).map(|m| m.as_str().to_owned()).collect();

let version_pragmas = utils::find_version_pragma(source.as_ref())
.map(|v| vec![v.to_string()])
.map(|v| vec![v.as_str().to_string()])
.unwrap_or_default();

let entry = CacheEntry {
Expand Down
2 changes: 1 addition & 1 deletion ethers-solc/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ impl Solc {
pub fn source_version_req(source: &Source) -> Result<VersionReq> {
let version =
utils::find_version_pragma(&source.content).ok_or(SolcError::PragmaNotFound)?;
Self::version_req(version)
Self::version_req(version.as_str())
}

/// Returns the corresponding SemVer version requirement for the solidity version
Expand Down
59 changes: 57 additions & 2 deletions ethers-solc/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ use crate::{
error::{Result, SolcError, SolcIoError},
hh::HardhatArtifact,
remappings::Remapping,
resolver::Graph,
utils, CompilerOutput, Source, Sources,
};
use ethers_core::{abi::Abi, types::Bytes};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
collections::BTreeMap,
convert::TryFrom,
fmt,
fmt::Formatter,
fmt::{self, Formatter},
fs, io,
path::{Component, Path, PathBuf},
};
Expand Down Expand Up @@ -171,6 +171,61 @@ impl ProjectPathsConfig {
pub fn find_libs(root: impl AsRef<Path>) -> Vec<PathBuf> {
vec![utils::find_fave_or_alt_path(root, "lib", "node_modules")]
}

/// Flattens all file imports into a single string
pub fn flatten(&self, target: &Path) -> Result<String> {
tracing::trace!("flattening file");
let graph = Graph::resolve(self)?;
self.flatten_node(target, &graph, false)
}

/// Flattens a single node from the dependency graph
fn flatten_node(
&self,
target: &Path,
graph: &Graph,
omit_version_pragma: bool,
) -> Result<String> {
let target_dir = target.parent().ok_or_else(|| {
SolcError::msg(format!("failed to get parent directory for \"{:?}\"", target.display()))
})?;
let target_index = graph.files().get(target).ok_or_else(|| {
SolcError::msg(format!("cannot resolve file at \"{:?}\"", target.display()))
})?;
let target_node = graph.node(*target_index);

let mut imports = target_node.imports().clone();
imports.sort_by(|a, b| a.loc().0.cmp(&b.loc().0));
rkrasiuk marked this conversation as resolved.
Show resolved Hide resolved

let mut content = target_node.content().bytes().collect::<Vec<_>>();
rkrasiuk marked this conversation as resolved.
Show resolved Hide resolved
let mut offset = 0_isize;

if omit_version_pragma {
if let Some(version) = target_node.version() {
let (start, end) = version.loc();
content.splice(start..end, std::iter::empty());
offset -= (end - start) as isize;
}
}

for import in imports.iter() {
let import_path = self.resolve_import(target_dir, import.path())?;
let import_content = self.flatten_node(&import_path, graph, true)?;
let import_content = import_content.trim().as_bytes().to_owned();
let import_content_len = import_content.len() as isize;
let (start, end) =
import.loc_by_offset(offset).expect("failed to determine import location");
rkrasiuk marked this conversation as resolved.
Show resolved Hide resolved
content.splice(start..end, import_content);
let import_offset = import_content_len - ((end - start) as isize);
offset += import_offset;
}

let result = String::from_utf8(content).map_err(|err| {
SolcError::msg(format!("failed to convert extended bytes to string: {}", err))
})?;

Ok(result)
}
}

impl fmt::Display for ProjectPathsConfig {
Expand Down
13 changes: 11 additions & 2 deletions ethers-solc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ use crate::{
};
use error::Result;
use std::{
borrow::Cow, collections::BTreeMap, convert::TryInto, fmt, fs, marker::PhantomData,
path::PathBuf,
borrow::Cow,
collections::BTreeMap,
convert::TryInto,
fmt, fs,
marker::PhantomData,
path::{Path, PathBuf},
};

/// Utilities for creating, mocking and testing of (temporary) projects
Expand Down Expand Up @@ -507,6 +511,11 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
}
Ok(())
}

rkrasiuk marked this conversation as resolved.
Show resolved Hide resolved
/// Flattens the target file into a single string suitable for verification
pub fn flatten(&self, target: &Path) -> Result<String> {
self.paths.flatten(target)
}
}

enum PreprocessedJob<T: ArtifactOutput> {
Expand Down
4 changes: 4 additions & 0 deletions ethers-solc/src/project_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ impl<T: ArtifactOutput> TempProject<T> {
self.project().compile()
}

pub fn flatten(&self, target: &Path) -> Result<String> {
self.project().flatten(target)
}

pub fn project_mut(&mut self) -> &mut Project<T> {
&mut self.inner
}
Expand Down
122 changes: 95 additions & 27 deletions ethers-solc/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@

use std::{
collections::{HashMap, VecDeque},
convert::TryInto,
path::{Path, PathBuf},
};

use rayon::prelude::*;
use regex::Match;
use semver::VersionReq;
use solang_parser::pt::{Import, SourceUnitPart};
use solang_parser::pt::{Import, Loc, SourceUnitPart};

use crate::{error::Result, utils, ProjectPathsConfig, Solc, Source, Sources};

Expand Down Expand Up @@ -138,14 +140,12 @@ impl Graph {
};

for import in node.data.imports.iter() {
match paths.resolve_import(cwd, import) {
match paths.resolve_import(cwd, import.path()) {
Ok(import) => {
add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
}
Err(err) => {
tracing::trace!("{}", err)
}
}
Err(err) => tracing::trace!("failed to resolve import component \"{:?}\"", err),
};
}
nodes.push(node);
edges.push(resolved_imports);
Expand Down Expand Up @@ -397,12 +397,80 @@ pub struct Node {
data: SolData,
}

impl Node {
pub fn content(&self) -> &str {
&self.source.content
}

pub fn imports(&self) -> &Vec<SolImport> {
&self.data.imports
}

pub fn version(&self) -> &Option<SolVersionPragma> {
&self.data.version
}
}

#[derive(Debug, Clone)]
#[allow(unused)]
struct SolData {
version: Option<String>,
version: Option<SolVersionPragma>,
version_req: Option<VersionReq>,
imports: Vec<PathBuf>,
imports: Vec<SolImport>,
}

#[derive(Debug, Clone)]
pub struct SolVersionPragma {
pub version: String,
loc: Location,
}

#[derive(Debug, Clone)]
pub struct SolImport {
path: PathBuf,
loc: Location,
}

#[derive(Debug, Clone)]
pub struct Location {
pub start: usize,
pub end: usize,
}

impl SolVersionPragma {
pub fn loc(&self) -> (usize, usize) {
(self.loc.start, self.loc.end)
}
}

impl SolImport {
pub fn path(&self) -> &PathBuf {
&self.path
}

pub fn loc(&self) -> (usize, usize) {
(self.loc.start, self.loc.end)
}

pub fn loc_by_offset(
&self,
offset: isize,
) -> std::result::Result<(usize, usize), std::num::TryFromIntError> {
let (start, end) = (self.loc.start as isize + offset, self.loc.end as isize + offset);
Ok((start.try_into()?, end.try_into()?))
}
}

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 {
Location { start: src.1, end: src.2 }
}
}

fn read_node(file: impl AsRef<Path>) -> Result<Node> {
Expand All @@ -418,24 +486,28 @@ fn read_node(file: impl AsRef<Path>) -> Result<Node> {
/// parsing fails, we'll fall back to extract that info via regex
fn parse_data(content: &str) -> SolData {
let mut version = None;
let mut imports = Vec::new();
let mut imports = Vec::<SolImport>::new();
match solang_parser::parse(content, 0) {
Ok(units) => {
for unit in units.0 {
match unit {
SourceUnitPart::PragmaDirective(_, pragma, value) => {
SourceUnitPart::PragmaDirective(loc, _, pragma, value) => {
if pragma.name == "solidity" {
// we're only interested in the solidity version pragma
version = Some(value.string);
version =
Some(SolVersionPragma { version: value.string, loc: loc.into() });
}
}
SourceUnitPart::ImportDirective(_, import) => {
let import = match import {
Import::Plain(s, _) => s,
Import::GlobalSymbol(s, _, _) => s,
Import::Rename(s, _, _) => s,
let (import, loc) = match import {
Import::Plain(s, l) => (s, l),
Import::GlobalSymbol(s, _, l) => (s, l),
Import::Rename(s, _, l) => (s, l),
};
imports.push(PathBuf::from(import.string));
imports.push(SolImport {
path: PathBuf::from(import.string),
loc: loc.into(),
});
}
_ => {}
}
Expand All @@ -446,21 +518,20 @@ fn parse_data(content: &str) -> SolData {
"failed to parse solidity ast: \"{:?}\". Falling back to regex to extract data",
err
);
version = utils::find_version_pragma(content).map(str::to_string);
version = utils::find_version_pragma(content)
.map(|m| SolVersionPragma { version: m.as_str().to_owned(), loc: m.into() });
imports = utils::find_import_paths(content)
.into_iter()
.map(|p| Path::new(p).to_path_buf())
.collect()
.map(|m| SolImport { path: PathBuf::from(m.as_str()), loc: m.into() })
.collect();
}
};
let version_req = if let Some(ref v) = version { Solc::version_req(v).ok() } else { None };
let version_req = version.as_ref().and_then(|v| Solc::version_req(&v.version).ok());
SolData { version_req, version, imports }
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;

#[test]
fn can_resolve_hardhat_dependency_graph() {
Expand Down Expand Up @@ -502,11 +573,8 @@ 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,
vec![
Path::new("ds-test/test.sol").to_path_buf(),
Path::new("./Dapp.sol").to_path_buf()
]
dapp_test.data.imports.iter().map(|i| i.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]);
}
Expand Down
Loading