Skip to main content

fallow_graph/resolve/
types.rs

1//! Type definitions and constants for import resolution.
2
3use std::path::{Path, PathBuf};
4use std::sync::Mutex;
5
6use oxc_resolver::Resolver;
7use rustc_hash::{FxHashMap, FxHashSet};
8
9use fallow_types::discover::FileId;
10
11/// Result of resolving an import specifier.
12#[derive(Debug, Clone)]
13pub enum ResolveResult {
14    /// Resolved to a file within the project.
15    InternalModule(FileId),
16    /// Resolved to a file outside the project (`node_modules`, `.json`, etc.).
17    ExternalFile(PathBuf),
18    /// Bare specifier — an npm package.
19    NpmPackage(String),
20    /// Could not resolve.
21    Unresolvable(String),
22}
23
24/// A resolved import with its target.
25#[derive(Debug, Clone)]
26pub struct ResolvedImport {
27    /// The original import information.
28    pub info: fallow_types::extract::ImportInfo,
29    /// Where the import resolved to.
30    pub target: ResolveResult,
31}
32
33/// A resolved re-export with its target.
34#[derive(Debug, Clone)]
35pub struct ResolvedReExport {
36    /// The original re-export information.
37    pub info: fallow_types::extract::ReExportInfo,
38    /// Where the re-export source resolved to.
39    pub target: ResolveResult,
40}
41
42/// Fully resolved module with all imports mapped to targets.
43#[derive(Debug)]
44pub struct ResolvedModule {
45    /// Unique file identifier.
46    pub file_id: FileId,
47    /// Absolute path to the module file.
48    pub path: PathBuf,
49    /// All export declarations in this module.
50    pub exports: Vec<fallow_types::extract::ExportInfo>,
51    /// All re-exports with resolved targets.
52    pub re_exports: Vec<ResolvedReExport>,
53    /// All static imports with resolved targets.
54    pub resolved_imports: Vec<ResolvedImport>,
55    /// All dynamic imports with resolved targets.
56    pub resolved_dynamic_imports: Vec<ResolvedImport>,
57    /// Dynamic import patterns matched against discovered files.
58    pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
59    /// Static member accesses (e.g., `Status.Active`).
60    pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
61    /// Identifiers used as whole objects (Object.values, for..in, spread, etc.).
62    pub whole_object_uses: Vec<String>,
63    /// Whether this module uses `CommonJS` exports.
64    pub has_cjs_exports: bool,
65    /// Whether this module declares at least one Angular `@Component({
66    /// templateUrl: ... })` decorator. Mirrors `ModuleInfo.has_angular_component_template_url`;
67    /// see that field for the contract this gate enforces.
68    pub has_angular_component_template_url: bool,
69    /// Local names of import bindings that are never referenced in this file.
70    pub unused_import_bindings: FxHashSet<String>,
71    /// Local import bindings referenced from type positions.
72    pub type_referenced_import_bindings: Vec<String>,
73    /// Local import bindings referenced from runtime/value positions.
74    pub value_referenced_import_bindings: Vec<String>,
75    /// Namespace-import aliases re-exported through an object literal.
76    /// See `fallow_types::extract::NamespaceObjectAlias` for the shape.
77    pub namespace_object_aliases: Vec<fallow_types::extract::NamespaceObjectAlias>,
78}
79
80impl Default for ResolvedModule {
81    fn default() -> Self {
82        Self {
83            file_id: FileId(0),
84            path: PathBuf::new(),
85            exports: vec![],
86            re_exports: vec![],
87            resolved_imports: vec![],
88            resolved_dynamic_imports: vec![],
89            resolved_dynamic_patterns: vec![],
90            member_accesses: vec![],
91            whole_object_uses: vec![],
92            has_cjs_exports: false,
93            has_angular_component_template_url: false,
94            unused_import_bindings: FxHashSet::default(),
95            type_referenced_import_bindings: vec![],
96            value_referenced_import_bindings: vec![],
97            namespace_object_aliases: vec![],
98        }
99    }
100}
101
102impl ResolvedModule {
103    /// Iterate over all concrete resolved imports in source order buckets.
104    ///
105    /// Includes static `import`/`require` edges and literal dynamic `import()`
106    /// edges. Dynamic import patterns are intentionally excluded because they
107    /// resolve to sets of files rather than single import specifiers.
108    pub fn all_resolved_imports(&self) -> impl Iterator<Item = &ResolvedImport> {
109        self.resolved_imports
110            .iter()
111            .chain(self.resolved_dynamic_imports.iter())
112    }
113}
114
115/// Shared context for resolving import specifiers.
116///
117/// Groups the immutable lookup tables and caches that are shared across all
118/// `resolve_specifier` calls within a single `resolve_all_imports` invocation.
119pub(super) struct ResolveContext<'a> {
120    /// The oxc_resolver instance (configured once, shared across threads).
121    pub resolver: &'a Resolver,
122    /// CSS-only resolver with the package.json `style` condition enabled.
123    /// Used only for stylesheet package subpaths so JS/TS imports do not
124    /// accidentally prefer CSS export branches.
125    pub style_resolver: &'a Resolver,
126    /// Ordered extension list used by the resolver.
127    pub extensions: &'a [String],
128    /// Canonical path → FileId lookup (raw paths when root is canonical).
129    pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
130    /// Raw (non-canonical) path → FileId lookup.
131    pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
132    /// Workspace name → canonical root path.
133    pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
134    /// Plugin-provided path aliases (prefix, replacement).
135    pub path_aliases: &'a [(String, String)],
136    /// Absolute directories to search when resolving bare SCSS/Sass
137    /// `@import` / `@use` specifiers. Populated from Angular's
138    /// `stylePreprocessorOptions.includePaths` and equivalent settings.
139    pub scss_include_paths: &'a [PathBuf],
140    /// Project root directory.
141    pub root: &'a Path,
142    /// Lazy canonical path → FileId fallback for intra-project symlinks.
143    /// Only initialized on first miss when root is canonical. `None` when
144    /// path_to_id already uses canonical paths (root is not canonical).
145    pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
146    /// Dedup set for broken-tsconfig warnings. Emits one `tracing::warn!`
147    /// per unique error message instead of spamming the log with one
148    /// warning per affected file. Shared across all parallel resolver
149    /// threads via `Mutex`. Empty and unused when no tsconfig errors occur.
150    pub tsconfig_warned: &'a Mutex<FxHashSet<String>>,
151}
152
153/// Thread-safe lazy canonical path index, built on first access.
154pub(super) struct CanonicalFallback<'a> {
155    files: &'a [fallow_types::discover::DiscoveredFile],
156    map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
157}
158
159impl<'a> CanonicalFallback<'a> {
160    pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
161        Self {
162            files,
163            map: std::sync::OnceLock::new(),
164        }
165    }
166
167    /// Look up a canonical path, lazily building the index on first call.
168    pub fn get(&self, canonical: &Path) -> Option<FileId> {
169        let map = self.map.get_or_init(|| {
170            tracing::debug!(
171                "intra-project symlinks detected, building canonical path index ({} files)",
172                self.files.len()
173            );
174            self.files
175                .iter()
176                .filter_map(|f| {
177                    dunce::canonicalize(&f.path)
178                        .ok()
179                        .map(|canonical| (canonical, f.id))
180                })
181                .collect()
182        });
183        map.get(canonical).copied()
184    }
185}
186
187#[cfg(all(test, not(miri)))]
188mod tests {
189    use super::*;
190    use fallow_types::discover::DiscoveredFile;
191
192    #[test]
193    fn canonical_fallback_returns_none_for_empty_files() {
194        let files: Vec<DiscoveredFile> = vec![];
195        let fallback = CanonicalFallback::new(&files);
196        assert!(fallback.get(Path::new("/nonexistent")).is_none());
197    }
198
199    #[test]
200    fn canonical_fallback_finds_existing_file() {
201        let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
202        let _ = std::fs::create_dir_all(&temp);
203        let test_file = temp.join("test.ts");
204        std::fs::write(&test_file, "").unwrap();
205
206        let files = vec![DiscoveredFile {
207            id: FileId(42),
208            path: test_file.clone(),
209            size_bytes: 0,
210        }];
211        let fallback = CanonicalFallback::new(&files);
212
213        let canonical = dunce::canonicalize(&test_file).unwrap();
214        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
215
216        // Second call uses cached map (OnceLock)
217        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
218
219        let _ = std::fs::remove_dir_all(&temp);
220    }
221
222    #[test]
223    fn canonical_fallback_returns_none_for_missing_path() {
224        let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
225        let _ = std::fs::create_dir_all(&temp);
226        let test_file = temp.join("exists.ts");
227        std::fs::write(&test_file, "").unwrap();
228
229        let files = vec![DiscoveredFile {
230            id: FileId(1),
231            path: test_file,
232            size_bytes: 0,
233        }];
234        let fallback = CanonicalFallback::new(&files);
235        assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
236
237        let _ = std::fs::remove_dir_all(&temp);
238    }
239}
240
241/// Known output directory names that may appear in exports map targets.
242/// When an exports map points to `./dist/utils.js`, we try replacing these
243/// prefixes with `src/` (the conventional source directory) to find the tracked
244/// source file.
245pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
246
247/// Source extensions to try when mapping a built output file back to source.
248pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
249
250/// React Native platform extension prefixes.
251/// Metro resolves platform-specific files (e.g., `./foo` -> `./foo.web.tsx` on web).
252pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];