use crate::{error::Result, utils, IncludePaths, ProjectPathsConfig, SolcError, Source, Sources};
use parse::{SolData, SolDataUnit, SolImport};
use rayon::prelude::*;
use semver::VersionReq;
use std::{
collections::{HashMap, HashSet, VecDeque},
fmt, io,
path::{Path, PathBuf},
};
mod parse;
mod tree;
use crate::utils::find_case_sensitive_existing_file;
pub use parse::SolImportAlias;
pub use tree::{print, Charset, TreeOptions};
#[derive(Debug)]
pub struct GraphEdges {
edges: Vec<Vec<usize>>,
indices: HashMap<PathBuf, usize>,
rev_indices: HashMap<usize, PathBuf>,
versions: HashMap<usize, Option<VersionReq>>,
data: HashMap<usize, SolData>,
num_input_files: usize,
unresolved_imports: HashSet<(PathBuf, PathBuf)>,
#[allow(unused)]
resolved_solc_include_paths: IncludePaths,
}
impl GraphEdges {
pub fn num_source_files(&self) -> usize {
self.num_input_files
}
pub fn files(&self) -> impl Iterator<Item = usize> + '_ {
0..self.edges.len()
}
pub fn source_files(&self) -> impl Iterator<Item = usize> + '_ {
0..self.num_input_files
}
pub fn library_files(&self) -> impl Iterator<Item = usize> + '_ {
self.files().skip(self.num_input_files)
}
pub fn include_paths(&self) -> &IncludePaths {
&self.resolved_solc_include_paths
}
pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> {
&self.unresolved_imports
}
pub fn imported_nodes(&self, from: usize) -> &[usize] {
&self.edges[from]
}
pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
NodesIter::new(from, self).skip(1)
}
pub fn imports(&self, file: impl AsRef<Path>) -> HashSet<&PathBuf> {
if let Some(start) = self.indices.get(file.as_ref()).copied() {
NodesIter::new(start, self).skip(1).map(move |idx| &self.rev_indices[&idx]).collect()
} else {
HashSet::new()
}
}
pub fn node_id(&self, file: impl AsRef<Path>) -> usize {
self.indices[file.as_ref()]
}
pub fn node_path(&self, id: usize) -> &PathBuf {
&self.rev_indices[&id]
}
pub fn is_input_file(&self, file: impl AsRef<Path>) -> bool {
if let Some(idx) = self.indices.get(file.as_ref()).copied() {
idx < self.num_input_files
} else {
false
}
}
pub fn version_requirement(&self, file: impl AsRef<Path>) -> Option<&VersionReq> {
self.indices
.get(file.as_ref())
.and_then(|idx| self.versions.get(idx))
.and_then(|v| v.as_ref())
}
pub fn get_link_references(&self, file: impl AsRef<Path>) -> HashSet<&PathBuf> {
let mut link_references = HashSet::new();
for import in self.all_imported_nodes(self.node_id(file)) {
let data = &self.data[&import];
if data.has_link_references() {
link_references.insert(&self.rev_indices[&import]);
}
}
link_references
}
}
#[derive(Debug)]
pub struct Graph {
nodes: Vec<Node>,
edges: GraphEdges,
#[allow(unused)]
root: PathBuf,
}
impl Graph {
pub fn print(&self) {
self.print_with_options(Default::default())
}
pub fn print_with_options(&self, opts: TreeOptions) {
let stdout = io::stdout();
let mut out = stdout.lock();
tree::print(self, &opts, &mut out).expect("failed to write to stdout.")
}
pub fn imported_nodes(&self, from: usize) -> &[usize] {
self.edges.imported_nodes(from)
}
pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
self.edges.all_imported_nodes(from)
}
pub(crate) fn has_outgoing_edges(&self, index: usize) -> bool {
!self.edges.edges[index].is_empty()
}
pub fn files(&self) -> &HashMap<PathBuf, usize> {
&self.edges.indices
}
pub fn node(&self, index: usize) -> &Node {
&self.nodes[index]
}
pub(crate) fn display_node(&self, index: usize) -> DisplayNode {
DisplayNode { node: self.node(index), root: &self.root }
}
pub fn node_ids(&self, start: usize) -> impl Iterator<Item = usize> + '_ {
NodesIter::new(start, &self.edges)
}
pub fn nodes(&self, start: usize) -> impl Iterator<Item = &Node> + '_ {
self.node_ids(start).map(move |idx| self.node(idx))
}
fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges) {
let Graph { nodes, mut edges, .. } = self;
let mut sources = Vec::new();
for (idx, node) in nodes.into_iter().enumerate() {
let Node { path, source, data } = node;
sources.push((path, source));
edges.data.insert(idx, data);
}
(sources, edges)
}
pub fn into_sources(self) -> (Sources, GraphEdges) {
let (sources, edges) = self.split();
(sources.into_iter().collect(), edges)
}
pub fn input_nodes(&self) -> impl Iterator<Item = &Node> {
self.nodes.iter().take(self.edges.num_input_files)
}
pub fn imports(&self, path: impl AsRef<Path>) -> HashSet<&PathBuf> {
self.edges.imports(path)
}
pub fn resolve_sources(paths: &ProjectPathsConfig, sources: Sources) -> Result<Graph> {
fn add_node(
unresolved: &mut VecDeque<(PathBuf, Node)>,
index: &mut HashMap<PathBuf, usize>,
resolved_imports: &mut Vec<usize>,
target: PathBuf,
) -> Result<()> {
if let Some(idx) = index.get(&target).copied() {
resolved_imports.push(idx);
} else {
let node = Node::read(&target)?;
unresolved.push_back((target.clone(), node));
let idx = index.len();
index.insert(target, idx);
resolved_imports.push(idx);
}
Ok(())
}
let mut unresolved: VecDeque<(PathBuf, Node)> = sources
.into_par_iter()
.map(|(path, source)| {
let data = SolData::parse(source.as_ref(), &path);
(path.clone(), Node { path, source, data })
})
.collect();
let mut index: HashMap<_, _> =
unresolved.iter().enumerate().map(|(idx, (p, _))| (p.clone(), idx)).collect();
let num_input_files = unresolved.len();
let mut nodes = Vec::with_capacity(unresolved.len());
let mut edges = Vec::with_capacity(unresolved.len());
let mut resolved_solc_include_paths = IncludePaths::default();
let mut unresolved_imports = HashSet::new();
while let Some((path, node)) = unresolved.pop_front() {
let mut resolved_imports = Vec::with_capacity(node.data.imports.len());
let cwd = match path.parent() {
Some(inner) => inner,
None => continue,
};
for import in node.data.imports.iter() {
let import_path = import.data().path();
match paths.resolve_import_and_include_paths(
cwd,
import_path,
&mut resolved_solc_include_paths,
) {
Ok(import) => {
add_node(&mut unresolved, &mut index, &mut resolved_imports, import)
.map_err(|err| {
match err {
err @ SolcError::ResolveCaseSensitiveFileName { .. } |
err @ SolcError::Resolve(_) => {
SolcError::FailedResolveImport(
Box::new(err),
node.path.clone(),
import_path.clone(),
)
}
_ => err,
}
})?
}
Err(err) => {
unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));
tracing::trace!(
"failed to resolve import component \"{:?}\" for {:?}",
err,
node.path
)
}
};
}
nodes.push(node);
edges.push(resolved_imports);
}
if !unresolved_imports.is_empty() {
crate::report::unresolved_imports(
&unresolved_imports
.iter()
.map(|(i, f)| (i.as_path(), f.as_path()))
.collect::<Vec<_>>(),
&paths.remappings,
);
}
let edges = GraphEdges {
edges,
rev_indices: index.iter().map(|(k, v)| (*v, k.clone())).collect(),
indices: index,
num_input_files,
versions: nodes
.iter()
.enumerate()
.map(|(idx, node)| (idx, node.data.version_req.clone()))
.collect(),
data: Default::default(),
unresolved_imports,
resolved_solc_include_paths,
};
Ok(Graph { nodes, edges, root: paths.root.clone() })
}
pub fn resolve(paths: &ProjectPathsConfig) -> Result<Graph> {
Self::resolve_sources(paths, paths.read_input_files()?)
}
}
#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
impl Graph {
pub fn into_sources_by_version(self, offline: bool) -> Result<(VersionedSources, GraphEdges)> {
fn insert_imports(
idx: usize,
all_nodes: &mut HashMap<usize, (PathBuf, Source)>,
sources: &mut Sources,
edges: &[Vec<usize>],
processed_sources: &mut HashSet<usize>,
) {
for dep in edges[idx].iter().copied() {
if !processed_sources.insert(dep) {
continue
}
if let Some((path, source)) = all_nodes.get(&dep).cloned() {
sources.insert(path, source);
insert_imports(dep, all_nodes, sources, edges, processed_sources);
}
}
}
let versioned_nodes = self.get_input_node_versions(offline)?;
let (nodes, edges) = self.split();
let mut versioned_sources = HashMap::with_capacity(versioned_nodes.len());
let mut all_nodes = nodes.into_iter().enumerate().collect::<HashMap<_, _>>();
for (version, input_node_indices) in versioned_nodes {
let mut sources = Sources::new();
let mut processed_sources = input_node_indices.iter().copied().collect();
for idx in input_node_indices {
let (path, source) = all_nodes.get(&idx).cloned().expect("node is preset. qed");
sources.insert(path, source);
insert_imports(
idx,
&mut all_nodes,
&mut sources,
&edges.edges,
&mut processed_sources,
);
}
versioned_sources.insert(version, sources);
}
Ok((
VersionedSources {
inner: versioned_sources,
offline,
resolved_solc_include_paths: edges.resolved_solc_include_paths.clone(),
},
edges,
))
}
fn format_imports_list<W: std::fmt::Write>(
&self,
idx: usize,
f: &mut W,
) -> std::result::Result<(), std::fmt::Error> {
let node = self.node(idx);
write!(f, "{} ", utils::source_name(&node.path, &self.root).display())?;
node.data.fmt_version(f)?;
write!(f, " imports:")?;
for dep in self.node_ids(idx).skip(1) {
let dep = self.node(dep);
write!(f, "\n {} ", utils::source_name(&dep.path, &self.root).display())?;
dep.data.fmt_version(f)?;
}
Ok(())
}
fn retain_compatible_versions(&self, idx: usize, candidates: &mut Vec<&crate::SolcVersion>) {
let nodes: HashSet<_> = self.node_ids(idx).collect();
for node in nodes {
if let Some(req) = &self.node(node).data.version_req {
candidates.retain(|v| req.matches(v.as_ref()));
}
if candidates.is_empty() {
return
}
}
}
pub fn ensure_compatible_imports(&self, offline: bool) -> Result<()> {
self.get_input_node_versions(offline)?;
Ok(())
}
fn get_input_node_versions(
&self,
offline: bool,
) -> Result<HashMap<crate::SolcVersion, Vec<usize>>> {
use crate::Solc;
tracing::trace!("resolving input node versions");
let mut errors = Vec::new();
let mut erroneous_nodes = HashSet::with_capacity(self.edges.num_input_files);
let all_versions = if offline { Solc::installed_versions() } else { Solc::all_versions() };
let mut versioned_nodes = HashMap::new();
let mut all_candidates = Vec::with_capacity(self.edges.num_input_files);
for idx in 0..self.edges.num_input_files {
let mut candidates = all_versions.iter().collect::<Vec<_>>();
self.retain_compatible_versions(idx, &mut candidates);
if candidates.is_empty() && !erroneous_nodes.contains(&idx) {
let node = self.node(idx);
if let Err(version_err) = node.check_available_version(&all_versions, offline) {
let f = utils::source_name(&node.path, &self.root).display();
errors.push(format!("Encountered invalid solc version in {f}: {version_err}"));
} else {
let mut msg = String::new();
self.format_imports_list(idx, &mut msg).unwrap();
errors.push(format!("Found incompatible Solidity versions:\n{msg}"));
}
erroneous_nodes.insert(idx);
} else {
let candidate =
if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
candidates[pos]
} else {
candidates.last().expect("not empty; qed.")
}
.clone();
all_candidates.push((idx, candidates.into_iter().collect::<HashSet<_>>()));
versioned_nodes.entry(candidate).or_insert_with(|| Vec::with_capacity(1)).push(idx);
}
}
if versioned_nodes.len() > 1 {
versioned_nodes = Self::resolve_multiple_versions(all_candidates);
}
if versioned_nodes.len() == 1 {
tracing::trace!(
"found exact solc version for all sources \"{}\"",
versioned_nodes.keys().next().unwrap()
);
}
if errors.is_empty() {
tracing::trace!(
"resolved {} versions {:?}",
versioned_nodes.len(),
versioned_nodes.keys()
);
Ok(versioned_nodes)
} else {
tracing::error!("failed to resolve versions");
Err(SolcError::msg(errors.join("\n")))
}
}
fn resolve_multiple_versions(
all_candidates: Vec<(usize, HashSet<&crate::SolcVersion>)>,
) -> HashMap<crate::SolcVersion, Vec<usize>> {
fn intersection<'a>(
mut sets: Vec<&HashSet<&'a crate::SolcVersion>>,
) -> Vec<&'a crate::SolcVersion> {
if sets.is_empty() {
return Vec::new()
}
let mut result = sets.pop().cloned().expect("not empty; qed.");
if !sets.is_empty() {
result.retain(|item| sets.iter().all(|set| set.contains(item)));
}
let mut v = result.into_iter().collect::<Vec<_>>();
v.sort_unstable();
v
}
fn remove_candidate(candidates: &mut Vec<&crate::SolcVersion>) -> crate::SolcVersion {
debug_assert!(!candidates.is_empty());
if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
candidates.remove(pos)
} else {
candidates.pop().expect("not empty; qed.")
}
.clone()
}
let all_sets = all_candidates.iter().map(|(_, versions)| versions).collect();
let mut intersection = intersection(all_sets);
if !intersection.is_empty() {
let exact_version = remove_candidate(&mut intersection);
let all_nodes = all_candidates.into_iter().map(|(node, _)| node).collect();
tracing::trace!(
"resolved solc version compatible with all sources \"{}\"",
exact_version
);
return HashMap::from([(exact_version, all_nodes)])
}
let mut versioned_nodes: HashMap<crate::SolcVersion, Vec<usize>> = HashMap::new();
for (node, versions) in all_candidates {
let mut versions = versions.into_iter().collect::<Vec<_>>();
versions.sort_unstable();
let candidate =
if let Some(idx) = versions.iter().rposition(|v| versioned_nodes.contains_key(v)) {
versions.remove(idx).clone()
} else {
remove_candidate(&mut versions)
};
versioned_nodes.entry(candidate).or_insert_with(|| Vec::with_capacity(1)).push(node);
}
tracing::trace!(
"no solc version can satisfy all source files, resolved multiple versions \"{:?}\"",
versioned_nodes.keys()
);
versioned_nodes
}
}
#[derive(Debug)]
pub struct NodesIter<'a> {
stack: VecDeque<usize>,
visited: HashSet<usize>,
graph: &'a GraphEdges,
}
impl<'a> NodesIter<'a> {
fn new(start: usize, graph: &'a GraphEdges) -> Self {
Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph }
}
}
impl<'a> Iterator for NodesIter<'a> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
let node = self.stack.pop_front()?;
if self.visited.insert(node) {
self.stack.extend(self.graph.imported_nodes(node).iter().copied());
}
Some(node)
}
}
#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
#[derive(Debug)]
pub struct VersionedSources {
resolved_solc_include_paths: IncludePaths,
inner: HashMap<crate::SolcVersion, Sources>,
offline: bool,
}
#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
impl VersionedSources {
pub fn get<T: crate::ArtifactOutput>(
self,
project: &crate::Project<T>,
) -> Result<std::collections::BTreeMap<crate::Solc, (semver::Version, Sources)>> {
use crate::Solc;
#[cfg(any(test, feature = "tests"))]
let _lock = crate::compile::take_solc_installer_lock();
let mut sources_by_version = std::collections::BTreeMap::new();
for (version, sources) in self.inner {
let solc = if !version.is_installed() {
if self.offline {
return Err(SolcError::msg(format!(
"missing solc \"{version}\" installation in offline mode"
)))
} else {
Solc::blocking_install(version.as_ref())?
}
} else {
Solc::find_svm_installed_version(version.to_string())?.ok_or_else(|| {
SolcError::msg(format!("solc \"{version}\" should have been installed"))
})?
};
if self.offline {
tracing::trace!(
"skip verifying solc checksum for {} in offline mode",
solc.solc.display()
);
} else {
tracing::trace!("verifying solc checksum for {}", solc.solc.display());
if let Err(err) = solc.verify_checksum() {
tracing::trace!(?err, "corrupted solc version, redownloading \"{}\"", version);
Solc::blocking_install(version.as_ref())?;
tracing::trace!("reinstalled solc: \"{}\"", version);
}
}
let version = solc.version()?;
let solc = project.configure_solc_with_version(
solc,
Some(version.clone()),
self.resolved_solc_include_paths.clone(),
);
sources_by_version.insert(solc, (version, sources));
}
Ok(sources_by_version)
}
}
#[derive(Debug)]
pub struct Node {
path: PathBuf,
source: Source,
data: SolData,
}
impl Node {
pub fn read(file: impl AsRef<Path>) -> Result<Self> {
let file = file.as_ref();
let source = Source::read(file).map_err(|err| {
let exists = err.path().exists();
if !exists && err.path().is_symlink() {
SolcError::ResolveBadSymlink(err)
} else {
if !exists {
if let Some(existing_file) = find_case_sensitive_existing_file(file) {
SolcError::ResolveCaseSensitiveFileName { error: err, existing_file }
} else {
SolcError::Resolve(err)
}
} else {
SolcError::Resolve(err)
}
}
})?;
let data = SolData::parse(source.as_ref(), file);
Ok(Self { path: file.to_path_buf(), source, data })
}
pub fn content(&self) -> &str {
&self.source.content
}
pub fn imports(&self) -> &Vec<SolDataUnit<SolImport>> {
&self.data.imports
}
pub fn version(&self) -> &Option<SolDataUnit<String>> {
&self.data.version
}
pub fn experimental(&self) -> &Option<SolDataUnit<String>> {
&self.data.experimental
}
pub fn license(&self) -> &Option<SolDataUnit<String>> {
&self.data.license
}
pub fn unpack(&self) -> (&PathBuf, &Source) {
(&self.path, &self.source)
}
#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
fn check_available_version(
&self,
all_versions: &[crate::SolcVersion],
offline: bool,
) -> std::result::Result<(), SourceVersionError> {
let Some(data) = &self.data.version else { return Ok(()) };
let v = data.data();
let req = crate::Solc::version_req(v)
.map_err(|err| SourceVersionError::InvalidVersion(v.to_string(), err))?;
if !all_versions.iter().any(|v| req.matches(v.as_ref())) {
return if offline {
Err(SourceVersionError::NoMatchingVersionOffline(req))
} else {
Err(SourceVersionError::NoMatchingVersion(req))
}
}
Ok(())
}
}
pub(crate) struct DisplayNode<'a> {
node: &'a Node,
root: &'a PathBuf,
}
impl<'a> fmt::Display for DisplayNode<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path = utils::source_name(&self.node.path, self.root);
write!(f, "{}", path.display())?;
if let Some(ref v) = self.node.data.version {
write!(f, " {}", v.data())?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[allow(unused)]
enum SourceVersionError {
#[error("Failed to parse solidity version {0}: {1}")]
InvalidVersion(String, SolcError),
#[error("No solc version exists that matches the version requirement: {0}")]
NoMatchingVersion(VersionReq),
#[error("No solc version installed that matches the version requirement: {0}")]
NoMatchingVersionOffline(VersionReq),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_resolve_hardhat_dependency_graph() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");
let paths = ProjectPathsConfig::hardhat(root).unwrap();
let graph = Graph::resolve(&paths).unwrap();
assert_eq!(graph.edges.num_input_files, 1);
assert_eq!(graph.files().len(), 2);
assert_eq!(
graph.files().clone(),
HashMap::from([
(paths.sources.join("Greeter.sol"), 0),
(paths.root.join("node_modules/hardhat/console.sol"), 1),
])
);
}
#[test]
fn can_resolve_dapp_dependency_graph() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
let paths = ProjectPathsConfig::dapptools(root).unwrap();
let graph = Graph::resolve(&paths).unwrap();
assert_eq!(graph.edges.num_input_files, 2);
assert_eq!(graph.files().len(), 3);
assert_eq!(
graph.files().clone(),
HashMap::from([
(paths.sources.join("Dapp.sol"), 0),
(paths.sources.join("Dapp.t.sol"), 1),
(paths.root.join("lib/ds-test/src/test.sol"), 2),
])
);
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().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]);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn can_print_dapp_sample_graph() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
let paths = ProjectPathsConfig::dapptools(root).unwrap();
let graph = Graph::resolve(&paths).unwrap();
let mut out = Vec::<u8>::new();
tree::print(&graph, &Default::default(), &mut out).unwrap();
assert_eq!(
"
src/Dapp.sol >=0.6.6
src/Dapp.t.sol >=0.6.6
├── lib/ds-test/src/test.sol >=0.4.23
└── src/Dapp.sol >=0.6.6
"
.trim_start()
.as_bytes()
.to_vec(),
out
);
}
#[test]
#[cfg(not(target_os = "windows"))]
fn can_print_hardhat_sample_graph() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");
let paths = ProjectPathsConfig::hardhat(root).unwrap();
let graph = Graph::resolve(&paths).unwrap();
let mut out = Vec::<u8>::new();
tree::print(&graph, &Default::default(), &mut out).unwrap();
assert_eq!(
"
contracts/Greeter.sol >=0.6.0
└── node_modules/hardhat/console.sol >= 0.4.22 <0.9.0
"
.trim_start()
.as_bytes()
.to_vec(),
out
);
}
}