1use crate::{error::Result, utils, IncludePaths, ProjectPathsConfig, SolcError, Source, Sources};
50use parse::{SolData, SolDataUnit, SolImport};
51use rayon::prelude::*;
52use semver::VersionReq;
53use std::{
54 collections::{HashMap, HashSet, VecDeque},
55 fmt, io,
56 path::{Path, PathBuf},
57};
58
59mod parse;
60mod tree;
61
62use crate::utils::find_case_sensitive_existing_file;
63pub use parse::SolImportAlias;
64pub use tree::{print, Charset, TreeOptions};
65
66#[derive(Debug)]
71pub struct GraphEdges {
72 edges: Vec<Vec<usize>>,
75 indices: HashMap<PathBuf, usize>,
77 rev_indices: HashMap<usize, PathBuf>,
79 versions: HashMap<usize, Option<VersionReq>>,
81 data: HashMap<usize, SolData>,
83 num_input_files: usize,
89 unresolved_imports: HashSet<(PathBuf, PathBuf)>,
91 #[allow(unused)]
97 resolved_solc_include_paths: IncludePaths,
98}
99
100impl GraphEdges {
101 pub fn num_source_files(&self) -> usize {
103 self.num_input_files
104 }
105
106 pub fn files(&self) -> impl Iterator<Item = usize> + '_ {
108 0..self.edges.len()
109 }
110
111 pub fn source_files(&self) -> impl Iterator<Item = usize> + '_ {
113 0..self.num_input_files
114 }
115
116 pub fn library_files(&self) -> impl Iterator<Item = usize> + '_ {
118 self.files().skip(self.num_input_files)
119 }
120
121 pub fn include_paths(&self) -> &IncludePaths {
123 &self.resolved_solc_include_paths
124 }
125
126 pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> {
128 &self.unresolved_imports
129 }
130
131 pub fn imported_nodes(&self, from: usize) -> &[usize] {
133 &self.edges[from]
134 }
135
136 pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
138 NodesIter::new(from, self).skip(1)
139 }
140
141 pub fn imports(&self, file: impl AsRef<Path>) -> HashSet<&PathBuf> {
143 if let Some(start) = self.indices.get(file.as_ref()).copied() {
144 NodesIter::new(start, self).skip(1).map(move |idx| &self.rev_indices[&idx]).collect()
145 } else {
146 HashSet::new()
147 }
148 }
149
150 pub fn node_id(&self, file: impl AsRef<Path>) -> usize {
152 self.indices[file.as_ref()]
153 }
154
155 pub fn node_path(&self, id: usize) -> &PathBuf {
157 &self.rev_indices[&id]
158 }
159
160 pub fn is_input_file(&self, file: impl AsRef<Path>) -> bool {
163 if let Some(idx) = self.indices.get(file.as_ref()).copied() {
164 idx < self.num_input_files
165 } else {
166 false
167 }
168 }
169
170 pub fn version_requirement(&self, file: impl AsRef<Path>) -> Option<&VersionReq> {
172 self.indices
173 .get(file.as_ref())
174 .and_then(|idx| self.versions.get(idx))
175 .and_then(|v| v.as_ref())
176 }
177
178 pub fn get_link_references(&self, file: impl AsRef<Path>) -> HashSet<&PathBuf> {
186 let mut link_references = HashSet::new();
187 for import in self.all_imported_nodes(self.node_id(file)) {
188 let data = &self.data[&import];
189 if data.has_link_references() {
190 link_references.insert(&self.rev_indices[&import]);
191 }
192 }
193 link_references
194 }
195}
196
197#[derive(Debug)]
201pub struct Graph {
202 nodes: Vec<Node>,
204 edges: GraphEdges,
206 #[allow(unused)]
208 root: PathBuf,
209}
210
211impl Graph {
212 pub fn print(&self) {
214 self.print_with_options(Default::default())
215 }
216
217 pub fn print_with_options(&self, opts: TreeOptions) {
219 let stdout = io::stdout();
220 let mut out = stdout.lock();
221 tree::print(self, &opts, &mut out).expect("failed to write to stdout.")
222 }
223
224 pub fn imported_nodes(&self, from: usize) -> &[usize] {
226 self.edges.imported_nodes(from)
227 }
228
229 pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
231 self.edges.all_imported_nodes(from)
232 }
233
234 pub(crate) fn has_outgoing_edges(&self, index: usize) -> bool {
236 !self.edges.edges[index].is_empty()
237 }
238
239 pub fn files(&self) -> &HashMap<PathBuf, usize> {
241 &self.edges.indices
242 }
243
244 pub fn node(&self, index: usize) -> &Node {
250 &self.nodes[index]
251 }
252
253 pub(crate) fn display_node(&self, index: usize) -> DisplayNode {
254 DisplayNode { node: self.node(index), root: &self.root }
255 }
256
257 pub fn node_ids(&self, start: usize) -> impl Iterator<Item = usize> + '_ {
264 NodesIter::new(start, &self.edges)
265 }
266
267 pub fn nodes(&self, start: usize) -> impl Iterator<Item = &Node> + '_ {
269 self.node_ids(start).map(move |idx| self.node(idx))
270 }
271
272 fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges) {
273 let Graph { nodes, mut edges, .. } = self;
274 let mut sources = Vec::new();
277 for (idx, node) in nodes.into_iter().enumerate() {
278 let Node { path, source, data } = node;
279 sources.push((path, source));
280 edges.data.insert(idx, data);
281 }
282
283 (sources, edges)
284 }
285
286 pub fn into_sources(self) -> (Sources, GraphEdges) {
289 let (sources, edges) = self.split();
290 (sources.into_iter().collect(), edges)
291 }
292
293 pub fn input_nodes(&self) -> impl Iterator<Item = &Node> {
297 self.nodes.iter().take(self.edges.num_input_files)
298 }
299
300 pub fn imports(&self, path: impl AsRef<Path>) -> HashSet<&PathBuf> {
302 self.edges.imports(path)
303 }
304
305 pub fn resolve_sources(paths: &ProjectPathsConfig, sources: Sources) -> Result<Graph> {
307 fn add_node(
311 unresolved: &mut VecDeque<(PathBuf, Node)>,
312 index: &mut HashMap<PathBuf, usize>,
313 resolved_imports: &mut Vec<usize>,
314 target: PathBuf,
315 ) -> Result<()> {
316 if let Some(idx) = index.get(&target).copied() {
317 resolved_imports.push(idx);
318 } else {
319 let node = Node::read(&target)?;
321 unresolved.push_back((target.clone(), node));
322 let idx = index.len();
323 index.insert(target, idx);
324 resolved_imports.push(idx);
325 }
326 Ok(())
327 }
328
329 let mut unresolved: VecDeque<(PathBuf, Node)> = sources
332 .into_par_iter()
333 .map(|(path, source)| {
334 let data = SolData::parse(source.as_ref(), &path);
335 (path.clone(), Node { path, source, data })
336 })
337 .collect();
338
339 let mut index: HashMap<_, _> =
341 unresolved.iter().enumerate().map(|(idx, (p, _))| (p.clone(), idx)).collect();
342
343 let num_input_files = unresolved.len();
344
345 let mut nodes = Vec::with_capacity(unresolved.len());
347 let mut edges = Vec::with_capacity(unresolved.len());
348
349 let mut resolved_solc_include_paths = IncludePaths::default();
352
353 let mut unresolved_imports = HashSet::new();
356
357 while let Some((path, node)) = unresolved.pop_front() {
360 let mut resolved_imports = Vec::with_capacity(node.data.imports.len());
361 let cwd = match path.parent() {
363 Some(inner) => inner,
364 None => continue,
365 };
366
367 for import in node.data.imports.iter() {
368 let import_path = import.data().path();
369 match paths.resolve_import_and_include_paths(
370 cwd,
371 import_path,
372 &mut resolved_solc_include_paths,
373 ) {
374 Ok(import) => {
375 add_node(&mut unresolved, &mut index, &mut resolved_imports, import)
376 .map_err(|err| {
377 match err {
378 err @ SolcError::ResolveCaseSensitiveFileName { .. } |
379 err @ SolcError::Resolve(_) => {
380 SolcError::FailedResolveImport(
383 Box::new(err),
384 node.path.clone(),
385 import_path.clone(),
386 )
387 }
388 _ => err,
389 }
390 })?
391 }
392 Err(err) => {
393 unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));
394 tracing::trace!(
395 "failed to resolve import component \"{:?}\" for {:?}",
396 err,
397 node.path
398 )
399 }
400 };
401 }
402
403 nodes.push(node);
404 edges.push(resolved_imports);
405 }
406
407 if !unresolved_imports.is_empty() {
408 crate::report::unresolved_imports(
410 &unresolved_imports
411 .iter()
412 .map(|(i, f)| (i.as_path(), f.as_path()))
413 .collect::<Vec<_>>(),
414 &paths.remappings,
415 );
416 }
417
418 let edges = GraphEdges {
419 edges,
420 rev_indices: index.iter().map(|(k, v)| (*v, k.clone())).collect(),
421 indices: index,
422 num_input_files,
423 versions: nodes
424 .iter()
425 .enumerate()
426 .map(|(idx, node)| (idx, node.data.version_req.clone()))
427 .collect(),
428 data: Default::default(),
429 unresolved_imports,
430 resolved_solc_include_paths,
431 };
432 Ok(Graph { nodes, edges, root: paths.root.clone() })
433 }
434
435 pub fn resolve(paths: &ProjectPathsConfig) -> Result<Graph> {
437 Self::resolve_sources(paths, paths.read_input_files()?)
438 }
439}
440
441#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
442impl Graph {
443 pub fn into_sources_by_version(self, offline: bool) -> Result<(VersionedSources, GraphEdges)> {
449 fn insert_imports(
457 idx: usize,
458 all_nodes: &mut HashMap<usize, (PathBuf, Source)>,
459 sources: &mut Sources,
460 edges: &[Vec<usize>],
461 processed_sources: &mut HashSet<usize>,
462 ) {
463 for dep in edges[idx].iter().copied() {
465 if !processed_sources.insert(dep) {
468 continue
469 }
470
471 if let Some((path, source)) = all_nodes.get(&dep).cloned() {
473 sources.insert(path, source);
474 insert_imports(dep, all_nodes, sources, edges, processed_sources);
475 }
476 }
477 }
478
479 let versioned_nodes = self.get_input_node_versions(offline)?;
480 let (nodes, edges) = self.split();
481
482 let mut versioned_sources = HashMap::with_capacity(versioned_nodes.len());
483
484 let mut all_nodes = nodes.into_iter().enumerate().collect::<HashMap<_, _>>();
485
486 for (version, input_node_indices) in versioned_nodes {
488 let mut sources = Sources::new();
489
490 let mut processed_sources = input_node_indices.iter().copied().collect();
492
493 for idx in input_node_indices {
495 let (path, source) = all_nodes.get(&idx).cloned().expect("node is preset. qed");
497 sources.insert(path, source);
498 insert_imports(
499 idx,
500 &mut all_nodes,
501 &mut sources,
502 &edges.edges,
503 &mut processed_sources,
504 );
505 }
506 versioned_sources.insert(version, sources);
507 }
508 Ok((
509 VersionedSources {
510 inner: versioned_sources,
511 offline,
512 resolved_solc_include_paths: edges.resolved_solc_include_paths.clone(),
513 },
514 edges,
515 ))
516 }
517
518 fn format_imports_list<W: std::fmt::Write>(
527 &self,
528 idx: usize,
529 f: &mut W,
530 ) -> std::result::Result<(), std::fmt::Error> {
531 let node = self.node(idx);
532 write!(f, "{} ", utils::source_name(&node.path, &self.root).display())?;
533 node.data.fmt_version(f)?;
534 write!(f, " imports:")?;
535 for dep in self.node_ids(idx).skip(1) {
536 let dep = self.node(dep);
537 write!(f, "\n {} ", utils::source_name(&dep.path, &self.root).display())?;
538 dep.data.fmt_version(f)?;
539 }
540
541 Ok(())
542 }
543
544 fn retain_compatible_versions(&self, idx: usize, candidates: &mut Vec<&crate::SolcVersion>) {
546 let nodes: HashSet<_> = self.node_ids(idx).collect();
547 for node in nodes {
548 if let Some(req) = &self.node(node).data.version_req {
549 candidates.retain(|v| req.matches(v.as_ref()));
550 }
551 if candidates.is_empty() {
552 return
554 }
555 }
556 }
557
558 pub fn ensure_compatible_imports(&self, offline: bool) -> Result<()> {
560 self.get_input_node_versions(offline)?;
561 Ok(())
562 }
563
564 fn get_input_node_versions(
575 &self,
576 offline: bool,
577 ) -> Result<HashMap<crate::SolcVersion, Vec<usize>>> {
578 use crate::Solc;
579
580 tracing::trace!("resolving input node versions");
581 let mut errors = Vec::new();
584 let mut erroneous_nodes = HashSet::with_capacity(self.edges.num_input_files);
586
587 let all_versions = if offline { Solc::installed_versions() } else { Solc::all_versions() };
589
590 let mut versioned_nodes = HashMap::new();
592
593 let mut all_candidates = Vec::with_capacity(self.edges.num_input_files);
595 for idx in 0..self.edges.num_input_files {
597 let mut candidates = all_versions.iter().collect::<Vec<_>>();
598 self.retain_compatible_versions(idx, &mut candidates);
601
602 if candidates.is_empty() && !erroneous_nodes.contains(&idx) {
603 let node = self.node(idx);
605 if let Err(version_err) = node.check_available_version(&all_versions, offline) {
606 let f = utils::source_name(&node.path, &self.root).display();
607 errors.push(format!("Encountered invalid solc version in {f}: {version_err}"));
608 } else {
609 let mut msg = String::new();
610 self.format_imports_list(idx, &mut msg).unwrap();
611 errors.push(format!("Found incompatible Solidity versions:\n{msg}"));
612 }
613
614 erroneous_nodes.insert(idx);
615 } else {
616 let candidate =
618 if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
619 candidates[pos]
620 } else {
621 candidates.last().expect("not empty; qed.")
622 }
623 .clone();
624
625 all_candidates.push((idx, candidates.into_iter().collect::<HashSet<_>>()));
627
628 versioned_nodes.entry(candidate).or_insert_with(|| Vec::with_capacity(1)).push(idx);
629 }
630 }
631
632 if versioned_nodes.len() > 1 {
635 versioned_nodes = Self::resolve_multiple_versions(all_candidates);
636 }
637
638 if versioned_nodes.len() == 1 {
639 tracing::trace!(
640 "found exact solc version for all sources \"{}\"",
641 versioned_nodes.keys().next().unwrap()
642 );
643 }
644
645 if errors.is_empty() {
646 tracing::trace!(
647 "resolved {} versions {:?}",
648 versioned_nodes.len(),
649 versioned_nodes.keys()
650 );
651 Ok(versioned_nodes)
652 } else {
653 tracing::error!("failed to resolve versions");
654 Err(SolcError::msg(errors.join("\n")))
655 }
656 }
657
658 fn resolve_multiple_versions(
664 all_candidates: Vec<(usize, HashSet<&crate::SolcVersion>)>,
665 ) -> HashMap<crate::SolcVersion, Vec<usize>> {
666 fn intersection<'a>(
668 mut sets: Vec<&HashSet<&'a crate::SolcVersion>>,
669 ) -> Vec<&'a crate::SolcVersion> {
670 if sets.is_empty() {
671 return Vec::new()
672 }
673
674 let mut result = sets.pop().cloned().expect("not empty; qed.");
675 if !sets.is_empty() {
676 result.retain(|item| sets.iter().all(|set| set.contains(item)));
677 }
678
679 let mut v = result.into_iter().collect::<Vec<_>>();
680 v.sort_unstable();
681 v
682 }
683
684 fn remove_candidate(candidates: &mut Vec<&crate::SolcVersion>) -> crate::SolcVersion {
688 debug_assert!(!candidates.is_empty());
689
690 if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
691 candidates.remove(pos)
692 } else {
693 candidates.pop().expect("not empty; qed.")
694 }
695 .clone()
696 }
697
698 let all_sets = all_candidates.iter().map(|(_, versions)| versions).collect();
699
700 let mut intersection = intersection(all_sets);
702 if !intersection.is_empty() {
703 let exact_version = remove_candidate(&mut intersection);
704 let all_nodes = all_candidates.into_iter().map(|(node, _)| node).collect();
705 tracing::trace!(
706 "resolved solc version compatible with all sources \"{}\"",
707 exact_version
708 );
709 return HashMap::from([(exact_version, all_nodes)])
710 }
711
712 let mut versioned_nodes: HashMap<crate::SolcVersion, Vec<usize>> = HashMap::new();
714
715 for (node, versions) in all_candidates {
718 let mut versions = versions.into_iter().collect::<Vec<_>>();
720 versions.sort_unstable();
721
722 let candidate =
723 if let Some(idx) = versions.iter().rposition(|v| versioned_nodes.contains_key(v)) {
724 versions.remove(idx).clone()
726 } else {
727 remove_candidate(&mut versions)
729 };
730
731 versioned_nodes.entry(candidate).or_insert_with(|| Vec::with_capacity(1)).push(node);
732 }
733
734 tracing::trace!(
735 "no solc version can satisfy all source files, resolved multiple versions \"{:?}\"",
736 versioned_nodes.keys()
737 );
738
739 versioned_nodes
740 }
741}
742
743#[derive(Debug)]
745pub struct NodesIter<'a> {
746 stack: VecDeque<usize>,
748 visited: HashSet<usize>,
749 graph: &'a GraphEdges,
750}
751
752impl<'a> NodesIter<'a> {
753 fn new(start: usize, graph: &'a GraphEdges) -> Self {
754 Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph }
755 }
756}
757
758impl<'a> Iterator for NodesIter<'a> {
759 type Item = usize;
760 fn next(&mut self) -> Option<Self::Item> {
761 let node = self.stack.pop_front()?;
762
763 if self.visited.insert(node) {
764 self.stack.extend(self.graph.imported_nodes(node).iter().copied());
766 }
767 Some(node)
768 }
769}
770
771#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
773#[derive(Debug)]
774pub struct VersionedSources {
775 resolved_solc_include_paths: IncludePaths,
776 inner: HashMap<crate::SolcVersion, Sources>,
777 offline: bool,
778}
779
780#[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
781impl VersionedSources {
782 pub fn get<T: crate::ArtifactOutput>(
788 self,
789 project: &crate::Project<T>,
790 ) -> Result<std::collections::BTreeMap<crate::Solc, (semver::Version, Sources)>> {
791 use crate::Solc;
792 #[cfg(any(test, feature = "tests"))]
794 let _lock = crate::compile::take_solc_installer_lock();
795
796 let mut sources_by_version = std::collections::BTreeMap::new();
797 for (version, sources) in self.inner {
798 let solc = if !version.is_installed() {
799 if self.offline {
800 return Err(SolcError::msg(format!(
801 "missing solc \"{version}\" installation in offline mode"
802 )))
803 } else {
804 Solc::blocking_install(version.as_ref())?
806 }
807 } else {
808 Solc::find_svm_installed_version(version.to_string())?.ok_or_else(|| {
810 SolcError::msg(format!("solc \"{version}\" should have been installed"))
811 })?
812 };
813
814 if self.offline {
815 tracing::trace!(
816 "skip verifying solc checksum for {} in offline mode",
817 solc.solc.display()
818 );
819 } else {
820 tracing::trace!("verifying solc checksum for {}", solc.solc.display());
821 if let Err(err) = solc.verify_checksum() {
822 tracing::trace!(?err, "corrupted solc version, redownloading \"{}\"", version);
823 Solc::blocking_install(version.as_ref())?;
824 tracing::trace!("reinstalled solc: \"{}\"", version);
825 }
826 }
827
828 let version = solc.version()?;
829
830 let solc = project.configure_solc_with_version(
832 solc,
833 Some(version.clone()),
834 self.resolved_solc_include_paths.clone(),
835 );
836 sources_by_version.insert(solc, (version, sources));
837 }
838 Ok(sources_by_version)
839 }
840}
841
842#[derive(Debug)]
843pub struct Node {
844 path: PathBuf,
846 source: Source,
848 data: SolData,
850}
851
852impl Node {
853 pub fn read(file: impl AsRef<Path>) -> Result<Self> {
855 let file = file.as_ref();
856 let source = Source::read(file).map_err(|err| {
857 let exists = err.path().exists();
858 if !exists && err.path().is_symlink() {
859 SolcError::ResolveBadSymlink(err)
860 } else {
861 if !exists {
863 if let Some(existing_file) = find_case_sensitive_existing_file(file) {
865 SolcError::ResolveCaseSensitiveFileName { error: err, existing_file }
866 } else {
867 SolcError::Resolve(err)
868 }
869 } else {
870 SolcError::Resolve(err)
871 }
872 }
873 })?;
874 let data = SolData::parse(source.as_ref(), file);
875 Ok(Self { path: file.to_path_buf(), source, data })
876 }
877
878 pub fn content(&self) -> &str {
879 &self.source.content
880 }
881
882 pub fn imports(&self) -> &Vec<SolDataUnit<SolImport>> {
883 &self.data.imports
884 }
885
886 pub fn version(&self) -> &Option<SolDataUnit<String>> {
887 &self.data.version
888 }
889
890 pub fn experimental(&self) -> &Option<SolDataUnit<String>> {
891 &self.data.experimental
892 }
893
894 pub fn license(&self) -> &Option<SolDataUnit<String>> {
895 &self.data.license
896 }
897
898 pub fn unpack(&self) -> (&PathBuf, &Source) {
899 (&self.path, &self.source)
900 }
901
902 #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
907 fn check_available_version(
908 &self,
909 all_versions: &[crate::SolcVersion],
910 offline: bool,
911 ) -> std::result::Result<(), SourceVersionError> {
912 let Some(data) = &self.data.version else { return Ok(()) };
913 let v = data.data();
914
915 let req = crate::Solc::version_req(v)
916 .map_err(|err| SourceVersionError::InvalidVersion(v.to_string(), err))?;
917
918 if !all_versions.iter().any(|v| req.matches(v.as_ref())) {
919 return if offline {
920 Err(SourceVersionError::NoMatchingVersionOffline(req))
921 } else {
922 Err(SourceVersionError::NoMatchingVersion(req))
923 }
924 }
925
926 Ok(())
927 }
928}
929
930pub(crate) struct DisplayNode<'a> {
932 node: &'a Node,
933 root: &'a PathBuf,
934}
935
936impl<'a> fmt::Display for DisplayNode<'a> {
937 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
938 let path = utils::source_name(&self.node.path, self.root);
939 write!(f, "{}", path.display())?;
940 if let Some(ref v) = self.node.data.version {
941 write!(f, " {}", v.data())?;
942 }
943 Ok(())
944 }
945}
946
947#[derive(Debug, thiserror::Error)]
949#[allow(unused)]
950enum SourceVersionError {
951 #[error("Failed to parse solidity version {0}: {1}")]
952 InvalidVersion(String, SolcError),
953 #[error("No solc version exists that matches the version requirement: {0}")]
954 NoMatchingVersion(VersionReq),
955 #[error("No solc version installed that matches the version requirement: {0}")]
956 NoMatchingVersionOffline(VersionReq),
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962
963 #[test]
964 fn can_resolve_hardhat_dependency_graph() {
965 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");
966 let paths = ProjectPathsConfig::hardhat(root).unwrap();
967
968 let graph = Graph::resolve(&paths).unwrap();
969
970 assert_eq!(graph.edges.num_input_files, 1);
971 assert_eq!(graph.files().len(), 2);
972
973 assert_eq!(
974 graph.files().clone(),
975 HashMap::from([
976 (paths.sources.join("Greeter.sol"), 0),
977 (paths.root.join("node_modules/hardhat/console.sol"), 1),
978 ])
979 );
980 }
981
982 #[test]
983 fn can_resolve_dapp_dependency_graph() {
984 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
985 let paths = ProjectPathsConfig::dapptools(root).unwrap();
986
987 let graph = Graph::resolve(&paths).unwrap();
988
989 assert_eq!(graph.edges.num_input_files, 2);
990 assert_eq!(graph.files().len(), 3);
991 assert_eq!(
992 graph.files().clone(),
993 HashMap::from([
994 (paths.sources.join("Dapp.sol"), 0),
995 (paths.sources.join("Dapp.t.sol"), 1),
996 (paths.root.join("lib/ds-test/src/test.sol"), 2),
997 ])
998 );
999
1000 let dapp_test = graph.node(1);
1001 assert_eq!(dapp_test.path, paths.sources.join("Dapp.t.sol"));
1002 assert_eq!(
1003 dapp_test.data.imports.iter().map(|i| i.data().path()).collect::<Vec<&PathBuf>>(),
1004 vec![&PathBuf::from("ds-test/test.sol"), &PathBuf::from("./Dapp.sol")]
1005 );
1006 assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);
1007 }
1008
1009 #[test]
1010 #[cfg(not(target_os = "windows"))]
1011 fn can_print_dapp_sample_graph() {
1012 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
1013 let paths = ProjectPathsConfig::dapptools(root).unwrap();
1014 let graph = Graph::resolve(&paths).unwrap();
1015 let mut out = Vec::<u8>::new();
1016 tree::print(&graph, &Default::default(), &mut out).unwrap();
1017
1018 assert_eq!(
1019 "
1020src/Dapp.sol >=0.6.6
1021src/Dapp.t.sol >=0.6.6
1022├── lib/ds-test/src/test.sol >=0.4.23
1023└── src/Dapp.sol >=0.6.6
1024"
1025 .trim_start()
1026 .as_bytes()
1027 .to_vec(),
1028 out
1029 );
1030 }
1031
1032 #[test]
1033 #[cfg(not(target_os = "windows"))]
1034 fn can_print_hardhat_sample_graph() {
1035 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");
1036 let paths = ProjectPathsConfig::hardhat(root).unwrap();
1037 let graph = Graph::resolve(&paths).unwrap();
1038 let mut out = Vec::<u8>::new();
1039 tree::print(&graph, &Default::default(), &mut out).unwrap();
1040 assert_eq!(
1041 "
1042contracts/Greeter.sol >=0.6.0
1043└── node_modules/hardhat/console.sol >= 0.4.22 <0.9.0
1044"
1045 .trim_start()
1046 .as_bytes()
1047 .to_vec(),
1048 out
1049 );
1050 }
1051}