Skip to main content

hearth_graph/resolve/
mod.rs

1//! Module resolution abstractions and language-specific dispatch.
2
3use compact_str::CompactString;
4
5use crate::imports::{ImportKind, RawImport};
6
7/// The result of classifying an import specifier.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum Resolved {
10    /// An absolute path to a workspace file.
11    Path(CompactString),
12    /// A dependency outside the workspace, such as an npm package or Rust crate.
13    ///
14    /// For JavaScript, if symlink canonicalization moves a package into a store
15    /// path without a `node_modules` component, the residual classification is
16    /// [`Self::Path`].
17    External(CompactString),
18    /// A specifier that could not be resolved.
19    Unresolved(UnresolvedReason),
20}
21
22/// Why an import could not be resolved.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum UnresolvedReason {
25    /// No matching path or package was found.
26    NotFound,
27    /// No resolver supports this import kind.
28    Unsupported,
29    /// Resolution could not be completed reliably.
30    Failed {
31        /// Broad category suitable for programmatic handling.
32        kind: FailedKind,
33        /// Resolver-specific diagnostic detail.
34        detail: CompactString,
35    },
36}
37
38/// Broad category for a failed resolution.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum FailedKind {
41    /// A configuration file was missing, malformed, or internally inconsistent.
42    Config,
43    /// Filesystem access failed.
44    Io,
45    /// The import or referrer specifier was invalid.
46    InvalidSpecifier,
47    /// A resolver failure that does not fit a more specific category.
48    Other,
49}
50
51/// Whether a resolution outcome covers every relevant resolution path.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ResolutionCompleteness {
54    /// The resolver fully modeled this import.
55    Complete,
56    /// The resolver returned a best-effort result that may omit another target.
57    Partial,
58}
59
60/// A resolution result and the filesystem paths consulted to produce it.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ResolutionOutcome {
63    /// The classified resolution.
64    pub resolved: Resolved,
65    /// Absolute paths of found and missing dependencies consulted during resolution.
66    ///
67    /// JavaScript outcomes retain both the configured root tsconfig-format file
68    /// and the config selected for an importing file, then follow the selected
69    /// config's `extends` chain. Traversing every project reference is out of
70    /// scope for v1.
71    pub dependencies: Vec<CompactString>,
72    /// Non-fatal observations made while collecting resolution dependencies.
73    pub notes: Vec<CompactString>,
74    /// Whether the resolver fully modeled every relevant resolution path.
75    pub completeness: ResolutionCompleteness,
76}
77
78/// A type-erased module resolver.
79pub trait Resolve: Send + Sync {
80    /// Baseline completeness for files handled by this resolver.
81    ///
82    /// This applies even when import extraction yields no imports, so a
83    /// best-effort resolver cannot become exact through an empty fold.
84    fn baseline_completeness(&self) -> ResolutionCompleteness {
85        ResolutionCompleteness::Complete
86    }
87
88    /// Resolve an import relative to its importing file.
89    ///
90    /// `from_file` must be an absolute path. Relative inputs return
91    /// [`UnresolvedReason::Failed`].
92    fn resolve(&self, from_file: &str, import: &RawImport) -> ResolutionOutcome;
93
94    /// Discard all cached filesystem and configuration state.
95    ///
96    /// This call must not overlap an in-flight [`Self::resolve`], as required by
97    /// `oxc_resolver`. The Hearth adapter guarantees exclusion through
98    /// single-flight sweeps.
99    fn clear_cache(&self);
100}
101
102/// Resolvers available for each supported language family.
103#[derive(Default)]
104pub struct ResolverSet {
105    /// Resolver for JavaScript and TypeScript imports.
106    pub js: Option<Box<dyn Resolve>>,
107    /// Resolver for Rust imports.
108    pub rust: Option<Box<dyn Resolve>>,
109}
110
111impl ResolverSet {
112    /// Return the baseline completeness for a registered language name.
113    #[must_use]
114    pub fn baseline_completeness(&self, language: &str) -> ResolutionCompleteness {
115        let resolver = match language {
116            "rust" => self.rust.as_deref(),
117            "typescript" | "tsx" | "javascript" | "jsx" | "vue" => self.js.as_deref(),
118            _ => None,
119        };
120        resolver.map_or(
121            ResolutionCompleteness::Complete,
122            Resolve::baseline_completeness,
123        )
124    }
125
126    /// Dispatch an import to its language-specific resolver.
127    pub fn resolve(&self, from_file: &str, import: &RawImport) -> ResolutionOutcome {
128        let resolver = match import.kind {
129            ImportKind::RustUse | ImportKind::RustMod => self.rust.as_deref(),
130            _ => self.js.as_deref(),
131        };
132
133        resolver.map_or_else(unsupported, |resolver| resolver.resolve(from_file, import))
134    }
135
136    /// Clear every configured resolver cache.
137    pub fn clear_cache(&self) {
138        if let Some(resolver) = &self.js {
139            resolver.clear_cache();
140        }
141        if let Some(resolver) = &self.rust {
142            resolver.clear_cache();
143        }
144    }
145}
146
147fn unsupported() -> ResolutionOutcome {
148    ResolutionOutcome {
149        resolved: Resolved::Unresolved(UnresolvedReason::Unsupported),
150        dependencies: Vec::new(),
151        notes: Vec::new(),
152        // Graph guarantees already degrade when import extraction is unsupported
153        // or no matching resolver is live.
154        completeness: ResolutionCompleteness::Complete,
155    }
156}
157
158#[cfg(feature = "resolve-js")]
159pub mod js;
160#[cfg(feature = "resolve-rust")]
161pub mod rust;