use crate::{
compilers::{Compiler, CompilerVersion, ParsedSource},
project::VersionedSources,
resolver::parse::SolParser,
ArtifactOutput, CompilerSettings, Project, ProjectPathsConfig, SourceParser,
};
use core::fmt;
use foundry_compilers_artifacts::sources::{Source, Sources};
use foundry_compilers_core::{
error::{Result, SolcError},
utils,
};
use semver::{Version, VersionReq};
use std::{
collections::{BTreeSet, HashMap, HashSet, VecDeque},
io,
path::{Path, PathBuf},
};
use yansi::{Color, Paint};
pub mod parse;
mod tree;
pub use parse::SolImportAlias;
pub use tree::{print, Charset, TreeOptions};
#[derive(Debug)]
pub struct ResolvedSources<'a, C: Compiler> {
pub sources: VersionedSources<'a, C::Language, C::Settings>,
pub primary_profiles: HashMap<PathBuf, &'a str>,
pub edges: GraphEdges<C::Parser>,
}
#[derive(Clone, Debug)]
pub struct GraphEdges<P: SourceParser> {
edges: Vec<Vec<usize>>,
rev_edges: Vec<Vec<usize>>,
indices: HashMap<PathBuf, usize>,
rev_indices: HashMap<usize, PathBuf>,
versions: HashMap<usize, Option<VersionReq>>,
data: Vec<P::ParsedSource>,
parser: Option<P>,
num_input_files: usize,
unresolved_imports: HashSet<(PathBuf, PathBuf)>,
resolved_solc_include_paths: BTreeSet<PathBuf>,
}
impl<P: SourceParser> Default for GraphEdges<P> {
fn default() -> Self {
Self {
edges: Default::default(),
rev_edges: Default::default(),
indices: Default::default(),
rev_indices: Default::default(),
versions: Default::default(),
data: Default::default(),
parser: Default::default(),
num_input_files: Default::default(),
unresolved_imports: Default::default(),
resolved_solc_include_paths: Default::default(),
}
}
}
impl<P: SourceParser> GraphEdges<P> {
pub fn parser(&self) -> &P {
self.parser.as_ref().unwrap()
}
pub fn parser_mut(&mut self) -> &mut P {
self.parser.as_mut().unwrap()
}
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) -> &BTreeSet<PathBuf> {
&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: &Path) -> HashSet<&Path> {
if let Some(start) = self.indices.get(file).copied() {
NodesIter::new(start, self).skip(1).map(move |idx| &*self.rev_indices[&idx]).collect()
} else {
HashSet::new()
}
}
pub fn importers(&self, file: &Path) -> HashSet<&Path> {
if let Some(start) = self.indices.get(file).copied() {
self.rev_edges[start].iter().map(move |idx| &*self.rev_indices[idx]).collect()
} else {
HashSet::new()
}
}
pub fn node_id(&self, file: &Path) -> usize {
self.indices[file]
}
pub fn node_path(&self, id: usize) -> &Path {
&self.rev_indices[&id]
}
pub fn is_input_file(&self, file: &Path) -> bool {
if let Some(idx) = self.indices.get(file).copied() {
idx < self.num_input_files
} else {
false
}
}
pub fn version_requirement(&self, file: &Path) -> Option<&VersionReq> {
self.indices.get(file).and_then(|idx| self.versions.get(idx)).and_then(Option::as_ref)
}
pub fn get_parsed_source(&self, file: &Path) -> Option<&P::ParsedSource>
where
P: SourceParser,
{
self.indices.get(file).and_then(|idx| self.data.get(*idx))
}
}
#[derive(Debug)]
pub struct Graph<P: SourceParser = SolParser> {
pub nodes: Vec<Node<P::ParsedSource>>,
edges: GraphEdges<P>,
root: PathBuf,
}
type L<P> = <<P as SourceParser>::ParsedSource as ParsedSource>::Language;
impl<P: SourceParser> Graph<P> {
pub fn parser(&self) -> &P {
self.edges.parser()
}
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 is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn node(&self, index: usize) -> &Node<P::ParsedSource> {
&self.nodes[index]
}
pub(crate) fn display_node(&self, index: usize) -> DisplayNode<'_, P::ParsedSource> {
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<P::ParsedSource>> + '_ {
self.node_ids(start).map(move |idx| self.node(idx))
}
fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges<P>) {
let Self { 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));
let idx2 = edges.data.len();
edges.data.push(data);
assert_eq!(idx, idx2);
}
(sources, edges)
}
pub fn into_sources(self) -> (Sources, GraphEdges<P>) {
let (sources, edges) = self.split();
(sources.into_iter().collect(), edges)
}
pub fn input_nodes(&self) -> impl Iterator<Item = &Node<P::ParsedSource>> {
self.nodes.iter().take(self.edges.num_input_files)
}
pub fn imports(&self, path: &Path) -> HashSet<&Path> {
self.edges.imports(path)
}
#[instrument(name = "Graph::resolve_sources", skip_all)]
pub fn resolve_sources(
paths: &ProjectPathsConfig<<P::ParsedSource as ParsedSource>::Language>,
mut sources: Sources,
) -> Result<Self> {
fn add_node<P: SourceParser>(
parser: &mut P,
unresolved: &mut VecDeque<(PathBuf, Node<P::ParsedSource>)>,
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 = parser.read(&target)?;
unresolved.push_back((target.clone(), node));
let idx = index.len();
index.insert(target, idx);
resolved_imports.push(idx);
}
Ok(())
}
sources.make_absolute(&paths.root);
let mut parser = P::new(paths.with_language_ref());
let mut unresolved: VecDeque<_> = parser.parse_sources(&mut sources)?.into();
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 rev_edges = Vec::with_capacity(unresolved.len());
let mut resolved_solc_include_paths = BTreeSet::new();
resolved_solc_include_paths.insert(paths.root.clone());
let mut unresolved_imports = HashSet::new();
while let Some((path, node)) = unresolved.pop_front() {
let mut resolved_imports = Vec::new();
let cwd = match path.parent() {
Some(inner) => inner,
None => continue,
};
for import_path in node.data.resolve_imports(paths, &mut resolved_solc_include_paths)? {
if let Some(err) = match paths.resolve_import_and_include_paths(
cwd,
&import_path,
&mut resolved_solc_include_paths,
) {
Ok(import) => add_node(
&mut parser,
&mut unresolved,
&mut index,
&mut resolved_imports,
import,
)
.err(),
Err(err) => Some(err),
} {
unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));
trace!("failed to resolve import component \"{:?}\" for {:?}", err, node.path)
}
}
nodes.push(node);
edges.push(resolved_imports);
rev_edges.push(Vec::new());
}
for (idx, edges) in edges.iter().enumerate() {
for &edge in edges.iter() {
rev_edges[edge].push(idx);
}
}
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,
);
}
parser.finalize_imports(&mut nodes, &resolved_solc_include_paths)?;
let edges = GraphEdges {
edges,
rev_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().cloned()))
.collect(),
data: Default::default(),
parser: Some(parser),
unresolved_imports,
resolved_solc_include_paths,
};
Ok(Self { nodes, edges, root: paths.root.clone() })
}
pub fn resolve(
paths: &ProjectPathsConfig<<P::ParsedSource as ParsedSource>::Language>,
) -> Result<Self> {
Self::resolve_sources(paths, paths.read_input_files()?)
}
pub fn into_sources_by_version<C, T>(
self,
project: &Project<C, T>,
) -> Result<ResolvedSources<'_, C>>
where
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
C: Compiler<Parser = P, Language = <P::ParsedSource as ParsedSource>::Language>,
{
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(project)?;
let versioned_nodes = self.resolve_settings(project, versioned_nodes)?;
let (nodes, edges) = self.split();
let mut all_nodes = nodes.into_iter().enumerate().collect::<HashMap<_, _>>();
let mut resulted_sources = HashMap::new();
let mut default_profiles = HashMap::new();
let profiles = project.settings_profiles().collect::<Vec<_>>();
for (language, versioned_nodes) in versioned_nodes {
let mut versioned_sources = Vec::with_capacity(versioned_nodes.len());
for (version, profile_to_nodes) in versioned_nodes {
for (profile_idx, input_node_indexes) in profile_to_nodes {
let mut sources = Sources::new();
let mut processed_sources = input_node_indexes.iter().copied().collect();
for idx in input_node_indexes {
let (path, source) =
all_nodes.get(&idx).cloned().expect("node is preset. qed");
default_profiles.insert(path.clone(), profiles[profile_idx].0);
sources.insert(path, source);
insert_imports(
idx,
&mut all_nodes,
&mut sources,
&edges.edges,
&mut processed_sources,
);
}
versioned_sources.push((version.clone(), sources, profiles[profile_idx]));
}
}
resulted_sources.insert(language, versioned_sources);
}
Ok(ResolvedSources { sources: resulted_sources, primary_profiles: default_profiles, edges })
}
fn format_imports_list<
C: Compiler,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
W: std::fmt::Write,
>(
&self,
idx: usize,
incompatible: HashSet<usize>,
project: &Project<C, T>,
f: &mut W,
) -> std::result::Result<(), std::fmt::Error> {
let format_node = |idx, f: &mut W| {
let node = self.node(idx);
let color = if incompatible.contains(&idx) { Color::Red } else { Color::White };
let mut line = utils::source_name(&node.path, &self.root).display().to_string();
if let Some(req) = self.version_requirement(idx, project) {
line.push_str(&format!(" {req}"));
}
write!(f, "{}", line.paint(color))
};
format_node(idx, f)?;
write!(f, " imports:")?;
for dep in self.node_ids(idx).skip(1) {
write!(f, "\n ")?;
format_node(dep, f)?;
}
Ok(())
}
fn version_requirement<
C: Compiler,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
>(
&self,
idx: usize,
project: &Project<C, T>,
) -> Option<VersionReq> {
let node = self.node(idx);
let parsed_req = node.data.version_req();
let other_req = project.restrictions.get(&node.path).and_then(|r| r.version.as_ref());
match (parsed_req, other_req) {
(Some(parsed_req), Some(other_req)) => {
let mut req = parsed_req.clone();
req.comparators.extend(other_req.comparators.clone());
Some(req)
}
(Some(parsed_req), None) => Some(parsed_req.clone()),
(None, Some(other_req)) => Some(other_req.clone()),
_ => None,
}
}
fn check_available_version<
C: Compiler,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
>(
&self,
idx: usize,
all_versions: &[&CompilerVersion],
project: &Project<C, T>,
) -> std::result::Result<(), SourceVersionError> {
let Some(req) = self.version_requirement(idx, project) else { return Ok(()) };
if !all_versions.iter().any(|v| req.matches(v.as_ref())) {
return if project.offline {
Err(SourceVersionError::NoMatchingVersionOffline(req))
} else {
Err(SourceVersionError::NoMatchingVersion(req))
};
}
Ok(())
}
fn retain_compatible_versions<
C: Compiler,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
>(
&self,
idx: usize,
candidates: &mut Vec<&CompilerVersion>,
project: &Project<C, T>,
) -> Result<(), String> {
let mut all_versions = candidates.clone();
let nodes: Vec<_> = self.node_ids(idx).collect();
let mut failed_node_idx = None;
for node in nodes.iter() {
if let Some(req) = self.version_requirement(*node, project) {
candidates.retain(|v| req.matches(v.as_ref()));
if candidates.is_empty() {
failed_node_idx = Some(*node);
break;
}
}
}
let Some(failed_node_idx) = failed_node_idx else {
return Ok(());
};
let failed_node = self.node(failed_node_idx);
if let Err(version_err) =
self.check_available_version(failed_node_idx, &all_versions, project)
{
let f = utils::source_name(&failed_node.path, &self.root).display();
return Err(format!("Encountered invalid solc version in {f}: {version_err}"));
} else {
if let Some(req) = self.version_requirement(failed_node_idx, project) {
all_versions.retain(|v| req.matches(v.as_ref()));
}
for node in &nodes {
if self.check_available_version(*node, &all_versions, project).is_err() {
let mut msg = "Found incompatible versions:\n".white().to_string();
self.format_imports_list(
idx,
[*node, failed_node_idx].into(),
project,
&mut msg,
)
.unwrap();
return Err(msg);
}
}
}
let mut msg = "Found incompatible versions:\n".white().to_string();
self.format_imports_list(idx, nodes.into_iter().collect(), project, &mut msg).unwrap();
Err(msg)
}
fn retain_compatible_profiles<
C: Compiler,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
>(
&self,
idx: usize,
project: &Project<C, T>,
candidates: &mut Vec<(usize, (&str, &C::Settings))>,
) -> Result<(), String> {
let mut all_profiles = candidates.clone();
let nodes: Vec<_> = self.node_ids(idx).collect();
let mut failed_node_idx = None;
for node in nodes.iter() {
if let Some(req) = project.restrictions.get(&self.node(*node).path) {
candidates.retain(|(_, (_, settings))| settings.satisfies_restrictions(&**req));
if candidates.is_empty() {
failed_node_idx = Some(*node);
break;
}
}
}
let Some(failed_node_idx) = failed_node_idx else {
return Ok(());
};
let failed_node = self.node(failed_node_idx);
if let Some(req) = project.restrictions.get(&failed_node.path) {
all_profiles.retain(|(_, (_, settings))| settings.satisfies_restrictions(&**req));
}
if all_profiles.is_empty() {
let f = utils::source_name(&failed_node.path, &self.root).display();
return Err(format!("Missing profile satisfying settings restrictions for {f}"));
}
for node in &nodes {
if let Some(req) = project.restrictions.get(&self.node(*node).path) {
if !all_profiles
.iter()
.any(|(_, (_, settings))| settings.satisfies_restrictions(&**req))
{
let mut msg = "Found incompatible settings restrictions:\n".white().to_string();
self.format_imports_list(
idx,
[*node, failed_node_idx].into(),
project,
&mut msg,
)
.unwrap();
return Err(msg);
}
}
}
let mut msg = "Found incompatible settings restrictions:\n".white().to_string();
self.format_imports_list(idx, nodes.into_iter().collect(), project, &mut msg).unwrap();
Err(msg)
}
fn input_nodes_by_language(&self) -> HashMap<L<P>, Vec<usize>> {
let mut nodes = HashMap::new();
for idx in 0..self.edges.num_input_files {
nodes.entry(self.nodes[idx].data.language()).or_insert_with(Vec::new).push(idx);
}
nodes
}
#[allow(clippy::type_complexity)]
fn get_input_node_versions<
C: Compiler<Language = L<P>>,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
>(
&self,
project: &Project<C, T>,
) -> Result<HashMap<L<P>, HashMap<Version, Vec<usize>>>> {
trace!("resolving input node versions");
let mut resulted_nodes = HashMap::new();
for (language, nodes) in self.input_nodes_by_language() {
let mut errors = Vec::new();
let all_versions = if project.offline {
project
.compiler
.available_versions(&language)
.into_iter()
.filter(|v| v.is_installed())
.collect()
} else {
project.compiler.available_versions(&language)
};
if all_versions.is_empty() && !nodes.is_empty() {
return Err(SolcError::msg(format!(
"Found {language} sources, but no compiler versions are available for it"
)));
}
let mut versioned_nodes = HashMap::new();
let mut all_candidates = Vec::with_capacity(self.edges.num_input_files);
for idx in nodes {
let mut candidates = all_versions.iter().collect::<Vec<_>>();
if let Err(err) = self.retain_compatible_versions(idx, &mut candidates, project) {
errors.push(err);
} 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 {
trace!(
"found exact solc version for all sources \"{}\"",
versioned_nodes.keys().next().unwrap()
);
}
if errors.is_empty() {
trace!("resolved {} versions {:?}", versioned_nodes.len(), versioned_nodes.keys());
resulted_nodes.insert(
language,
versioned_nodes
.into_iter()
.map(|(v, nodes)| (Version::from(v), nodes))
.collect(),
);
} else {
let s = errors.join("\n");
debug!("failed to resolve versions: {s}");
return Err(SolcError::msg(s));
}
}
Ok(resulted_nodes)
}
#[allow(clippy::complexity)]
fn resolve_settings<
C: Compiler<Language = L<P>>,
T: ArtifactOutput<CompilerContract = C::CompilerContract>,
>(
&self,
project: &Project<C, T>,
input_nodes_versions: HashMap<L<P>, HashMap<Version, Vec<usize>>>,
) -> Result<HashMap<L<P>, HashMap<Version, HashMap<usize, Vec<usize>>>>> {
let mut resulted_sources = HashMap::new();
let mut errors = Vec::new();
for (language, versions) in input_nodes_versions {
let mut versioned_sources = HashMap::new();
for (version, nodes) in versions {
let mut profile_to_nodes = HashMap::new();
for idx in nodes {
let mut profile_candidates =
project.settings_profiles().enumerate().collect::<Vec<_>>();
if let Err(err) =
self.retain_compatible_profiles(idx, project, &mut profile_candidates)
{
errors.push(err);
} else {
let (profile_idx, _) = profile_candidates.first().expect("exists");
profile_to_nodes.entry(*profile_idx).or_insert_with(Vec::new).push(idx);
}
}
versioned_sources.insert(version, profile_to_nodes);
}
resulted_sources.insert(language, versioned_sources);
}
if errors.is_empty() {
Ok(resulted_sources)
} else {
let s = errors.join("\n");
debug!("failed to resolve settings: {s}");
Err(SolcError::msg(s))
}
}
fn resolve_multiple_versions(
all_candidates: Vec<(usize, HashSet<&CompilerVersion>)>,
) -> HashMap<CompilerVersion, Vec<usize>> {
fn intersection<'a>(
mut sets: Vec<&HashSet<&'a CompilerVersion>>,
) -> Vec<&'a CompilerVersion> {
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<&CompilerVersion>) -> CompilerVersion {
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();
trace!("resolved solc version compatible with all sources \"{}\"", exact_version);
return HashMap::from([(exact_version, all_nodes)]);
}
let mut versioned_nodes: HashMap<_, _> = 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);
}
trace!(
"no solc version can satisfy all source files, resolved multiple versions \"{:?}\"",
versioned_nodes.keys()
);
versioned_nodes
}
}
#[derive(Debug)]
pub struct NodesIter<'a, P: SourceParser> {
stack: VecDeque<usize>,
visited: HashSet<usize>,
graph: &'a GraphEdges<P>,
}
impl<'a, P: SourceParser> NodesIter<'a, P> {
fn new(start: usize, graph: &'a GraphEdges<P>) -> Self {
Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph }
}
}
impl<P: SourceParser> Iterator for NodesIter<'_, P> {
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)
}
}
#[derive(Debug)]
pub struct Node<S> {
path: PathBuf,
source: Source,
pub data: S,
}
impl<S> Node<S> {
pub fn new(path: PathBuf, source: Source, data: S) -> Self {
Self { path, source, data }
}
pub fn map_data<T>(self, f: impl FnOnce(S) -> T) -> Node<T> {
Node::new(self.path, self.source, f(self.data))
}
}
impl<S: ParsedSource> Node<S> {
pub fn read(file: &Path) -> Result<Self> {
let source = Source::read_(file)?;
Self::parse(file, source)
}
pub fn parse(file: &Path, source: Source) -> Result<Self> {
let data = S::parse(source.as_ref(), file)?;
Ok(Self::new(file.to_path_buf(), source, data))
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn content(&self) -> &str {
&self.source.content
}
pub fn unpack(&self) -> (&Path, &Source) {
(&self.path, &self.source)
}
}
pub(crate) struct DisplayNode<'a, S> {
node: &'a Node<S>,
root: &'a PathBuf,
}
impl<S: ParsedSource> fmt::Display for DisplayNode<'_, S> {
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(v) = self.node.data.version_req() {
write!(f, " {v}")?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)]
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 = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample");
let paths = ProjectPathsConfig::hardhat(&root).unwrap();
let graph = Graph::<SolParser>::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 = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
let paths = ProjectPathsConfig::dapptools(&root).unwrap();
let graph = Graph::<SolParser>::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<&Path>>(),
vec![Path::new("ds-test/test.sol"), Path::new("./Dapp.sol")]
);
assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);
}
#[test]
fn can_print_dapp_sample_graph() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
let paths = ProjectPathsConfig::dapptools(&root).unwrap();
let graph = Graph::<SolParser>::resolve(&paths).unwrap();
let mut out = Vec::<u8>::new();
tree::print(&graph, &Default::default(), &mut out).unwrap();
if !cfg!(windows) {
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
);
}
graph.edges.parser().compiler.enter(|c| {
assert_eq!(c.gcx().sources.len(), 3);
});
}
#[test]
#[cfg(not(target_os = "windows"))]
fn can_print_hardhat_sample_graph() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample");
let paths = ProjectPathsConfig::hardhat(&root).unwrap();
let graph = Graph::<SolParser>::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
",
String::from_utf8(out).unwrap()
);
}
#[test]
#[cfg(feature = "svm-solc")]
fn test_print_unresolved() {
use crate::{solc::SolcCompiler, ProjectBuilder};
let root =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/incompatible-pragmas");
let paths = ProjectPathsConfig::dapptools(&root).unwrap();
let graph = Graph::<SolParser>::resolve(&paths).unwrap();
let Err(SolcError::Message(err)) = graph.get_input_node_versions(
&ProjectBuilder::<SolcCompiler>::default()
.paths(paths)
.build(SolcCompiler::AutoDetect)
.unwrap(),
) else {
panic!("expected error");
};
snapbox::assert_data_eq!(
err,
snapbox::str![[r#"
[37mFound incompatible versions:
[0m[31msrc/A.sol =0.8.25[0m imports:
[37msrc/B.sol[0m
[31msrc/C.sol =0.7.0[0m
"#]]
);
}
#[cfg(target_os = "linux")]
#[test]
fn can_read_different_case() {
use crate::resolver::parse::SolData;
use std::fs::{self, create_dir_all};
use utils::tempdir;
let tmp_dir = tempdir("out").unwrap();
let path = tmp_dir.path().join("forge-std");
create_dir_all(&path).unwrap();
let existing = path.join("Test.sol");
let non_existing = path.join("test.sol");
fs::write(
existing,
"
pragma solidity ^0.8.10;
contract A {}
",
)
.unwrap();
assert!(!non_existing.exists());
let found = crate::resolver::Node::<SolData>::read(&non_existing).unwrap_err();
matches!(found, SolcError::ResolveCaseSensitiveFileName { .. });
}
}