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}