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;