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
68impl Default for ResolvedModule {
69    fn default() -> Self {
70        Self {
71            file_id: FileId(0),
72            path: PathBuf::new(),
73            exports: vec![],
74            re_exports: vec![],
75            resolved_imports: vec![],
76            resolved_dynamic_imports: vec![],
77            resolved_dynamic_patterns: vec![],
78            member_accesses: vec![],
79            whole_object_uses: vec![],
80            has_cjs_exports: false,
81            unused_import_bindings: FxHashSet::default(),
82        }
83    }
84}
85
86/// Shared context for resolving import specifiers.
87///
88/// Groups the immutable lookup tables and caches that are shared across all
89/// `resolve_specifier` calls within a single `resolve_all_imports` invocation.
90pub(super) struct ResolveContext<'a> {
91    /// The oxc_resolver instance (configured once, shared across threads).
92    pub resolver: &'a Resolver,
93    /// Canonical path → FileId lookup (raw paths when root is canonical).
94    pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
95    /// Raw (non-canonical) path → FileId lookup.
96    pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
97    /// Workspace name → canonical root path.
98    pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
99    /// Plugin-provided path aliases (prefix, replacement).
100    pub path_aliases: &'a [(String, String)],
101    /// Project root directory.
102    pub root: &'a Path,
103    /// Lazy canonical path → FileId fallback for intra-project symlinks.
104    /// Only initialized on first miss when root is canonical. `None` when
105    /// path_to_id already uses canonical paths (root is not canonical).
106    pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
107}
108
109/// Thread-safe lazy canonical path index, built on first access.
110pub(super) struct CanonicalFallback<'a> {
111    files: &'a [fallow_types::discover::DiscoveredFile],
112    map: std::sync::OnceLock<FxHashMap<std::path::PathBuf, FileId>>,
113}
114
115impl<'a> CanonicalFallback<'a> {
116    pub const fn new(files: &'a [fallow_types::discover::DiscoveredFile]) -> Self {
117        Self {
118            files,
119            map: std::sync::OnceLock::new(),
120        }
121    }
122
123    /// Look up a canonical path, lazily building the index on first call.
124    pub fn get(&self, canonical: &Path) -> Option<FileId> {
125        let map = self.map.get_or_init(|| {
126            tracing::debug!(
127                "intra-project symlinks detected — building canonical path index ({} files)",
128                self.files.len()
129            );
130            self.files
131                .iter()
132                .filter_map(|f| {
133                    dunce::canonicalize(&f.path)
134                        .ok()
135                        .map(|canonical| (canonical, f.id))
136                })
137                .collect()
138        });
139        map.get(canonical).copied()
140    }
141}
142
143#[cfg(all(test, not(miri)))]
144mod tests {
145    use super::*;
146    use fallow_types::discover::DiscoveredFile;
147
148    #[test]
149    fn canonical_fallback_returns_none_for_empty_files() {
150        let files: Vec<DiscoveredFile> = vec![];
151        let fallback = CanonicalFallback::new(&files);
152        assert!(fallback.get(Path::new("/nonexistent")).is_none());
153    }
154
155    #[test]
156    fn canonical_fallback_finds_existing_file() {
157        let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
158        let _ = std::fs::create_dir_all(&temp);
159        let test_file = temp.join("test.ts");
160        std::fs::write(&test_file, "").unwrap();
161
162        let files = vec![DiscoveredFile {
163            id: FileId(42),
164            path: test_file.clone(),
165            size_bytes: 0,
166        }];
167        let fallback = CanonicalFallback::new(&files);
168
169        let canonical = dunce::canonicalize(&test_file).unwrap();
170        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
171
172        // Second call uses cached map (OnceLock)
173        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
174
175        let _ = std::fs::remove_dir_all(&temp);
176    }
177
178    #[test]
179    fn canonical_fallback_returns_none_for_missing_path() {
180        let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
181        let _ = std::fs::create_dir_all(&temp);
182        let test_file = temp.join("exists.ts");
183        std::fs::write(&test_file, "").unwrap();
184
185        let files = vec![DiscoveredFile {
186            id: FileId(1),
187            path: test_file,
188            size_bytes: 0,
189        }];
190        let fallback = CanonicalFallback::new(&files);
191        assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
192
193        let _ = std::fs::remove_dir_all(&temp);
194    }
195}
196
197/// Known output directory names that may appear in exports map targets.
198/// When an exports map points to `./dist/utils.js`, we try replacing these
199/// prefixes with `src/` (the conventional source directory) to find the tracked
200/// source file.
201pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
202
203/// Source extensions to try when mapping a built output file back to source.
204pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
205
206/// React Native platform extension prefixes.
207/// Metro resolves platform-specific files (e.g., `./foo` -> `./foo.web.tsx` on web).
208pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];