1use crate::{
49 compilers::{Compiler, CompilerVersion, Language, ParsedSource},
50 project::VersionedSources,
51 ArtifactOutput, CompilerSettings, Project, ProjectPathsConfig,
52};
53use core::fmt;
54use foundry_compilers_artifacts::sources::{Source, Sources};
55use foundry_compilers_core::{
56 error::{Result, SolcError},
57 utils::{self, find_case_sensitive_existing_file},
58};
59use parse::SolData;
60use rayon::prelude::*;
61use semver::{Version, VersionReq};
62use std::{
63 collections::{BTreeSet, HashMap, HashSet, VecDeque},
64 io,
65 path::{Path, PathBuf},
66};
67use yansi::{Color, Paint};
68
69pub mod parse;
70mod tree;
71
72pub use parse::SolImportAlias;
73pub use tree::{print, Charset, TreeOptions};
74
75#[derive(Debug)]
77pub struct ResolvedSources<'a, C: Compiler> {
78 pub sources: VersionedSources<'a, C::Language, C::Settings>,
83 pub primary_profiles: HashMap<PathBuf, &'a str>,
91 pub edges: GraphEdges<C::ParsedSource>,
93}
94
95#[derive(Debug)]
100pub struct GraphEdges<D> {
101 edges: Vec<Vec<usize>>,
104 rev_edges: Vec<Vec<usize>>,
106 indices: HashMap<PathBuf, usize>,
108 rev_indices: HashMap<usize, PathBuf>,
110 versions: HashMap<usize, Option<VersionReq>>,
112 data: HashMap<usize, D>,
114 num_input_files: usize,
120 unresolved_imports: HashSet<(PathBuf, PathBuf)>,
122 resolved_solc_include_paths: BTreeSet<PathBuf>,
128}
129
130impl<D> GraphEdges<D> {
131 pub fn num_source_files(&self) -> usize {
133 self.num_input_files
134 }
135
136 pub fn files(&self) -> impl Iterator<Item = usize> + '_ {
138 0..self.edges.len()
139 }
140
141 pub fn source_files(&self) -> impl Iterator<Item = usize> + '_ {
143 0..self.num_input_files
144 }
145
146 pub fn library_files(&self) -> impl Iterator<Item = usize> + '_ {
148 self.files().skip(self.num_input_files)
149 }
150
151 pub fn include_paths(&self) -> &BTreeSet<PathBuf> {
153 &self.resolved_solc_include_paths
154 }
155
156 pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> {
158 &self.unresolved_imports
159 }
160
161 pub fn imported_nodes(&self, from: usize) -> &[usize] {
163 &self.edges[from]
164 }
165
166 pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
168 NodesIter::new(from, self).skip(1)
169 }
170
171 pub fn imports(&self, file: &Path) -> HashSet<&Path> {
173 if let Some(start) = self.indices.get(file).copied() {
174 NodesIter::new(start, self).skip(1).map(move |idx| &*self.rev_indices[&idx]).collect()
175 } else {
176 HashSet::new()
177 }
178 }
179
180 pub fn importers(&self, file: &Path) -> HashSet<&Path> {
182 if let Some(start) = self.indices.get(file).copied() {
183 self.rev_edges[start].iter().map(move |idx| &*self.rev_indices[idx]).collect()
184 } else {
185 HashSet::new()
186 }
187 }
188
189 pub fn node_id(&self, file: &Path) -> usize {
191 self.indices[file]
192 }
193
194 pub fn node_path(&self, id: usize) -> &Path {
196 &self.rev_indices[&id]
197 }
198
199 pub fn is_input_file(&self, file: &Path) -> bool {
202 if let Some(idx) = self.indices.get(file).copied() {
203 idx < self.num_input_files
204 } else {
205 false
206 }
207 }
208
209 pub fn version_requirement(&self, file: &Path) -> Option<&VersionReq> {
211 self.indices.get(file).and_then(|idx| self.versions.get(idx)).and_then(Option::as_ref)
212 }
213
214 pub fn get_parsed_source(&self, file: &Path) -> Option<&D> {
216 self.indices.get(file).and_then(|idx| self.data.get(idx))
217 }
218}
219
220#[derive(Debug)]
226pub struct Graph<D = SolData> {
227 pub nodes: Vec<Node<D>>,
229 edges: GraphEdges<D>,
231 root: PathBuf,
233}
234
235impl<L: Language, D: ParsedSource<Language = L>> Graph<D> {
236 pub fn print(&self) {
238 self.print_with_options(Default::default())
239 }
240
241 pub fn print_with_options(&self, opts: TreeOptions) {
243 let stdout = io::stdout();
244 let mut out = stdout.lock();
245 tree::print(self, &opts, &mut out).expect("failed to write to stdout.")
246 }
247
248 pub fn imported_nodes(&self, from: usize) -> &[usize] {
250 self.edges.imported_nodes(from)
251 }
252
253 pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
255 self.edges.all_imported_nodes(from)
256 }
257
258 pub(crate) fn has_outgoing_edges(&self, index: usize) -> bool {
260 !self.edges.edges[index].is_empty()
261 }
262
263 pub fn files(&self) -> &HashMap<PathBuf, usize> {
265 &self.edges.indices
266 }
267
268 pub fn is_empty(&self) -> bool {
270 self.nodes.is_empty()
271 }
272
273 pub fn node(&self, index: usize) -> &Node<D> {
279 &self.nodes[index]
280 }
281
282 pub(crate) fn display_node(&self, index: usize) -> DisplayNode<'_, D> {
283 DisplayNode { node: self.node(index), root: &self.root }
284 }
285
286 pub fn node_ids(&self, start: usize) -> impl Iterator<Item = usize> + '_ {
293 NodesIter::new(start, &self.edges)
294 }
295
296 pub fn nodes(&self, start: usize) -> impl Iterator<Item = &Node<D>> + '_ {
298 self.node_ids(start).map(move |idx| self.node(idx))
299 }
300
301 fn split(self) -> (Vec<(PathBuf, Source)>, GraphEdges<D>) {
302 let Self { nodes, mut edges, .. } = self;
303 let mut sources = Vec::new();
306 for (idx, node) in nodes.into_iter().enumerate() {
307 let Node { path, source, data } = node;
308 sources.push((path, source));
309 edges.data.insert(idx, data);
310 }
311
312 (sources, edges)
313 }
314
315 pub fn into_sources(self) -> (Sources, GraphEdges<D>) {
318 let (sources, edges) = self.split();
319 (sources.into_iter().collect(), edges)
320 }
321
322 pub fn input_nodes(&self) -> impl Iterator<Item = &Node<D>> {
326 self.nodes.iter().take(self.edges.num_input_files)
327 }
328
329 pub fn imports(&self, path: &Path) -> HashSet<&Path> {
331 self.edges.imports(path)
332 }
333
334 pub fn resolve_sources(
336 paths: &ProjectPathsConfig<D::Language>,
337 sources: Sources,
338 ) -> Result<Self> {
339 fn add_node<D: ParsedSource>(
343 unresolved: &mut VecDeque<(PathBuf, Node<D>)>,
344 index: &mut HashMap<PathBuf, usize>,
345 resolved_imports: &mut Vec<usize>,
346 target: PathBuf,
347 ) -> Result<()> {
348 if let Some(idx) = index.get(&target).copied() {
349 resolved_imports.push(idx);
350 } else {
351 let node = Node::read(&target)?;
353 unresolved.push_back((target.clone(), node));
354 let idx = index.len();
355 index.insert(target, idx);
356 resolved_imports.push(idx);
357 }
358 Ok(())
359 }
360
361 let mut unresolved: VecDeque<_> = sources
364 .0
365 .into_par_iter()
366 .map(|(path, source)| {
367 let data = D::parse(source.as_ref(), &path)?;
368 Ok((path.clone(), Node { path, source, data }))
369 })
370 .collect::<Result<_>>()?;
371
372 let mut index: HashMap<_, _> =
374 unresolved.iter().enumerate().map(|(idx, (p, _))| (p.clone(), idx)).collect();
375
376 let num_input_files = unresolved.len();
377
378 let mut nodes = Vec::with_capacity(unresolved.len());
380 let mut edges = Vec::with_capacity(unresolved.len());
381 let mut rev_edges = Vec::with_capacity(unresolved.len());
382
383 let mut resolved_solc_include_paths = BTreeSet::new();
386 resolved_solc_include_paths.insert(paths.root.clone());
387
388 let mut unresolved_imports = HashSet::new();
391
392 while let Some((path, node)) = unresolved.pop_front() {
395 let mut resolved_imports = Vec::new();
396 let cwd = match path.parent() {
398 Some(inner) => inner,
399 None => continue,
400 };
401
402 for import_path in node.data.resolve_imports(paths, &mut resolved_solc_include_paths)? {
403 match paths.resolve_import_and_include_paths(
404 cwd,
405 &import_path,
406 &mut resolved_solc_include_paths,
407 ) {
408 Ok(import) => {
409 add_node(&mut unresolved, &mut index, &mut resolved_imports, import)
410 .map_err(|err| {
411 match err {
412 SolcError::ResolveCaseSensitiveFileName { .. }
413 | SolcError::Resolve(_) => {
414 SolcError::FailedResolveImport(
417 Box::new(err),
418 node.path.clone(),
419 import_path.clone(),
420 )
421 }
422 _ => err,
423 }
424 })?
425 }
426 Err(err) => {
427 unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));
428 trace!(
429 "failed to resolve import component \"{:?}\" for {:?}",
430 err,
431 node.path
432 )
433 }
434 };
435 }
436
437 nodes.push(node);
438 edges.push(resolved_imports);
439 rev_edges.push(Vec::new());
441 }
442
443 for (idx, edges) in edges.iter().enumerate() {
445 for &edge in edges.iter() {
446 rev_edges[edge].push(idx);
447 }
448 }
449
450 if !unresolved_imports.is_empty() {
451 crate::report::unresolved_imports(
453 &unresolved_imports
454 .iter()
455 .map(|(i, f)| (i.as_path(), f.as_path()))
456 .collect::<Vec<_>>(),
457 &paths.remappings,
458 );
459 }
460
461 let edges = GraphEdges {
462 edges,
463 rev_edges,
464 rev_indices: index.iter().map(|(k, v)| (*v, k.clone())).collect(),
465 indices: index,
466 num_input_files,
467 versions: nodes
468 .iter()
469 .enumerate()
470 .map(|(idx, node)| (idx, node.data.version_req().cloned()))
471 .collect(),
472 data: Default::default(),
473 unresolved_imports,
474 resolved_solc_include_paths,
475 };
476 Ok(Self { nodes, edges, root: paths.root.clone() })
477 }
478
479 pub fn resolve(paths: &ProjectPathsConfig<D::Language>) -> Result<Self> {
481 Self::resolve_sources(paths, paths.read_input_files()?)
482 }
483}
484
485impl<L: Language, D: ParsedSource<Language = L>> Graph<D> {
486 pub fn into_sources_by_version<C, T>(
492 self,
493 project: &Project<C, T>,
494 ) -> Result<ResolvedSources<'_, C>>
495 where
496 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
497 C: Compiler<ParsedSource = D, Language = L>,
498 {
499 fn insert_imports(
507 idx: usize,
508 all_nodes: &mut HashMap<usize, (PathBuf, Source)>,
509 sources: &mut Sources,
510 edges: &[Vec<usize>],
511 processed_sources: &mut HashSet<usize>,
512 ) {
513 for dep in edges[idx].iter().copied() {
515 if !processed_sources.insert(dep) {
518 continue;
519 }
520
521 if let Some((path, source)) = all_nodes.get(&dep).cloned() {
523 sources.insert(path, source);
524 insert_imports(dep, all_nodes, sources, edges, processed_sources);
525 }
526 }
527 }
528
529 let versioned_nodes = self.get_input_node_versions(project)?;
530 let versioned_nodes = self.resolve_settings(project, versioned_nodes)?;
531 let (nodes, edges) = self.split();
532
533 let mut all_nodes = nodes.into_iter().enumerate().collect::<HashMap<_, _>>();
534
535 let mut resulted_sources = HashMap::new();
536 let mut default_profiles = HashMap::new();
537
538 let profiles = project.settings_profiles().collect::<Vec<_>>();
539
540 for (language, versioned_nodes) in versioned_nodes {
542 let mut versioned_sources = Vec::with_capacity(versioned_nodes.len());
543
544 for (version, profile_to_nodes) in versioned_nodes {
545 for (profile_idx, input_node_indixies) in profile_to_nodes {
546 let mut sources = Sources::new();
547
548 let mut processed_sources = input_node_indixies.iter().copied().collect();
550
551 for idx in input_node_indixies {
553 let (path, source) =
556 all_nodes.get(&idx).cloned().expect("node is preset. qed");
557
558 default_profiles.insert(path.clone(), profiles[profile_idx].0);
559 sources.insert(path, source);
560 insert_imports(
561 idx,
562 &mut all_nodes,
563 &mut sources,
564 &edges.edges,
565 &mut processed_sources,
566 );
567 }
568 versioned_sources.push((version.clone(), sources, profiles[profile_idx]));
569 }
570 }
571
572 resulted_sources.insert(language, versioned_sources);
573 }
574
575 Ok(ResolvedSources { sources: resulted_sources, primary_profiles: default_profiles, edges })
576 }
577
578 fn format_imports_list<
587 C: Compiler,
588 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
589 W: std::fmt::Write,
590 >(
591 &self,
592 idx: usize,
593 incompatible: HashSet<usize>,
594 project: &Project<C, T>,
595 f: &mut W,
596 ) -> std::result::Result<(), std::fmt::Error> {
597 let format_node = |idx, f: &mut W| {
598 let node = self.node(idx);
599 let color = if incompatible.contains(&idx) { Color::Red } else { Color::White };
600
601 let mut line = utils::source_name(&node.path, &self.root).display().to_string();
602 if let Some(req) = self.version_requirement(idx, project) {
603 line.push_str(&format!(" {req}"));
604 }
605
606 write!(f, "{}", line.paint(color))
607 };
608 format_node(idx, f)?;
609 write!(f, " imports:")?;
610 for dep in self.node_ids(idx).skip(1) {
611 write!(f, "\n ")?;
612 format_node(dep, f)?;
613 }
614
615 Ok(())
616 }
617
618 fn version_requirement<
620 C: Compiler,
621 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
622 >(
623 &self,
624 idx: usize,
625 project: &Project<C, T>,
626 ) -> Option<VersionReq> {
627 let node = self.node(idx);
628 let parsed_req = node.data.version_req();
629 let other_req = project.restrictions.get(&node.path).and_then(|r| r.version.as_ref());
630
631 match (parsed_req, other_req) {
632 (Some(parsed_req), Some(other_req)) => {
633 let mut req = parsed_req.clone();
634 req.comparators.extend(other_req.comparators.clone());
635 Some(req)
636 }
637 (Some(parsed_req), None) => Some(parsed_req.clone()),
638 (None, Some(other_req)) => Some(other_req.clone()),
639 _ => None,
640 }
641 }
642
643 fn check_available_version<
648 C: Compiler,
649 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
650 >(
651 &self,
652 idx: usize,
653 all_versions: &[&CompilerVersion],
654 project: &Project<C, T>,
655 ) -> std::result::Result<(), SourceVersionError> {
656 let Some(req) = self.version_requirement(idx, project) else { return Ok(()) };
657
658 if !all_versions.iter().any(|v| req.matches(v.as_ref())) {
659 return if project.offline {
660 Err(SourceVersionError::NoMatchingVersionOffline(req))
661 } else {
662 Err(SourceVersionError::NoMatchingVersion(req))
663 };
664 }
665
666 Ok(())
667 }
668
669 fn retain_compatible_versions<
672 C: Compiler,
673 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
674 >(
675 &self,
676 idx: usize,
677 candidates: &mut Vec<&CompilerVersion>,
678 project: &Project<C, T>,
679 ) -> Result<(), String> {
680 let mut all_versions = candidates.clone();
681
682 let nodes: Vec<_> = self.node_ids(idx).collect();
683 let mut failed_node_idx = None;
684 for node in nodes.iter() {
685 if let Some(req) = self.version_requirement(*node, project) {
686 candidates.retain(|v| req.matches(v.as_ref()));
687
688 if candidates.is_empty() {
689 failed_node_idx = Some(*node);
690 break;
691 }
692 }
693 }
694
695 let Some(failed_node_idx) = failed_node_idx else {
696 return Ok(());
698 };
699
700 let failed_node = self.node(failed_node_idx);
704
705 if let Err(version_err) =
706 self.check_available_version(failed_node_idx, &all_versions, project)
707 {
708 let f = utils::source_name(&failed_node.path, &self.root).display();
710 return Err(format!("Encountered invalid solc version in {f}: {version_err}"));
711 } else {
712 if let Some(req) = self.version_requirement(failed_node_idx, project) {
717 all_versions.retain(|v| req.matches(v.as_ref()));
718 }
719
720 for node in &nodes {
722 if self.check_available_version(*node, &all_versions, project).is_err() {
723 let mut msg = "Found incompatible versions:\n".white().to_string();
724
725 self.format_imports_list(
726 idx,
727 [*node, failed_node_idx].into(),
728 project,
729 &mut msg,
730 )
731 .unwrap();
732 return Err(msg);
733 }
734 }
735 }
736
737 let mut msg = "Found incompatible versions:\n".white().to_string();
738 self.format_imports_list(idx, nodes.into_iter().collect(), project, &mut msg).unwrap();
739 Err(msg)
740 }
741
742 fn retain_compatible_profiles<
744 C: Compiler,
745 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
746 >(
747 &self,
748 idx: usize,
749 project: &Project<C, T>,
750 candidates: &mut Vec<(usize, (&str, &C::Settings))>,
751 ) -> Result<(), String> {
752 let mut all_profiles = candidates.clone();
753
754 let nodes: Vec<_> = self.node_ids(idx).collect();
755 let mut failed_node_idx = None;
756 for node in nodes.iter() {
757 if let Some(req) = project.restrictions.get(&self.node(*node).path) {
758 candidates.retain(|(_, (_, settings))| settings.satisfies_restrictions(&**req));
759 if candidates.is_empty() {
760 failed_node_idx = Some(*node);
761 break;
762 }
763 }
764 }
765
766 let Some(failed_node_idx) = failed_node_idx else {
767 return Ok(());
769 };
770
771 let failed_node = self.node(failed_node_idx);
772
773 if let Some(req) = project.restrictions.get(&failed_node.path) {
775 all_profiles.retain(|(_, (_, settings))| settings.satisfies_restrictions(&**req));
776 }
777
778 if all_profiles.is_empty() {
779 let f = utils::source_name(&failed_node.path, &self.root).display();
780 return Err(format!("Missing profile satisfying settings restrictions for {f}"));
781 }
782
783 for node in &nodes {
785 if let Some(req) = project.restrictions.get(&self.node(*node).path) {
786 if !all_profiles
787 .iter()
788 .any(|(_, (_, settings))| settings.satisfies_restrictions(&**req))
789 {
790 let mut msg = "Found incompatible settings restrictions:\n".white().to_string();
791
792 self.format_imports_list(
793 idx,
794 [*node, failed_node_idx].into(),
795 project,
796 &mut msg,
797 )
798 .unwrap();
799 return Err(msg);
800 }
801 }
802 }
803
804 let mut msg = "Found incompatible settings restrictions:\n".white().to_string();
805 self.format_imports_list(idx, nodes.into_iter().collect(), project, &mut msg).unwrap();
806 Err(msg)
807 }
808
809 fn input_nodes_by_language(&self) -> HashMap<D::Language, Vec<usize>> {
810 let mut nodes = HashMap::new();
811
812 for idx in 0..self.edges.num_input_files {
813 nodes.entry(self.nodes[idx].data.language()).or_insert_with(Vec::new).push(idx);
814 }
815
816 nodes
817 }
818
819 fn get_input_node_versions<
830 C: Compiler<Language = L>,
831 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
832 >(
833 &self,
834 project: &Project<C, T>,
835 ) -> Result<HashMap<L, HashMap<Version, Vec<usize>>>> {
836 trace!("resolving input node versions");
837
838 let mut resulted_nodes = HashMap::new();
839
840 for (language, nodes) in self.input_nodes_by_language() {
841 let mut errors = Vec::new();
845
846 let all_versions = if project.offline {
848 project
849 .compiler
850 .available_versions(&language)
851 .into_iter()
852 .filter(|v| v.is_installed())
853 .collect()
854 } else {
855 project.compiler.available_versions(&language)
856 };
857
858 if all_versions.is_empty() && !nodes.is_empty() {
859 return Err(SolcError::msg(format!(
860 "Found {language} sources, but no compiler versions are available for it"
861 )));
862 }
863
864 let mut versioned_nodes = HashMap::new();
866
867 let mut all_candidates = Vec::with_capacity(self.edges.num_input_files);
869 for idx in nodes {
871 let mut candidates = all_versions.iter().collect::<Vec<_>>();
872 if let Err(err) = self.retain_compatible_versions(idx, &mut candidates, project) {
875 errors.push(err);
876 } else {
877 let candidate =
880 if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
881 candidates[pos]
882 } else {
883 candidates.last().expect("not empty; qed.")
884 }
885 .clone();
886
887 all_candidates.push((idx, candidates.into_iter().collect::<HashSet<_>>()));
889
890 versioned_nodes
891 .entry(candidate)
892 .or_insert_with(|| Vec::with_capacity(1))
893 .push(idx);
894 }
895 }
896
897 if versioned_nodes.len() > 1 {
900 versioned_nodes = Self::resolve_multiple_versions(all_candidates);
901 }
902
903 if versioned_nodes.len() == 1 {
904 trace!(
905 "found exact solc version for all sources \"{}\"",
906 versioned_nodes.keys().next().unwrap()
907 );
908 }
909
910 if errors.is_empty() {
911 trace!("resolved {} versions {:?}", versioned_nodes.len(), versioned_nodes.keys());
912 resulted_nodes.insert(
913 language,
914 versioned_nodes
915 .into_iter()
916 .map(|(v, nodes)| (Version::from(v), nodes))
917 .collect(),
918 );
919 } else {
920 let s = errors.join("\n");
921 debug!("failed to resolve versions: {s}");
922 return Err(SolcError::msg(s));
923 }
924 }
925
926 Ok(resulted_nodes)
927 }
928
929 #[allow(clippy::complexity)]
930 fn resolve_settings<
931 C: Compiler<Language = L>,
932 T: ArtifactOutput<CompilerContract = C::CompilerContract>,
933 >(
934 &self,
935 project: &Project<C, T>,
936 input_nodes_versions: HashMap<L, HashMap<Version, Vec<usize>>>,
937 ) -> Result<HashMap<L, HashMap<Version, HashMap<usize, Vec<usize>>>>> {
938 let mut resulted_sources = HashMap::new();
939 let mut errors = Vec::new();
940 for (language, versions) in input_nodes_versions {
941 let mut versioned_sources = HashMap::new();
942 for (version, nodes) in versions {
943 let mut profile_to_nodes = HashMap::new();
944 for idx in nodes {
945 let mut profile_candidates =
946 project.settings_profiles().enumerate().collect::<Vec<_>>();
947 if let Err(err) =
948 self.retain_compatible_profiles(idx, project, &mut profile_candidates)
949 {
950 errors.push(err);
951 } else {
952 let (profile_idx, _) = profile_candidates.first().expect("exists");
953 profile_to_nodes.entry(*profile_idx).or_insert_with(Vec::new).push(idx);
954 }
955 }
956 versioned_sources.insert(version, profile_to_nodes);
957 }
958 resulted_sources.insert(language, versioned_sources);
959 }
960
961 if errors.is_empty() {
962 Ok(resulted_sources)
963 } else {
964 let s = errors.join("\n");
965 debug!("failed to resolve settings: {s}");
966 Err(SolcError::msg(s))
967 }
968 }
969
970 fn resolve_multiple_versions(
976 all_candidates: Vec<(usize, HashSet<&CompilerVersion>)>,
977 ) -> HashMap<CompilerVersion, Vec<usize>> {
978 fn intersection<'a>(
980 mut sets: Vec<&HashSet<&'a CompilerVersion>>,
981 ) -> Vec<&'a CompilerVersion> {
982 if sets.is_empty() {
983 return Vec::new();
984 }
985
986 let mut result = sets.pop().cloned().expect("not empty; qed.");
987 if !sets.is_empty() {
988 result.retain(|item| sets.iter().all(|set| set.contains(item)));
989 }
990
991 let mut v = result.into_iter().collect::<Vec<_>>();
992 v.sort_unstable();
993 v
994 }
995
996 fn remove_candidate(candidates: &mut Vec<&CompilerVersion>) -> CompilerVersion {
1000 debug_assert!(!candidates.is_empty());
1001
1002 if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
1003 candidates.remove(pos)
1004 } else {
1005 candidates.pop().expect("not empty; qed.")
1006 }
1007 .clone()
1008 }
1009
1010 let all_sets = all_candidates.iter().map(|(_, versions)| versions).collect();
1011
1012 let mut intersection = intersection(all_sets);
1014 if !intersection.is_empty() {
1015 let exact_version = remove_candidate(&mut intersection);
1016 let all_nodes = all_candidates.into_iter().map(|(node, _)| node).collect();
1017 trace!("resolved solc version compatible with all sources \"{}\"", exact_version);
1018 return HashMap::from([(exact_version, all_nodes)]);
1019 }
1020
1021 let mut versioned_nodes: HashMap<_, _> = HashMap::new();
1023
1024 for (node, versions) in all_candidates {
1027 let mut versions = versions.into_iter().collect::<Vec<_>>();
1029 versions.sort_unstable();
1030
1031 let candidate = if let Some(idx) =
1032 versions.iter().rposition(|v| versioned_nodes.contains_key(*v))
1033 {
1034 versions.remove(idx).clone()
1036 } else {
1037 remove_candidate(&mut versions)
1039 };
1040
1041 versioned_nodes.entry(candidate).or_insert_with(|| Vec::with_capacity(1)).push(node);
1042 }
1043
1044 trace!(
1045 "no solc version can satisfy all source files, resolved multiple versions \"{:?}\"",
1046 versioned_nodes.keys()
1047 );
1048
1049 versioned_nodes
1050 }
1051}
1052
1053#[derive(Debug)]
1055pub struct NodesIter<'a, D> {
1056 stack: VecDeque<usize>,
1058 visited: HashSet<usize>,
1059 graph: &'a GraphEdges<D>,
1060}
1061
1062impl<'a, D> NodesIter<'a, D> {
1063 fn new(start: usize, graph: &'a GraphEdges<D>) -> Self {
1064 Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph }
1065 }
1066}
1067
1068impl<D> Iterator for NodesIter<'_, D> {
1069 type Item = usize;
1070 fn next(&mut self) -> Option<Self::Item> {
1071 let node = self.stack.pop_front()?;
1072
1073 if self.visited.insert(node) {
1074 self.stack.extend(self.graph.imported_nodes(node).iter().copied());
1076 }
1077 Some(node)
1078 }
1079}
1080
1081#[derive(Debug)]
1082pub struct Node<D> {
1083 path: PathBuf,
1085 source: Source,
1087 pub data: D,
1089}
1090
1091impl<D: ParsedSource> Node<D> {
1092 pub fn read(file: &Path) -> Result<Self> {
1094 let source = Source::read(file).map_err(|err| {
1095 let exists = err.path().exists();
1096 if !exists && err.path().is_symlink() {
1097 SolcError::ResolveBadSymlink(err)
1098 } else {
1099 if !exists {
1101 if let Some(existing_file) = find_case_sensitive_existing_file(file) {
1103 SolcError::ResolveCaseSensitiveFileName { error: err, existing_file }
1104 } else {
1105 SolcError::Resolve(err)
1106 }
1107 } else {
1108 SolcError::Resolve(err)
1109 }
1110 }
1111 })?;
1112 let data = D::parse(source.as_ref(), file)?;
1113 Ok(Self { path: file.to_path_buf(), source, data })
1114 }
1115
1116 pub fn path(&self) -> &Path {
1118 &self.path
1119 }
1120
1121 pub fn content(&self) -> &str {
1123 &self.source.content
1124 }
1125
1126 pub fn unpack(&self) -> (&Path, &Source) {
1127 (&self.path, &self.source)
1128 }
1129}
1130
1131pub(crate) struct DisplayNode<'a, D> {
1133 node: &'a Node<D>,
1134 root: &'a PathBuf,
1135}
1136
1137impl<D: ParsedSource> fmt::Display for DisplayNode<'_, D> {
1138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1139 let path = utils::source_name(&self.node.path, self.root);
1140 write!(f, "{}", path.display())?;
1141 if let Some(v) = self.node.data.version_req() {
1142 write!(f, " {v}")?;
1143 }
1144 Ok(())
1145 }
1146}
1147
1148#[derive(Debug, thiserror::Error)]
1150#[allow(dead_code)]
1151enum SourceVersionError {
1152 #[error("Failed to parse solidity version {0}: {1}")]
1153 InvalidVersion(String, SolcError),
1154 #[error("No solc version exists that matches the version requirement: {0}")]
1155 NoMatchingVersion(VersionReq),
1156 #[error("No solc version installed that matches the version requirement: {0}")]
1157 NoMatchingVersionOffline(VersionReq),
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162 use super::*;
1163
1164 #[test]
1165 fn can_resolve_hardhat_dependency_graph() {
1166 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample");
1167 let paths = ProjectPathsConfig::hardhat(&root).unwrap();
1168
1169 let graph = Graph::<SolData>::resolve(&paths).unwrap();
1170
1171 assert_eq!(graph.edges.num_input_files, 1);
1172 assert_eq!(graph.files().len(), 2);
1173
1174 assert_eq!(
1175 graph.files().clone(),
1176 HashMap::from([
1177 (paths.sources.join("Greeter.sol"), 0),
1178 (paths.root.join("node_modules/hardhat/console.sol"), 1),
1179 ])
1180 );
1181 }
1182
1183 #[test]
1184 fn can_resolve_dapp_dependency_graph() {
1185 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
1186 let paths = ProjectPathsConfig::dapptools(&root).unwrap();
1187
1188 let graph = Graph::<SolData>::resolve(&paths).unwrap();
1189
1190 assert_eq!(graph.edges.num_input_files, 2);
1191 assert_eq!(graph.files().len(), 3);
1192 assert_eq!(
1193 graph.files().clone(),
1194 HashMap::from([
1195 (paths.sources.join("Dapp.sol"), 0),
1196 (paths.sources.join("Dapp.t.sol"), 1),
1197 (paths.root.join("lib/ds-test/src/test.sol"), 2),
1198 ])
1199 );
1200
1201 let dapp_test = graph.node(1);
1202 assert_eq!(dapp_test.path, paths.sources.join("Dapp.t.sol"));
1203 assert_eq!(
1204 dapp_test.data.imports.iter().map(|i| i.data().path()).collect::<Vec<&Path>>(),
1205 vec![Path::new("ds-test/test.sol"), Path::new("./Dapp.sol")]
1206 );
1207 assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);
1208 }
1209
1210 #[test]
1211 #[cfg(not(target_os = "windows"))]
1212 fn can_print_dapp_sample_graph() {
1213 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
1214 let paths = ProjectPathsConfig::dapptools(&root).unwrap();
1215 let graph = Graph::<SolData>::resolve(&paths).unwrap();
1216 let mut out = Vec::<u8>::new();
1217 tree::print(&graph, &Default::default(), &mut out).unwrap();
1218
1219 assert_eq!(
1220 "
1221src/Dapp.sol >=0.6.6
1222src/Dapp.t.sol >=0.6.6
1223├── lib/ds-test/src/test.sol >=0.4.23
1224└── src/Dapp.sol >=0.6.6
1225"
1226 .trim_start()
1227 .as_bytes()
1228 .to_vec(),
1229 out
1230 );
1231 }
1232
1233 #[test]
1234 #[cfg(not(target_os = "windows"))]
1235 fn can_print_hardhat_sample_graph() {
1236 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample");
1237 let paths = ProjectPathsConfig::hardhat(&root).unwrap();
1238 let graph = Graph::<SolData>::resolve(&paths).unwrap();
1239 let mut out = Vec::<u8>::new();
1240 tree::print(&graph, &Default::default(), &mut out).unwrap();
1241 assert_eq!(
1242 "contracts/Greeter.sol >=0.6.0
1243└── node_modules/hardhat/console.sol >=0.4.22, <0.9.0
1244",
1245 String::from_utf8(out).unwrap()
1246 );
1247 }
1248
1249 #[test]
1250 #[cfg(feature = "svm-solc")]
1251 fn test_print_unresolved() {
1252 use crate::{solc::SolcCompiler, ProjectBuilder};
1253
1254 let root =
1255 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/incompatible-pragmas");
1256 let paths = ProjectPathsConfig::dapptools(&root).unwrap();
1257 let graph = Graph::<SolData>::resolve(&paths).unwrap();
1258 let Err(SolcError::Message(err)) = graph.get_input_node_versions(
1259 &ProjectBuilder::<SolcCompiler>::default()
1260 .paths(paths)
1261 .build(SolcCompiler::AutoDetect)
1262 .unwrap(),
1263 ) else {
1264 panic!("expected error");
1265 };
1266
1267 snapbox::assert_data_eq!(
1268 err,
1269 snapbox::str![[r#"
1270[37mFound incompatible versions:
1271[0m[31msrc/A.sol =0.8.25[0m imports:
1272 [37msrc/B.sol[0m
1273 [31msrc/C.sol =0.7.0[0m
1274"#]]
1275 );
1276 }
1277
1278 #[cfg(target_os = "linux")]
1279 #[test]
1280 fn can_read_different_case() {
1281 use crate::resolver::parse::SolData;
1282 use std::fs::{self, create_dir_all};
1283 use utils::tempdir;
1284
1285 let tmp_dir = tempdir("out").unwrap();
1286 let path = tmp_dir.path().join("forge-std");
1287 create_dir_all(&path).unwrap();
1288 let existing = path.join("Test.sol");
1289 let non_existing = path.join("test.sol");
1290 fs::write(
1291 existing,
1292 "
1293pragma solidity ^0.8.10;
1294contract A {}
1295 ",
1296 )
1297 .unwrap();
1298
1299 assert!(!non_existing.exists());
1300
1301 let found = crate::resolver::Node::<SolData>::read(&non_existing).unwrap_err();
1302 matches!(found, SolcError::ResolveCaseSensitiveFileName { .. });
1303 }
1304}