foundry_compilers/resolver/
mod.rs

1//! Resolution of the entire dependency graph for a project.
2//!
3//! This module implements the core logic in taking all contracts of a project and creating a
4//! resolved graph with applied remappings for all source contracts.
5//!
6//! Some constraints we're working with when resolving contracts
7//!
8//!   1. Each file can contain several source units and can have any number of imports/dependencies
9//!      (using the term interchangeably). Each dependency can declare a version range that it is
10//!      compatible with, solidity version pragma.
11//!   2. A dependency can be imported from any directory, see `Remappings`
12//!
13//! Finding all dependencies is fairly simple, we're simply doing a DFS, starting from the source
14//! contracts
15//!
16//! ## Solc version auto-detection
17//!
18//! Solving a constraint graph is an NP-hard problem. The algorithm for finding the "best" solution
19//! makes several assumptions and tries to find a version of "Solc" that is compatible with all
20//! source files.
21//!
22//! The algorithm employed here is fairly simple, we simply do a DFS over all the source files and
23//! find the set of Solc versions that the file and all its imports are compatible with, and then we
24//! try to find a single Solc version that is compatible with all the files. This is effectively the
25//! intersection of all version sets.
26//!
27//! We always try to activate the highest (installed) solc version first. Uninstalled solc is only
28//! used if this version is the only compatible version for a single file or in the intersection of
29//! all version sets.
30//!
31//! This leads to finding the optimal version, if there is one. If there is no single Solc version
32//! that is compatible with all sources and their imports, then suddenly this becomes a very
33//! difficult problem, because what would be the "best" solution. In this case, just choose the
34//! latest (installed) Solc version and try to minimize the number of Solc versions used.
35//!
36//! ## Performance
37//!
38//! Note that this is a relatively performance-critical portion of the ethers-solc preprocessing.
39//! The data that needs to be processed is proportional to the size of the dependency
40//! graph, which can, depending on the project, often be quite large.
41//!
42//! Note that, unlike the solidity compiler, we work with the filesystem, where we have to resolve
43//! remappings and follow relative paths. We're also limiting the nodes in the graph to solidity
44//! files, since we're only interested in their
45//! [version pragma](https://docs.soliditylang.org/en/develop/layout-of-source-files.html#version-pragma),
46//! which is defined on a per source file basis.
47
48use 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/// Container for result of version and profile resolution of sources contained in [`Graph`].
76#[derive(Debug)]
77pub struct ResolvedSources<'a, C: Compiler> {
78    /// Resolved set of sources.
79    ///
80    /// Mapping from language to a [`Vec`] of compiler inputs consisting of version, sources set
81    /// and settings.
82    pub sources: VersionedSources<'a, C::Language, C::Settings>,
83    /// A mapping from a source file path to the primary profile name selected for it.
84    ///
85    /// This is required because the same source file might be compiled with multiple different
86    /// profiles if it's present as a dependency for other sources. We want to keep a single name
87    /// of the profile which was chosen specifically for each source so that we can default to it.
88    /// Right now, this is used when generating artifact names, "primary" artifact will never have
89    /// a profile suffix.
90    pub primary_profiles: HashMap<PathBuf, &'a str>,
91    /// Graph edges.
92    pub edges: GraphEdges<C::ParsedSource>,
93}
94
95/// The underlying edges of the graph which only contains the raw relationship data.
96///
97/// This is kept separate from the `Graph` as the `Node`s get consumed when the `Solc` to `Sources`
98/// set is determined.
99#[derive(Debug)]
100pub struct GraphEdges<D> {
101    /// The indices of `edges` correspond to the `nodes`. That is, `edges[0]`
102    /// is the set of outgoing edges for `nodes[0]`.
103    edges: Vec<Vec<usize>>,
104    /// Reverse of `edges`. That is, `rev_edges[0]` is the set of incoming edges for `nodes[0]`.
105    rev_edges: Vec<Vec<usize>>,
106    /// index maps for a solidity file to an index, for fast lookup.
107    indices: HashMap<PathBuf, usize>,
108    /// reverse of `indices` for reverse lookup
109    rev_indices: HashMap<usize, PathBuf>,
110    /// the identified version requirement of a file
111    versions: HashMap<usize, Option<VersionReq>>,
112    /// the extracted data from the source file
113    data: HashMap<usize, D>,
114    /// with how many input files we started with, corresponds to `let input_files =
115    /// nodes[..num_input_files]`.
116    ///
117    /// Combined with the `indices` this way we can determine if a file was original added to the
118    /// graph as input or was added as resolved import, see [`Self::is_input_file()`]
119    num_input_files: usize,
120    /// tracks all imports that we failed to resolve for a file
121    unresolved_imports: HashSet<(PathBuf, PathBuf)>,
122    /// tracks additional include paths resolved by scanning all imports of the graph
123    ///
124    /// Absolute imports, like `import "src/Contract.sol"` are possible, but this does not play
125    /// nice with the standard-json import format, since the VFS won't be able to resolve
126    /// "src/Contract.sol" without help via `--include-path`
127    resolved_solc_include_paths: BTreeSet<PathBuf>,
128}
129
130impl<D> GraphEdges<D> {
131    /// How many files are source files
132    pub fn num_source_files(&self) -> usize {
133        self.num_input_files
134    }
135
136    /// Returns an iterator over all file indices
137    pub fn files(&self) -> impl Iterator<Item = usize> + '_ {
138        0..self.edges.len()
139    }
140
141    /// Returns an iterator over all source file indices
142    pub fn source_files(&self) -> impl Iterator<Item = usize> + '_ {
143        0..self.num_input_files
144    }
145
146    /// Returns an iterator over all library files
147    pub fn library_files(&self) -> impl Iterator<Item = usize> + '_ {
148        self.files().skip(self.num_input_files)
149    }
150
151    /// Returns all additional `--include-paths`
152    pub fn include_paths(&self) -> &BTreeSet<PathBuf> {
153        &self.resolved_solc_include_paths
154    }
155
156    /// Returns all imports that we failed to resolve
157    pub fn unresolved_imports(&self) -> &HashSet<(PathBuf, PathBuf)> {
158        &self.unresolved_imports
159    }
160
161    /// Returns a list of nodes the given node index points to for the given kind.
162    pub fn imported_nodes(&self, from: usize) -> &[usize] {
163        &self.edges[from]
164    }
165
166    /// Returns an iterator that yields all imports of a node and all their imports
167    pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
168        NodesIter::new(from, self).skip(1)
169    }
170
171    /// Returns all files imported by the given file
172    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    /// Returns all files that import the given file
181    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    /// Returns the id of the given file
190    pub fn node_id(&self, file: &Path) -> usize {
191        self.indices[file]
192    }
193
194    /// Returns the path of the given node
195    pub fn node_path(&self, id: usize) -> &Path {
196        &self.rev_indices[&id]
197    }
198
199    /// Returns true if the `file` was originally included when the graph was first created and not
200    /// added when all `imports` were resolved
201    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    /// Returns the `VersionReq` for the given file
210    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    /// Returns the parsed source data for the given file
215    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/// Represents a fully-resolved solidity dependency graph.
221///
222/// Each node in the graph is a file and edges represent dependencies between them.
223///
224/// See also <https://docs.soliditylang.org/en/latest/layout-of-source-files.html?highlight=import#importing-other-source-files>
225#[derive(Debug)]
226pub struct Graph<D = SolData> {
227    /// all nodes in the project, a `Node` represents a single file
228    pub nodes: Vec<Node<D>>,
229    /// relationship of the nodes
230    edges: GraphEdges<D>,
231    /// the root of the project this graph represents
232    root: PathBuf,
233}
234
235impl<L: Language, D: ParsedSource<Language = L>> Graph<D> {
236    /// Print the graph to `StdOut`
237    pub fn print(&self) {
238        self.print_with_options(Default::default())
239    }
240
241    /// Print the graph to `StdOut` using the provided `TreeOptions`
242    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    /// Returns a list of nodes the given node index points to for the given kind.
249    pub fn imported_nodes(&self, from: usize) -> &[usize] {
250        self.edges.imported_nodes(from)
251    }
252
253    /// Returns an iterator that yields all imports of a node and all their imports
254    pub fn all_imported_nodes(&self, from: usize) -> impl Iterator<Item = usize> + '_ {
255        self.edges.all_imported_nodes(from)
256    }
257
258    /// Returns `true` if the given node has any outgoing edges.
259    pub(crate) fn has_outgoing_edges(&self, index: usize) -> bool {
260        !self.edges.edges[index].is_empty()
261    }
262
263    /// Returns all the resolved files and their index in the graph.
264    pub fn files(&self) -> &HashMap<PathBuf, usize> {
265        &self.edges.indices
266    }
267
268    /// Returns `true` if the graph is empty.
269    pub fn is_empty(&self) -> bool {
270        self.nodes.is_empty()
271    }
272
273    /// Gets a node by index.
274    ///
275    /// # Panics
276    ///
277    /// if the `index` node id is not included in the graph
278    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    /// Returns an iterator that yields all nodes of the dependency tree that the given node id
287    /// spans, starting with the node itself.
288    ///
289    /// # Panics
290    ///
291    /// if the `start` node id is not included in the graph
292    pub fn node_ids(&self, start: usize) -> impl Iterator<Item = usize> + '_ {
293        NodesIter::new(start, &self.edges)
294    }
295
296    /// Same as `Self::node_ids` but returns the actual `Node`
297    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        // need to move the extracted data to the edges, essentially splitting the node so we have
304        // access to the data at a later stage in the compile pipeline
305        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    /// Consumes the `Graph`, effectively splitting the `nodes` and the `GraphEdges` off and
316    /// returning the `nodes` converted to `Sources`
317    pub fn into_sources(self) -> (Sources, GraphEdges<D>) {
318        let (sources, edges) = self.split();
319        (sources.into_iter().collect(), edges)
320    }
321
322    /// Returns an iterator that yields only those nodes that represent input files.
323    /// See `Self::resolve_sources`
324    /// This won't yield any resolved library nodes
325    pub fn input_nodes(&self) -> impl Iterator<Item = &Node<D>> {
326        self.nodes.iter().take(self.edges.num_input_files)
327    }
328
329    /// Returns all files imported by the given file
330    pub fn imports(&self, path: &Path) -> HashSet<&Path> {
331        self.edges.imports(path)
332    }
333
334    /// Resolves a number of sources within the given config
335    #[instrument(name = "Graph::resolve_sources", skip_all)]
336    pub fn resolve_sources(
337        paths: &ProjectPathsConfig<D::Language>,
338        sources: Sources,
339    ) -> Result<Self> {
340        /// checks if the given target path was already resolved, if so it adds its id to the list
341        /// of resolved imports. If it hasn't been resolved yet, it queues in the file for
342        /// processing
343        fn add_node<D: ParsedSource>(
344            unresolved: &mut VecDeque<(PathBuf, Node<D>)>,
345            index: &mut HashMap<PathBuf, usize>,
346            resolved_imports: &mut Vec<usize>,
347            target: PathBuf,
348        ) -> Result<()> {
349            if let Some(idx) = index.get(&target).copied() {
350                resolved_imports.push(idx);
351            } else {
352                // imported file is not part of the input files
353                let node = Node::read(&target)?;
354                unresolved.push_back((target.clone(), node));
355                let idx = index.len();
356                index.insert(target, idx);
357                resolved_imports.push(idx);
358            }
359            Ok(())
360        }
361
362        // we start off by reading all input files, which includes all solidity files from the
363        // source and test folder
364        let mut unresolved: VecDeque<_> = sources
365            .0
366            .into_par_iter()
367            .map(|(path, source)| {
368                let data = D::parse(source.as_ref(), &path)?;
369                Ok((path.clone(), Node { path, source, data }))
370            })
371            .collect::<Result<_>>()?;
372
373        // identifiers of all resolved files
374        let mut index: HashMap<_, _> =
375            unresolved.iter().enumerate().map(|(idx, (p, _))| (p.clone(), idx)).collect();
376
377        let num_input_files = unresolved.len();
378
379        // contains the files and their dependencies
380        let mut nodes = Vec::with_capacity(unresolved.len());
381        let mut edges = Vec::with_capacity(unresolved.len());
382        let mut rev_edges = Vec::with_capacity(unresolved.len());
383
384        // tracks additional paths that should be used with `--include-path`, these are libraries
385        // that use absolute imports like `import "src/Contract.sol"`
386        let mut resolved_solc_include_paths = BTreeSet::new();
387        resolved_solc_include_paths.insert(paths.root.clone());
388
389        // keep track of all unique paths that we failed to resolve to not spam the reporter with
390        // the same path
391        let mut unresolved_imports = HashSet::new();
392
393        // now we need to resolve all imports for the source file and those imported from other
394        // locations
395        while let Some((path, node)) = unresolved.pop_front() {
396            let mut resolved_imports = Vec::new();
397            // parent directory of the current file
398            let cwd = match path.parent() {
399                Some(inner) => inner,
400                None => continue,
401            };
402
403            for import_path in node.data.resolve_imports(paths, &mut resolved_solc_include_paths)? {
404                if let Some(err) = match paths.resolve_import_and_include_paths(
405                    cwd,
406                    &import_path,
407                    &mut resolved_solc_include_paths,
408                ) {
409                    Ok(import) => {
410                        add_node(&mut unresolved, &mut index, &mut resolved_imports, import).err()
411                    }
412                    Err(err) => Some(err),
413                } {
414                    unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));
415                    trace!("failed to resolve import component \"{:?}\" for {:?}", err, node.path)
416                }
417            }
418
419            nodes.push(node);
420            edges.push(resolved_imports);
421            // Will be populated later
422            rev_edges.push(Vec::new());
423        }
424
425        // Build `rev_edges`
426        for (idx, edges) in edges.iter().enumerate() {
427            for &edge in edges.iter() {
428                rev_edges[edge].push(idx);
429            }
430        }
431
432        if !unresolved_imports.is_empty() {
433            // notify on all unresolved imports
434            crate::report::unresolved_imports(
435                &unresolved_imports
436                    .iter()
437                    .map(|(i, f)| (i.as_path(), f.as_path()))
438                    .collect::<Vec<_>>(),
439                &paths.remappings,
440            );
441        }
442
443        let edges = GraphEdges {
444            edges,
445            rev_edges,
446            rev_indices: index.iter().map(|(k, v)| (*v, k.clone())).collect(),
447            indices: index,
448            num_input_files,
449            versions: nodes
450                .iter()
451                .enumerate()
452                .map(|(idx, node)| (idx, node.data.version_req().cloned()))
453                .collect(),
454            data: Default::default(),
455            unresolved_imports,
456            resolved_solc_include_paths,
457        };
458        Ok(Self { nodes, edges, root: paths.root.clone() })
459    }
460
461    /// Resolves the dependencies of a project's source contracts
462    pub fn resolve(paths: &ProjectPathsConfig<D::Language>) -> Result<Self> {
463        Self::resolve_sources(paths, paths.read_input_files()?)
464    }
465}
466
467impl<L: Language, D: ParsedSource<Language = L>> Graph<D> {
468    /// Consumes the nodes of the graph and returns all input files together with their appropriate
469    /// version and the edges of the graph
470    ///
471    /// First we determine the compatible version for each input file (from sources and test folder,
472    /// see `Self::resolve`) and then we add all resolved library imports.
473    pub fn into_sources_by_version<C, T>(
474        self,
475        project: &Project<C, T>,
476    ) -> Result<ResolvedSources<'_, C>>
477    where
478        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
479        C: Compiler<ParsedSource = D, Language = L>,
480    {
481        /// insert the imports of the given node into the sources map
482        /// There can be following graph:
483        /// `A(<=0.8.10) imports C(>0.4.0)` and `B(0.8.11) imports C(>0.4.0)`
484        /// where `C` is a library import, in which case we assign `C` only to the first input file.
485        /// However, it's not required to include them in the solc `CompilerInput` as they would get
486        /// picked up by solc otherwise, but we add them, so we can create a corresponding
487        /// cache entry for them as well. This can be optimized however
488        fn insert_imports(
489            idx: usize,
490            all_nodes: &mut HashMap<usize, (PathBuf, Source)>,
491            sources: &mut Sources,
492            edges: &[Vec<usize>],
493            processed_sources: &mut HashSet<usize>,
494        ) {
495            // iterate over all dependencies not processed yet
496            for dep in edges[idx].iter().copied() {
497                // keep track of processed dependencies, if the dep was already in the set we have
498                // processed it already
499                if !processed_sources.insert(dep) {
500                    continue;
501                }
502
503                // library import
504                if let Some((path, source)) = all_nodes.get(&dep).cloned() {
505                    sources.insert(path, source);
506                    insert_imports(dep, all_nodes, sources, edges, processed_sources);
507                }
508            }
509        }
510
511        let versioned_nodes = self.get_input_node_versions(project)?;
512        let versioned_nodes = self.resolve_settings(project, versioned_nodes)?;
513        let (nodes, edges) = self.split();
514
515        let mut all_nodes = nodes.into_iter().enumerate().collect::<HashMap<_, _>>();
516
517        let mut resulted_sources = HashMap::new();
518        let mut default_profiles = HashMap::new();
519
520        let profiles = project.settings_profiles().collect::<Vec<_>>();
521
522        // determine the `Sources` set for each solc version
523        for (language, versioned_nodes) in versioned_nodes {
524            let mut versioned_sources = Vec::with_capacity(versioned_nodes.len());
525
526            for (version, profile_to_nodes) in versioned_nodes {
527                for (profile_idx, input_node_indixies) in profile_to_nodes {
528                    let mut sources = Sources::new();
529
530                    // all input nodes will be processed
531                    let mut processed_sources = input_node_indixies.iter().copied().collect();
532
533                    // we only process input nodes (from sources, tests for example)
534                    for idx in input_node_indixies {
535                        // insert the input node in the sources set and remove it from the available
536                        // set
537                        let (path, source) =
538                            all_nodes.get(&idx).cloned().expect("node is preset. qed");
539
540                        default_profiles.insert(path.clone(), profiles[profile_idx].0);
541                        sources.insert(path, source);
542                        insert_imports(
543                            idx,
544                            &mut all_nodes,
545                            &mut sources,
546                            &edges.edges,
547                            &mut processed_sources,
548                        );
549                    }
550                    versioned_sources.push((version.clone(), sources, profiles[profile_idx]));
551                }
552            }
553
554            resulted_sources.insert(language, versioned_sources);
555        }
556
557        Ok(ResolvedSources { sources: resulted_sources, primary_profiles: default_profiles, edges })
558    }
559
560    /// Writes the list of imported files into the given formatter:
561    ///
562    /// ```text
563    /// path/to/a.sol (<version>) imports:
564    ///     path/to/b.sol (<version>)
565    ///     path/to/c.sol (<version>)
566    ///     ...
567    /// ```
568    fn format_imports_list<
569        C: Compiler,
570        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
571        W: std::fmt::Write,
572    >(
573        &self,
574        idx: usize,
575        incompatible: HashSet<usize>,
576        project: &Project<C, T>,
577        f: &mut W,
578    ) -> std::result::Result<(), std::fmt::Error> {
579        let format_node = |idx, f: &mut W| {
580            let node = self.node(idx);
581            let color = if incompatible.contains(&idx) { Color::Red } else { Color::White };
582
583            let mut line = utils::source_name(&node.path, &self.root).display().to_string();
584            if let Some(req) = self.version_requirement(idx, project) {
585                line.push_str(&format!(" {req}"));
586            }
587
588            write!(f, "{}", line.paint(color))
589        };
590        format_node(idx, f)?;
591        write!(f, " imports:")?;
592        for dep in self.node_ids(idx).skip(1) {
593            write!(f, "\n    ")?;
594            format_node(dep, f)?;
595        }
596
597        Ok(())
598    }
599
600    /// Combines version requirement parsed from file and from project restrictions.
601    fn version_requirement<
602        C: Compiler,
603        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
604    >(
605        &self,
606        idx: usize,
607        project: &Project<C, T>,
608    ) -> Option<VersionReq> {
609        let node = self.node(idx);
610        let parsed_req = node.data.version_req();
611        let other_req = project.restrictions.get(&node.path).and_then(|r| r.version.as_ref());
612
613        match (parsed_req, other_req) {
614            (Some(parsed_req), Some(other_req)) => {
615                let mut req = parsed_req.clone();
616                req.comparators.extend(other_req.comparators.clone());
617                Some(req)
618            }
619            (Some(parsed_req), None) => Some(parsed_req.clone()),
620            (None, Some(other_req)) => Some(other_req.clone()),
621            _ => None,
622        }
623    }
624
625    /// Checks that the file's version is even available.
626    ///
627    /// This returns an error if the file's version is invalid semver, or is not available such as
628    /// 0.8.20, if the highest available version is `0.8.19`
629    fn check_available_version<
630        C: Compiler,
631        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
632    >(
633        &self,
634        idx: usize,
635        all_versions: &[&CompilerVersion],
636        project: &Project<C, T>,
637    ) -> std::result::Result<(), SourceVersionError> {
638        let Some(req) = self.version_requirement(idx, project) else { return Ok(()) };
639
640        if !all_versions.iter().any(|v| req.matches(v.as_ref())) {
641            return if project.offline {
642                Err(SourceVersionError::NoMatchingVersionOffline(req))
643            } else {
644                Err(SourceVersionError::NoMatchingVersion(req))
645            };
646        }
647
648        Ok(())
649    }
650
651    /// Filters incompatible versions from the `candidates`. It iterates over node imports and in
652    /// case if there is no compatible version it returns the latest seen node id.
653    fn retain_compatible_versions<
654        C: Compiler,
655        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
656    >(
657        &self,
658        idx: usize,
659        candidates: &mut Vec<&CompilerVersion>,
660        project: &Project<C, T>,
661    ) -> Result<(), String> {
662        let mut all_versions = candidates.clone();
663
664        let nodes: Vec<_> = self.node_ids(idx).collect();
665        let mut failed_node_idx = None;
666        for node in nodes.iter() {
667            if let Some(req) = self.version_requirement(*node, project) {
668                candidates.retain(|v| req.matches(v.as_ref()));
669
670                if candidates.is_empty() {
671                    failed_node_idx = Some(*node);
672                    break;
673                }
674            }
675        }
676
677        let Some(failed_node_idx) = failed_node_idx else {
678            // everything is fine
679            return Ok(());
680        };
681
682        // This now keeps data for the node which were the last one before we had no candidates
683        // left. It means that there is a node directly conflicting with it in `nodes` coming
684        // before.
685        let failed_node = self.node(failed_node_idx);
686
687        if let Err(version_err) =
688            self.check_available_version(failed_node_idx, &all_versions, project)
689        {
690            // check if the version is even valid
691            let f = utils::source_name(&failed_node.path, &self.root).display();
692            return Err(format!("Encountered invalid solc version in {f}: {version_err}"));
693        } else {
694            // if the node requirement makes sense, it means that there is at least one node
695            // which requirement conflicts with it
696
697            // retain only versions compatible with the `failed_node`
698            if let Some(req) = self.version_requirement(failed_node_idx, project) {
699                all_versions.retain(|v| req.matches(v.as_ref()));
700            }
701
702            // iterate over all the nodes once again and find the one incompatible
703            for node in &nodes {
704                if self.check_available_version(*node, &all_versions, project).is_err() {
705                    let mut msg = "Found incompatible versions:\n".white().to_string();
706
707                    self.format_imports_list(
708                        idx,
709                        [*node, failed_node_idx].into(),
710                        project,
711                        &mut msg,
712                    )
713                    .unwrap();
714                    return Err(msg);
715                }
716            }
717        }
718
719        let mut msg = "Found incompatible versions:\n".white().to_string();
720        self.format_imports_list(idx, nodes.into_iter().collect(), project, &mut msg).unwrap();
721        Err(msg)
722    }
723
724    /// Filters profiles incompatible with the given node and its imports.
725    fn retain_compatible_profiles<
726        C: Compiler,
727        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
728    >(
729        &self,
730        idx: usize,
731        project: &Project<C, T>,
732        candidates: &mut Vec<(usize, (&str, &C::Settings))>,
733    ) -> Result<(), String> {
734        let mut all_profiles = candidates.clone();
735
736        let nodes: Vec<_> = self.node_ids(idx).collect();
737        let mut failed_node_idx = None;
738        for node in nodes.iter() {
739            if let Some(req) = project.restrictions.get(&self.node(*node).path) {
740                candidates.retain(|(_, (_, settings))| settings.satisfies_restrictions(&**req));
741                if candidates.is_empty() {
742                    failed_node_idx = Some(*node);
743                    break;
744                }
745            }
746        }
747
748        let Some(failed_node_idx) = failed_node_idx else {
749            // everything is fine
750            return Ok(());
751        };
752
753        let failed_node = self.node(failed_node_idx);
754
755        // retain only profiles compatible with the `failed_node`
756        if let Some(req) = project.restrictions.get(&failed_node.path) {
757            all_profiles.retain(|(_, (_, settings))| settings.satisfies_restrictions(&**req));
758        }
759
760        if all_profiles.is_empty() {
761            let f = utils::source_name(&failed_node.path, &self.root).display();
762            return Err(format!("Missing profile satisfying settings restrictions for {f}"));
763        }
764
765        // iterate over all the nodes once again and find the one incompatible
766        for node in &nodes {
767            if let Some(req) = project.restrictions.get(&self.node(*node).path) {
768                if !all_profiles
769                    .iter()
770                    .any(|(_, (_, settings))| settings.satisfies_restrictions(&**req))
771                {
772                    let mut msg = "Found incompatible settings restrictions:\n".white().to_string();
773
774                    self.format_imports_list(
775                        idx,
776                        [*node, failed_node_idx].into(),
777                        project,
778                        &mut msg,
779                    )
780                    .unwrap();
781                    return Err(msg);
782                }
783            }
784        }
785
786        let mut msg = "Found incompatible settings restrictions:\n".white().to_string();
787        self.format_imports_list(idx, nodes.into_iter().collect(), project, &mut msg).unwrap();
788        Err(msg)
789    }
790
791    fn input_nodes_by_language(&self) -> HashMap<D::Language, Vec<usize>> {
792        let mut nodes = HashMap::new();
793
794        for idx in 0..self.edges.num_input_files {
795            nodes.entry(self.nodes[idx].data.language()).or_insert_with(Vec::new).push(idx);
796        }
797
798        nodes
799    }
800
801    /// Returns a map of versions together with the input nodes that are compatible with that
802    /// version.
803    ///
804    /// This will essentially do a DFS on all input sources and their transitive imports and
805    /// checking that all can compiled with the version stated in the input file.
806    ///
807    /// Returns an error message with __all__ input files that don't have compatible imports.
808    ///
809    /// This also attempts to prefer local installations over remote available.
810    /// If `offline` is set to `true` then only already installed.
811    fn get_input_node_versions<
812        C: Compiler<Language = L>,
813        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
814    >(
815        &self,
816        project: &Project<C, T>,
817    ) -> Result<HashMap<L, HashMap<Version, Vec<usize>>>> {
818        trace!("resolving input node versions");
819
820        let mut resulted_nodes = HashMap::new();
821
822        for (language, nodes) in self.input_nodes_by_language() {
823            // this is likely called by an application and will be eventually printed so we don't
824            // exit on first error, instead gather all the errors and return a bundled
825            // error message instead
826            let mut errors = Vec::new();
827
828            // the sorted list of all versions
829            let all_versions = if project.offline {
830                project
831                    .compiler
832                    .available_versions(&language)
833                    .into_iter()
834                    .filter(|v| v.is_installed())
835                    .collect()
836            } else {
837                project.compiler.available_versions(&language)
838            };
839
840            if all_versions.is_empty() && !nodes.is_empty() {
841                return Err(SolcError::msg(format!(
842                    "Found {language} sources, but no compiler versions are available for it"
843                )));
844            }
845
846            // stores all versions and their nodes that can be compiled
847            let mut versioned_nodes = HashMap::new();
848
849            // stores all files and the versions they're compatible with
850            let mut all_candidates = Vec::with_capacity(self.edges.num_input_files);
851            // walking through the node's dep tree and filtering the versions along the way
852            for idx in nodes {
853                let mut candidates = all_versions.iter().collect::<Vec<_>>();
854                // remove all incompatible versions from the candidates list by checking the node
855                // and all its imports
856                if let Err(err) = self.retain_compatible_versions(idx, &mut candidates, project) {
857                    errors.push(err);
858                } else {
859                    // found viable candidates, pick the most recent version that's already
860                    // installed
861                    let candidate =
862                        if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
863                            candidates[pos]
864                        } else {
865                            candidates.last().expect("not empty; qed.")
866                        }
867                        .clone();
868
869                    // also store all possible candidates to optimize the set
870                    all_candidates.push((idx, candidates.into_iter().collect::<HashSet<_>>()));
871
872                    versioned_nodes
873                        .entry(candidate)
874                        .or_insert_with(|| Vec::with_capacity(1))
875                        .push(idx);
876                }
877            }
878
879            // detected multiple versions but there might still exist a single version that
880            // satisfies all sources
881            if versioned_nodes.len() > 1 {
882                versioned_nodes = Self::resolve_multiple_versions(all_candidates);
883            }
884
885            if versioned_nodes.len() == 1 {
886                trace!(
887                    "found exact solc version for all sources  \"{}\"",
888                    versioned_nodes.keys().next().unwrap()
889                );
890            }
891
892            if errors.is_empty() {
893                trace!("resolved {} versions {:?}", versioned_nodes.len(), versioned_nodes.keys());
894                resulted_nodes.insert(
895                    language,
896                    versioned_nodes
897                        .into_iter()
898                        .map(|(v, nodes)| (Version::from(v), nodes))
899                        .collect(),
900                );
901            } else {
902                let s = errors.join("\n");
903                debug!("failed to resolve versions: {s}");
904                return Err(SolcError::msg(s));
905            }
906        }
907
908        Ok(resulted_nodes)
909    }
910
911    #[allow(clippy::complexity)]
912    fn resolve_settings<
913        C: Compiler<Language = L>,
914        T: ArtifactOutput<CompilerContract = C::CompilerContract>,
915    >(
916        &self,
917        project: &Project<C, T>,
918        input_nodes_versions: HashMap<L, HashMap<Version, Vec<usize>>>,
919    ) -> Result<HashMap<L, HashMap<Version, HashMap<usize, Vec<usize>>>>> {
920        let mut resulted_sources = HashMap::new();
921        let mut errors = Vec::new();
922        for (language, versions) in input_nodes_versions {
923            let mut versioned_sources = HashMap::new();
924            for (version, nodes) in versions {
925                let mut profile_to_nodes = HashMap::new();
926                for idx in nodes {
927                    let mut profile_candidates =
928                        project.settings_profiles().enumerate().collect::<Vec<_>>();
929                    if let Err(err) =
930                        self.retain_compatible_profiles(idx, project, &mut profile_candidates)
931                    {
932                        errors.push(err);
933                    } else {
934                        let (profile_idx, _) = profile_candidates.first().expect("exists");
935                        profile_to_nodes.entry(*profile_idx).or_insert_with(Vec::new).push(idx);
936                    }
937                }
938                versioned_sources.insert(version, profile_to_nodes);
939            }
940            resulted_sources.insert(language, versioned_sources);
941        }
942
943        if errors.is_empty() {
944            Ok(resulted_sources)
945        } else {
946            let s = errors.join("\n");
947            debug!("failed to resolve settings: {s}");
948            Err(SolcError::msg(s))
949        }
950    }
951
952    /// Tries to find the "best" set of versions to nodes, See [Solc version
953    /// auto-detection](#solc-version-auto-detection)
954    ///
955    /// This is a bit inefficient but is fine, the max. number of versions is ~80 and there's
956    /// a high chance that the number of source files is <50, even for larger projects.
957    fn resolve_multiple_versions(
958        all_candidates: Vec<(usize, HashSet<&CompilerVersion>)>,
959    ) -> HashMap<CompilerVersion, Vec<usize>> {
960        // returns the intersection as sorted set of nodes
961        fn intersection<'a>(
962            mut sets: Vec<&HashSet<&'a CompilerVersion>>,
963        ) -> Vec<&'a CompilerVersion> {
964            if sets.is_empty() {
965                return Vec::new();
966            }
967
968            let mut result = sets.pop().cloned().expect("not empty; qed.");
969            if !sets.is_empty() {
970                result.retain(|item| sets.iter().all(|set| set.contains(item)));
971            }
972
973            let mut v = result.into_iter().collect::<Vec<_>>();
974            v.sort_unstable();
975            v
976        }
977
978        /// returns the highest version that is installed
979        /// if the candidates set only contains uninstalled versions then this returns the highest
980        /// uninstalled version
981        fn remove_candidate(candidates: &mut Vec<&CompilerVersion>) -> CompilerVersion {
982            debug_assert!(!candidates.is_empty());
983
984            if let Some(pos) = candidates.iter().rposition(|v| v.is_installed()) {
985                candidates.remove(pos)
986            } else {
987                candidates.pop().expect("not empty; qed.")
988            }
989            .clone()
990        }
991
992        let all_sets = all_candidates.iter().map(|(_, versions)| versions).collect();
993
994        // find all versions that satisfy all nodes
995        let mut intersection = intersection(all_sets);
996        if !intersection.is_empty() {
997            let exact_version = remove_candidate(&mut intersection);
998            let all_nodes = all_candidates.into_iter().map(|(node, _)| node).collect();
999            trace!("resolved solc version compatible with all sources  \"{}\"", exact_version);
1000            return HashMap::from([(exact_version, all_nodes)]);
1001        }
1002
1003        // no version satisfies all nodes
1004        let mut versioned_nodes: HashMap<_, _> = HashMap::new();
1005
1006        // try to minimize the set of versions, this is guaranteed to lead to `versioned_nodes.len()
1007        // > 1` as no solc version exists that can satisfy all sources
1008        for (node, versions) in all_candidates {
1009            // need to sort them again
1010            let mut versions = versions.into_iter().collect::<Vec<_>>();
1011            versions.sort_unstable();
1012
1013            let candidate = if let Some(idx) =
1014                versions.iter().rposition(|v| versioned_nodes.contains_key(*v))
1015            {
1016                // use a version that's already in the set
1017                versions.remove(idx).clone()
1018            } else {
1019                // use the highest version otherwise
1020                remove_candidate(&mut versions)
1021            };
1022
1023            versioned_nodes.entry(candidate).or_insert_with(|| Vec::with_capacity(1)).push(node);
1024        }
1025
1026        trace!(
1027            "no solc version can satisfy all source files, resolved multiple versions  \"{:?}\"",
1028            versioned_nodes.keys()
1029        );
1030
1031        versioned_nodes
1032    }
1033}
1034
1035/// An iterator over a node and its dependencies
1036#[derive(Debug)]
1037pub struct NodesIter<'a, D> {
1038    /// stack of nodes
1039    stack: VecDeque<usize>,
1040    visited: HashSet<usize>,
1041    graph: &'a GraphEdges<D>,
1042}
1043
1044impl<'a, D> NodesIter<'a, D> {
1045    fn new(start: usize, graph: &'a GraphEdges<D>) -> Self {
1046        Self { stack: VecDeque::from([start]), visited: HashSet::new(), graph }
1047    }
1048}
1049
1050impl<D> Iterator for NodesIter<'_, D> {
1051    type Item = usize;
1052    fn next(&mut self) -> Option<Self::Item> {
1053        let node = self.stack.pop_front()?;
1054
1055        if self.visited.insert(node) {
1056            // push the node's direct dependencies to the stack if we haven't visited it already
1057            self.stack.extend(self.graph.imported_nodes(node).iter().copied());
1058        }
1059        Some(node)
1060    }
1061}
1062
1063#[derive(Debug)]
1064pub struct Node<D> {
1065    /// path of the solidity  file
1066    path: PathBuf,
1067    /// content of the solidity file
1068    source: Source,
1069    /// parsed data
1070    pub data: D,
1071}
1072
1073impl<D: ParsedSource> Node<D> {
1074    /// Reads the content of the file and returns a [Node] containing relevant information
1075    pub fn read(file: &Path) -> Result<Self> {
1076        let source = Source::read(file).map_err(|err| {
1077            let exists = err.path().exists();
1078            if !exists && err.path().is_symlink() {
1079                SolcError::ResolveBadSymlink(err)
1080            } else {
1081                // This is an additional check useful on OS that have case-sensitive paths, See also <https://docs.soliditylang.org/en/v0.8.17/path-resolution.html#import-callback>
1082                if !exists {
1083                    // check if there exists a file with different case
1084                    if let Some(existing_file) = find_case_sensitive_existing_file(file) {
1085                        SolcError::ResolveCaseSensitiveFileName { error: err, existing_file }
1086                    } else {
1087                        SolcError::Resolve(err)
1088                    }
1089                } else {
1090                    SolcError::Resolve(err)
1091                }
1092            }
1093        })?;
1094        let data = D::parse(source.as_ref(), file)?;
1095        Ok(Self { path: file.to_path_buf(), source, data })
1096    }
1097
1098    /// Returns the path of the file.
1099    pub fn path(&self) -> &Path {
1100        &self.path
1101    }
1102
1103    /// Returns the contents of the file.
1104    pub fn content(&self) -> &str {
1105        &self.source.content
1106    }
1107
1108    pub fn unpack(&self) -> (&Path, &Source) {
1109        (&self.path, &self.source)
1110    }
1111}
1112
1113/// Helper type for formatting a node
1114pub(crate) struct DisplayNode<'a, D> {
1115    node: &'a Node<D>,
1116    root: &'a PathBuf,
1117}
1118
1119impl<D: ParsedSource> fmt::Display for DisplayNode<'_, D> {
1120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1121        let path = utils::source_name(&self.node.path, self.root);
1122        write!(f, "{}", path.display())?;
1123        if let Some(v) = self.node.data.version_req() {
1124            write!(f, " {v}")?;
1125        }
1126        Ok(())
1127    }
1128}
1129
1130/// Errors thrown when checking the solc version of a file
1131#[derive(Debug, thiserror::Error)]
1132#[allow(dead_code)]
1133enum SourceVersionError {
1134    #[error("Failed to parse solidity version {0}: {1}")]
1135    InvalidVersion(String, SolcError),
1136    #[error("No solc version exists that matches the version requirement: {0}")]
1137    NoMatchingVersion(VersionReq),
1138    #[error("No solc version installed that matches the version requirement: {0}")]
1139    NoMatchingVersionOffline(VersionReq),
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144    use super::*;
1145
1146    #[test]
1147    fn can_resolve_hardhat_dependency_graph() {
1148        let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample");
1149        let paths = ProjectPathsConfig::hardhat(&root).unwrap();
1150
1151        let graph = Graph::<SolData>::resolve(&paths).unwrap();
1152
1153        assert_eq!(graph.edges.num_input_files, 1);
1154        assert_eq!(graph.files().len(), 2);
1155
1156        assert_eq!(
1157            graph.files().clone(),
1158            HashMap::from([
1159                (paths.sources.join("Greeter.sol"), 0),
1160                (paths.root.join("node_modules/hardhat/console.sol"), 1),
1161            ])
1162        );
1163    }
1164
1165    #[test]
1166    fn can_resolve_dapp_dependency_graph() {
1167        let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
1168        let paths = ProjectPathsConfig::dapptools(&root).unwrap();
1169
1170        let graph = Graph::<SolData>::resolve(&paths).unwrap();
1171
1172        assert_eq!(graph.edges.num_input_files, 2);
1173        assert_eq!(graph.files().len(), 3);
1174        assert_eq!(
1175            graph.files().clone(),
1176            HashMap::from([
1177                (paths.sources.join("Dapp.sol"), 0),
1178                (paths.sources.join("Dapp.t.sol"), 1),
1179                (paths.root.join("lib/ds-test/src/test.sol"), 2),
1180            ])
1181        );
1182
1183        let dapp_test = graph.node(1);
1184        assert_eq!(dapp_test.path, paths.sources.join("Dapp.t.sol"));
1185        assert_eq!(
1186            dapp_test.data.imports.iter().map(|i| i.data().path()).collect::<Vec<&Path>>(),
1187            vec![Path::new("ds-test/test.sol"), Path::new("./Dapp.sol")]
1188        );
1189        assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);
1190    }
1191
1192    #[test]
1193    #[cfg(not(target_os = "windows"))]
1194    fn can_print_dapp_sample_graph() {
1195        let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/dapp-sample");
1196        let paths = ProjectPathsConfig::dapptools(&root).unwrap();
1197        let graph = Graph::<SolData>::resolve(&paths).unwrap();
1198        let mut out = Vec::<u8>::new();
1199        tree::print(&graph, &Default::default(), &mut out).unwrap();
1200
1201        assert_eq!(
1202            "
1203src/Dapp.sol >=0.6.6
1204src/Dapp.t.sol >=0.6.6
1205├── lib/ds-test/src/test.sol >=0.4.23
1206└── src/Dapp.sol >=0.6.6
1207"
1208            .trim_start()
1209            .as_bytes()
1210            .to_vec(),
1211            out
1212        );
1213    }
1214
1215    #[test]
1216    #[cfg(not(target_os = "windows"))]
1217    fn can_print_hardhat_sample_graph() {
1218        let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/hardhat-sample");
1219        let paths = ProjectPathsConfig::hardhat(&root).unwrap();
1220        let graph = Graph::<SolData>::resolve(&paths).unwrap();
1221        let mut out = Vec::<u8>::new();
1222        tree::print(&graph, &Default::default(), &mut out).unwrap();
1223        assert_eq!(
1224            "contracts/Greeter.sol >=0.6.0
1225└── node_modules/hardhat/console.sol >=0.4.22, <0.9.0
1226",
1227            String::from_utf8(out).unwrap()
1228        );
1229    }
1230
1231    #[test]
1232    #[cfg(feature = "svm-solc")]
1233    fn test_print_unresolved() {
1234        use crate::{solc::SolcCompiler, ProjectBuilder};
1235
1236        let root =
1237            Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test-data/incompatible-pragmas");
1238        let paths = ProjectPathsConfig::dapptools(&root).unwrap();
1239        let graph = Graph::<SolData>::resolve(&paths).unwrap();
1240        let Err(SolcError::Message(err)) = graph.get_input_node_versions(
1241            &ProjectBuilder::<SolcCompiler>::default()
1242                .paths(paths)
1243                .build(SolcCompiler::AutoDetect)
1244                .unwrap(),
1245        ) else {
1246            panic!("expected error");
1247        };
1248
1249        snapbox::assert_data_eq!(
1250            err,
1251            snapbox::str![[r#"
1252Found incompatible versions:
1253src/A.sol =0.8.25 imports:
1254    src/B.sol
1255    src/C.sol =0.7.0
1256"#]]
1257        );
1258    }
1259
1260    #[cfg(target_os = "linux")]
1261    #[test]
1262    fn can_read_different_case() {
1263        use crate::resolver::parse::SolData;
1264        use std::fs::{self, create_dir_all};
1265        use utils::tempdir;
1266
1267        let tmp_dir = tempdir("out").unwrap();
1268        let path = tmp_dir.path().join("forge-std");
1269        create_dir_all(&path).unwrap();
1270        let existing = path.join("Test.sol");
1271        let non_existing = path.join("test.sol");
1272        fs::write(
1273            existing,
1274            "
1275pragma solidity ^0.8.10;
1276contract A {}
1277        ",
1278        )
1279        .unwrap();
1280
1281        assert!(!non_existing.exists());
1282
1283        let found = crate::resolver::Node::<SolData>::read(&non_existing).unwrap_err();
1284        matches!(found, SolcError::ResolveCaseSensitiveFileName { .. });
1285    }
1286}