fallow_graph/resolve/
types.rs1use std::path::{Path, PathBuf};
4
5use oxc_resolver::Resolver;
6use rustc_hash::{FxHashMap, FxHashSet};
7
8use fallow_types::discover::FileId;
9
10#[derive(Debug, Clone)]
12pub enum ResolveResult {
13 InternalModule(FileId),
15 ExternalFile(PathBuf),
17 NpmPackage(String),
19 Unresolvable(String),
21}
22
23#[derive(Debug, Clone)]
25pub struct ResolvedImport {
26 pub info: fallow_types::extract::ImportInfo,
28 pub target: ResolveResult,
30}
31
32#[derive(Debug, Clone)]
34pub struct ResolvedReExport {
35 pub info: fallow_types::extract::ReExportInfo,
37 pub target: ResolveResult,
39}
40
41#[derive(Debug)]
43pub struct ResolvedModule {
44 pub file_id: FileId,
46 pub path: PathBuf,
48 pub exports: Vec<fallow_types::extract::ExportInfo>,
50 pub re_exports: Vec<ResolvedReExport>,
52 pub resolved_imports: Vec<ResolvedImport>,
54 pub resolved_dynamic_imports: Vec<ResolvedImport>,
56 pub resolved_dynamic_patterns: Vec<(fallow_types::extract::DynamicImportPattern, Vec<FileId>)>,
58 pub member_accesses: Vec<fallow_types::extract::MemberAccess>,
60 pub whole_object_uses: Vec<String>,
62 pub has_cjs_exports: bool,
64 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
86pub(super) struct ResolveContext<'a> {
91 pub resolver: &'a Resolver,
93 pub path_to_id: &'a FxHashMap<&'a Path, FileId>,
95 pub raw_path_to_id: &'a FxHashMap<&'a Path, FileId>,
97 pub workspace_roots: &'a FxHashMap<&'a str, &'a Path>,
99 pub path_aliases: &'a [(String, String)],
101 pub root: &'a Path,
103 pub canonical_fallback: Option<&'a CanonicalFallback<'a>>,
107}
108
109pub(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 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 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
197pub const OUTPUT_DIRS: &[&str] = &["dist", "build", "out", "esm", "cjs"];
202
203pub const SOURCE_EXTS: &[&str] = &["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"];
205
206pub const RN_PLATFORM_PREFIXES: &[&str] = &[".web", ".ios", ".android", ".native"];