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;
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: Vec<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 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::warn!(
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                    f.path
116                        .canonicalize()
117                        .ok()
118                        .map(|canonical| (canonical, f.id))
119                })
120                .collect()
121        });
122        map.get(canonical).copied()
123    }
124}
125
126#[cfg(all(test, not(miri)))]
127mod tests {
128    use super::*;
129    use fallow_types::discover::DiscoveredFile;
130    use std::path::PathBuf;
131
132    #[test]
133    fn canonical_fallback_returns_none_for_empty_files() {
134        let files: Vec<DiscoveredFile> = vec![];
135        let fallback = CanonicalFallback::new(&files);
136        assert!(fallback.get(Path::new("/nonexistent")).is_none());
137    }
138
139    #[test]
140    fn canonical_fallback_finds_existing_file() {
141        let temp = std::env::temp_dir().join("fallow-test-canonical-fallback");
142        let _ = std::fs::create_dir_all(&temp);
143        let test_file = temp.join("test.ts");
144        std::fs::write(&test_file, "").unwrap();
145
146        let files = vec![DiscoveredFile {
147            id: FileId(42),
148            path: test_file.clone(),
149            size_bytes: 0,
150        }];
151        let fallback = CanonicalFallback::new(&files);
152
153        let canonical = test_file.canonicalize().unwrap();
154        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
155
156        // Second call uses cached map (OnceLock)
157        assert_eq!(fallback.get(&canonical), Some(FileId(42)));
158
159        let _ = std::fs::remove_dir_all(&temp);
160    }
161
162    #[test]
163    fn canonical_fallback_returns_none_for_missing_path() {
164        let temp = std::env::temp_dir().join("fallow-test-canonical-miss");
165        let _ = std::fs::create_dir_all(&temp);
166        let test_file = temp.join("exists.ts");
167        std::fs::write(&test_file, "").unwrap();
168
169        let files = vec![DiscoveredFile {
170            id: FileId(1),
171            path: test_file,
172            size_bytes: 0,
173        }];
174        let fallback = CanonicalFallback::new(&files);
175        assert!(fallback.get(Path::new("/nonexistent/file.ts")).is_none());
176
177        let _ = std::fs::remove_dir_all(&temp);
178    }
179}
180
181/// Known output directory names that may appear in exports map targets.
182/// When an exports map points to `./dist/utils.js`, we try replacing these
183/// prefixes with `src/` (the conventional source directory) to find the tracked
184/// source file.
185pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
186
187/// Source extensions to try when mapping a built output file back to source.
188pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
189
190/// React Native platform extension prefixes.
191/// Metro resolves platform-specific files (e.g., `./foo` -> `./foo.web.tsx` on web).
192pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];