Skip to main content

fallow_graph/resolve/
types.rs

1//! Type definitions and constants for import resolution.
2
3use std::path::{Path, PathBuf};
4
5use oxc_resolver::Resolver;
6use rustc_hash::{FxHashMap, FxHashSet};
7
8use fallow_types::discover::FileId;
9
10/// Result of resolving an import specifier.
11#[derive(Debug, Clone)]
12pub enum ResolveResult {
13    /// Resolved to a file within the project.
14    InternalModule(FileId),
15    /// Resolved to a file outside the project (`node_modules`, `.json`, etc.).
16    ExternalFile(PathBuf),
17    /// Bare specifier — an npm package.
18    NpmPackage(String),
19    /// Could not resolve.
20    Unresolvable(String),
21}
22
23/// A resolved import with its target.
24#[derive(Debug, Clone)]
25pub struct ResolvedImport {
26    /// The original import information.
27    pub info: fallow_types::extract::ImportInfo,
28    /// Where the import resolved to.
29    pub target: ResolveResult,
30}
31
32/// A resolved re-export with its target.
33#[derive(Debug, Clone)]
34pub struct ResolvedReExport {
35    /// The original re-export information.
36    pub info: fallow_types::extract::ReExportInfo,
37    /// Where the re-export source resolved to.
38    pub target: ResolveResult,
39}
40
41/// Fully resolved module with all imports mapped to targets.
42#[derive(Debug)]
43pub struct ResolvedModule {
44    /// Unique file identifier.
45    pub file_id: FileId,
46    /// Absolute path to the module file.
47    pub path: PathBuf,
48    /// All export declarations in this module.
49    pub exports: Vec<fallow_types::extract::ExportInfo>,
50    /// All re-exports with resolved targets.
51    pub re_exports: Vec<ResolvedReExport>,
52    /// All static imports with resolved targets.
53    pub resolved_imports: Vec<ResolvedImport>,
54    /// All dynamic imports with resolved targets.
55    pub resolved_dynamic_imports: Vec<ResolvedImport>,
56    /// Dynamic import patterns matched against discovered files.
57    pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
58    /// Static member accesses (e.g., `Status.Active`).
59    pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
60    /// Identifiers used as whole objects (Object.values, for..in, spread, etc.).
61    pub whole_object_uses: Vec<String>,
62    /// Whether this module uses `CommonJS` exports.
63    pub has_cjs_exports: bool,
64    /// Local names of import bindings that are never referenced in this file.
65    pub unused_import_bindings: FxHashSet<String>,
66}
67
68/// Shared context for resolving import specifiers.
69///
70/// Groups the immutable lookup tables and caches that are shared across all
71/// `resolve_specifier` calls within a single `resolve_all_imports` invocation.
72pub(super) struct ResolveContext<'a> {
73    /// The oxc_resolver instance (configured once, shared across threads).
74    pub resolver: &'a Resolver,
75    /// Canonical path → FileId lookup (raw paths when root is canonical).
76    pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
77    /// Raw (non-canonical) path → FileId lookup.
78    pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
79    /// Workspace name → canonical root path.
80    pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
81    /// Plugin-provided path aliases (prefix, replacement).
82    pub path_aliases: &'a [(String, String)],
83    /// Project root directory.
84    pub root: &'a Path,
85    /// Lazy canonical path → FileId fallback for intra-project symlinks.
86    /// Only initialized on first miss when root is canonical. `None` when
87    /// path_to_id already uses canonical paths (root is not canonical).
88    pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
89}
90
91/// Thread-safe lazy canonical path index, built on first access.
92pub(super) struct CanonicalFallback<'a> {
93    files: &'a [fallow_types::discover::DiscoveredFile],
94    map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
95}
96
97impl<'a> CanonicalFallback<'a> {
98    pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
99        Self {
100            files,
101            map: std::sync::OnceLock::new(),
102        }
103    }
104
105    /// Look up a canonical path, lazily building the index on first call.
106    pub fn get(&self, canonical: &Path) -> Option<FileId> {
107        let map = self.map.get_or_init(|| {
108            tracing::debug!(
109                "intra-project symlinks detected — building canonical path index ({} files)",
110                self.files.len()
111            );
112            self.files
113                .iter()
114                .filter_map(|f| {
115                    dunce::canonicalize(&f.path)
116                        .ok()
117                        .map(|canonical| (canonical, f.id))
118                })
119                .collect()
120        });
121        map.get(canonical).copied()
122    }
123}
124
125#[cfg(all(test, not(miri)))]
126mod tests {
127    use super::*;
128    use fallow_types::discover::DiscoveredFile;
129
130    #[test]
131    fn canonical_fallback_returns_none_for_empty_files() {
132        let files: Vec<DiscoveredFile> = vec![];
133        let fallback = CanonicalFallback::new(&files);
134        assert!(fallback.get(Path::new("/nonexistent")).is_none());
135    }
136
137    #[test]
138    fn canonical_fallback_finds_existing_file() {
139        let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
140        let _ = std::fs::create_dir_all(&temp);
141        let test_file = temp.join("test.ts");
142        std::fs::write(&test_file, "").unwrap();
143
144        let files = vec![DiscoveredFile {
145            id: FileId(42),
146            path: test_file.clone(),
147            size_bytes: 0,
148        }];
149        let fallback = CanonicalFallback::new(&files);
150
151        let canonical = dunce::canonicalize(&test_file).unwrap();
152        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
153
154        // Second call uses cached map (OnceLock)
155        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
156
157        let _ = std::fs::remove_dir_all(&temp);
158    }
159
160    #[test]
161    fn canonical_fallback_returns_none_for_missing_path() {
162        let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
163        let _ = std::fs::create_dir_all(&temp);
164        let test_file = temp.join("exists.ts");
165        std::fs::write(&test_file, "").unwrap();
166
167        let files = vec![DiscoveredFile {
168            id: FileId(1),
169            path: test_file,
170            size_bytes: 0,
171        }];
172        let fallback = CanonicalFallback::new(&files);
173        assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
174
175        let _ = std::fs::remove_dir_all(&temp);
176    }
177}
178
179/// Known output directory names that may appear in exports map targets.
180/// When an exports map points to `./dist/utils.js`, we try replacing these
181/// prefixes with `src/` (the conventional source directory) to find the tracked
182/// source file.
183pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
184
185/// Source extensions to try when mapping a built output file back to source.
186pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
187
188/// React Native platform extension prefixes.
189/// Metro resolves platform-specific files (e.g., `./foo` -> `./foo.web.tsx` on web).
190pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];