Skip to main content

fallow_graph/resolve/
mod.rs

1//! Import specifier resolution using `oxc_resolver`.
2//!
3//! Resolves all import specifiers across all modules in parallel, mapping each to
4//! an internal file, npm package, or unresolvable target. Includes support for
5//! tsconfig path aliases, pnpm virtual store paths, React Native platform extensions,
6//! and dynamic import pattern matching via glob.
7
8mod cache;
9pub(crate) mod fallbacks;
10mod path_info;
11mod react_native;
12mod specifier;
13mod types;
14
15pub use path_info::{extract_package_name, is_path_alias};
16pub use types::{ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport};
17
18use std::path::{Path, PathBuf};
19
20use rayon::prelude::*;
21use rustc_hash::FxHashMap;
22
23use fallow_types::discover::{DiscoveredFile, FileId};
24use fallow_types::extract::{ImportInfo, ModuleInfo};
25
26use cache::BareSpecifierCache;
27use fallbacks::make_glob_from_pattern;
28use specifier::{create_resolver, resolve_specifier};
29use types::ResolveContext;
30
31/// Resolve all imports across all modules in parallel.
32pub fn resolve_all_imports(
33    modules: &[ModuleInfo],
34    files: &[DiscoveredFile],
35    workspaces: &[fallow_config::WorkspaceInfo],
36    active_plugins: &[String],
37    path_aliases: &[(String, String)],
38    root: &Path,
39) -> Vec<ResolvedModule> {
40    // Build workspace name → root index for pnpm store fallback.
41    // Canonicalize roots to match path_to_id (which uses canonical paths).
42    // Without this, macOS /var → /private/var and similar platform symlinks
43    // cause workspace roots to mismatch canonical file paths.
44    let canonical_ws_roots: Vec<PathBuf> = workspaces
45        .par_iter()
46        .map(|ws| ws.root.canonicalize().unwrap_or_else(|_| ws.root.clone()))
47        .collect();
48    let workspace_roots: FxHashMap<&str, &Path> = workspaces
49        .iter()
50        .zip(canonical_ws_roots.iter())
51        .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
52        .collect();
53
54    // Pre-compute canonical paths ONCE for all files in parallel (avoiding repeated syscalls).
55    // Each canonicalize() is a syscall — parallelizing over rayon reduces wall time.
56    let canonical_paths: Vec<PathBuf> = files
57        .par_iter()
58        .map(|f| f.path.canonicalize().unwrap_or_else(|_| f.path.clone()))
59        .collect();
60
61    // Build path -> FileId index using pre-computed canonical paths
62    let path_to_id: FxHashMap<&Path, FileId> = canonical_paths
63        .iter()
64        .enumerate()
65        .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
66        .collect();
67
68    // Also index by non-canonical path for fallback lookups
69    let raw_path_to_id: FxHashMap<&Path, FileId> =
70        files.iter().map(|f| (f.path.as_path(), f.id)).collect();
71
72    // FileIds are sequential 0..n, so direct array indexing is faster than FxHashMap.
73    let file_paths: Vec<&Path> = files.iter().map(|f| f.path.as_path()).collect();
74
75    // Create resolver ONCE and share across threads (oxc_resolver::Resolver is Send + Sync)
76    let resolver = create_resolver(active_plugins);
77
78    // Cache for bare specifier resolutions (e.g., `react`, `lodash/merge`)
79    let bare_cache = BareSpecifierCache::new();
80
81    // Shared resolution context — avoids passing 7 arguments to every resolve_specifier call
82    let ctx = ResolveContext {
83        resolver: &resolver,
84        path_to_id: &path_to_id,
85        raw_path_to_id: &raw_path_to_id,
86        bare_cache: &bare_cache,
87        workspace_roots: &workspace_roots,
88        path_aliases,
89        root,
90    };
91
92    // Resolve in parallel — shared resolver instance
93    modules
94        .par_iter()
95        .filter_map(|module| {
96            let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
97                tracing::warn!(
98                    file_id = module.file_id.0,
99                    "Skipping module with unknown file_id during resolution"
100                );
101                return None;
102            };
103
104            let resolved_imports: Vec<ResolvedImport> = module
105                .imports
106                .iter()
107                .map(|imp| ResolvedImport {
108                    info: imp.clone(),
109                    target: resolve_specifier(&ctx, file_path, &imp.source),
110                })
111                .collect();
112
113            let resolved_dynamic_imports: Vec<ResolvedImport> = module
114                .dynamic_imports
115                .iter()
116                .flat_map(|imp| {
117                    let target = resolve_specifier(&ctx, file_path, &imp.source);
118                    if !imp.destructured_names.is_empty() {
119                        // `const { a, b } = await import('./x')` → Named imports
120                        imp.destructured_names
121                            .iter()
122                            .map(|name| ResolvedImport {
123                                info: ImportInfo {
124                                    source: imp.source.clone(),
125                                    imported_name: fallow_types::extract::ImportedName::Named(
126                                        name.clone(),
127                                    ),
128                                    local_name: name.clone(),
129                                    is_type_only: false,
130                                    span: imp.span,
131                                },
132                                target: target.clone(),
133                            })
134                            .collect()
135                    } else if imp.local_name.is_some() {
136                        // `const mod = await import('./x')` → Namespace with local_name
137                        vec![ResolvedImport {
138                            info: ImportInfo {
139                                source: imp.source.clone(),
140                                imported_name: fallow_types::extract::ImportedName::Namespace,
141                                local_name: imp.local_name.clone().unwrap_or_default(),
142                                is_type_only: false,
143                                span: imp.span,
144                            },
145                            target,
146                        }]
147                    } else {
148                        // Side-effect only: `await import('./x')` with no assignment
149                        vec![ResolvedImport {
150                            info: ImportInfo {
151                                source: imp.source.clone(),
152                                imported_name: fallow_types::extract::ImportedName::SideEffect,
153                                local_name: String::new(),
154                                is_type_only: false,
155                                span: imp.span,
156                            },
157                            target,
158                        }]
159                    }
160                })
161                .collect();
162
163            let re_exports: Vec<ResolvedReExport> = module
164                .re_exports
165                .iter()
166                .map(|re| ResolvedReExport {
167                    info: re.clone(),
168                    target: resolve_specifier(&ctx, file_path, &re.source),
169                })
170                .collect();
171
172            // Also resolve require() calls.
173            // Destructured requires → Named imports; others → Namespace (conservative).
174            let require_imports: Vec<ResolvedImport> = module
175                .require_calls
176                .iter()
177                .flat_map(|req| {
178                    let target = resolve_specifier(&ctx, file_path, &req.source);
179                    if req.destructured_names.is_empty() {
180                        vec![ResolvedImport {
181                            info: ImportInfo {
182                                source: req.source.clone(),
183                                imported_name: fallow_types::extract::ImportedName::Namespace,
184                                local_name: req.local_name.clone().unwrap_or_default(),
185                                is_type_only: false,
186                                span: req.span,
187                            },
188                            target,
189                        }]
190                    } else {
191                        req.destructured_names
192                            .iter()
193                            .map(|name| ResolvedImport {
194                                info: ImportInfo {
195                                    source: req.source.clone(),
196                                    imported_name: fallow_types::extract::ImportedName::Named(
197                                        name.clone(),
198                                    ),
199                                    local_name: name.clone(),
200                                    is_type_only: false,
201                                    span: req.span,
202                                },
203                                target: target.clone(),
204                            })
205                            .collect()
206                    }
207                })
208                .collect();
209
210            let mut all_imports = resolved_imports;
211            all_imports.extend(require_imports);
212
213            // Resolve dynamic import patterns via glob matching against discovered files.
214            // Use pre-computed canonical paths (no syscalls in inner loop).
215            let from_dir = canonical_paths
216                .get(module.file_id.0 as usize)
217                .and_then(|p| p.parent())
218                .unwrap_or(file_path);
219            let resolved_dynamic_patterns: Vec<(
220                fallow_types::extract::DynamicImportPattern,
221                Vec<FileId>,
222            )> = module
223                .dynamic_import_patterns
224                .iter()
225                .filter_map(|pattern| {
226                    let glob_str = make_glob_from_pattern(pattern);
227                    let matcher = globset::Glob::new(&glob_str)
228                        .ok()
229                        .map(|g| g.compile_matcher())?;
230                    let matched: Vec<FileId> = canonical_paths
231                        .iter()
232                        .enumerate()
233                        .filter(|(_idx, canonical)| {
234                            canonical.strip_prefix(from_dir).is_ok_and(|relative| {
235                                let rel_str = format!("./{}", relative.to_string_lossy());
236                                matcher.is_match(&rel_str)
237                            })
238                        })
239                        .map(|(idx, _)| files[idx].id)
240                        .collect();
241                    if matched.is_empty() {
242                        None
243                    } else {
244                        Some((pattern.clone(), matched))
245                    }
246                })
247                .collect();
248
249            Some(ResolvedModule {
250                file_id: module.file_id,
251                path: file_path.to_path_buf(),
252                exports: module.exports.clone(),
253                re_exports,
254                resolved_imports: all_imports,
255                resolved_dynamic_imports,
256                resolved_dynamic_patterns,
257                member_accesses: module.member_accesses.clone(),
258                whole_object_uses: module.whole_object_uses.clone(),
259                has_cjs_exports: module.has_cjs_exports,
260                unused_import_bindings: module.unused_import_bindings.clone(),
261            })
262        })
263        .collect()
264}