1use std::fs;
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6
7use crate::cache::{self, CacheWriteHandle, ParseCache};
8use crate::error::Error;
9use crate::graph::ModuleGraph;
10use crate::lang::{self, LanguageSupport};
11use crate::walker;
12
13#[derive(Debug)]
15pub struct LoadedGraph {
16 pub graph: ModuleGraph,
17 pub root: PathBuf,
19 pub entry: PathBuf,
21 pub valid_extensions: &'static [&'static str],
23 pub from_cache: bool,
25 pub unresolvable_dynamic_count: usize,
27 pub unresolvable_dynamic_files: Vec<(PathBuf, usize)>,
29 pub file_warnings: Vec<String>,
31}
32
33#[must_use = "the CacheWriteHandle joins a background thread on drop"]
41pub fn load_graph(entry: &Path, no_cache: bool) -> Result<(LoadedGraph, CacheWriteHandle), Error> {
42 let entry = entry
43 .canonicalize()
44 .map_err(|e| Error::EntryNotFound(entry.to_path_buf(), e))?;
45
46 if entry.is_dir() {
47 return Err(Error::EntryIsDirectory(entry));
48 }
49
50 let (root, kind) = lang::detect_project(&entry).ok_or_else(|| {
51 let ext = entry.extension().and_then(|e| e.to_str()).map(String::from);
52 Error::UnsupportedFileType(ext)
53 })?;
54
55 let lang_support: Box<dyn LanguageSupport> = match kind {
56 lang::ProjectKind::TypeScript => Box::new(lang::typescript::TypeScriptSupport::new(&root)),
57 lang::ProjectKind::Python => Box::new(lang::python::PythonSupport::new(&root)),
58 };
59
60 let valid_extensions = lang_support.extensions();
61 let (result, handle) = build_or_load(&entry, &root, no_cache, lang_support.as_ref());
62
63 Ok((
64 LoadedGraph {
65 graph: result.graph,
66 root,
67 entry,
68 valid_extensions,
69 from_cache: result.from_cache,
70 unresolvable_dynamic_count: result.unresolvable_dynamic_count,
71 unresolvable_dynamic_files: result.unresolvable_dynamic_files,
72 file_warnings: result.file_warnings,
73 },
74 handle,
75 ))
76}
77
78struct BuildResult {
83 graph: ModuleGraph,
84 unresolvable_dynamic_count: usize,
85 unresolvable_dynamic_files: Vec<(PathBuf, usize)>,
86 file_warnings: Vec<String>,
87 from_cache: bool,
88}
89
90fn build_or_load(
91 entry: &Path,
92 root: &Path,
93 no_cache: bool,
94 lang: &dyn LanguageSupport,
95) -> (BuildResult, CacheWriteHandle) {
96 let mut cache = if no_cache {
97 ParseCache::new()
98 } else {
99 ParseCache::load(root)
100 };
101
102 if !no_cache {
104 let resolve_fn = |spec: &str| lang.resolve(root, spec).is_some();
105 match cache.try_load_graph(entry, &resolve_fn) {
106 cache::GraphCacheResult::Hit {
107 graph,
108 unresolvable_dynamic,
109 unresolvable_dynamic_files,
110 unresolved_specifiers,
111 needs_resave,
112 } => {
113 let handle = if needs_resave {
114 cache.save(
115 root,
116 entry,
117 &graph,
118 unresolved_specifiers,
119 unresolvable_dynamic,
120 unresolvable_dynamic_files.clone(),
121 )
122 } else {
123 CacheWriteHandle::none()
124 };
125 return (
126 BuildResult {
127 graph,
128 unresolvable_dynamic_count: unresolvable_dynamic,
129 unresolvable_dynamic_files,
130 file_warnings: Vec::new(),
131 from_cache: true,
132 },
133 handle,
134 );
135 }
136 cache::GraphCacheResult::Stale {
137 mut graph,
138 unresolvable_dynamic,
139 unresolvable_dynamic_files,
140 changed_files,
141 } => {
142 if let Some(result) = try_incremental_update(
145 &mut cache,
146 &mut graph,
147 &changed_files,
148 unresolvable_dynamic,
149 unresolvable_dynamic_files,
150 lang,
151 ) {
152 graph.compute_package_info();
153 let handle = cache.save_incremental(
154 root,
155 entry,
156 &graph,
157 &changed_files,
158 result.unresolvable_dynamic,
159 result.unresolvable_dynamic_files.clone(),
160 );
161 return (
162 BuildResult {
163 graph,
164 unresolvable_dynamic_count: result.unresolvable_dynamic,
165 unresolvable_dynamic_files: result.unresolvable_dynamic_files,
166 file_warnings: Vec::new(),
167 from_cache: true,
168 },
169 handle,
170 );
171 }
172 }
174 cache::GraphCacheResult::Miss => {}
175 }
176 }
177
178 let result = walker::build_graph(entry, root, lang, &mut cache);
180 let unresolvable_count: usize = result.unresolvable_dynamic.iter().map(|(_, c)| c).sum();
181 let handle = cache.save(
182 root,
183 entry,
184 &result.graph,
185 result.unresolved_specifiers,
186 unresolvable_count,
187 result.unresolvable_dynamic.clone(),
188 );
189 (
190 BuildResult {
191 graph: result.graph,
192 unresolvable_dynamic_count: unresolvable_count,
193 unresolvable_dynamic_files: result.unresolvable_dynamic,
194 file_warnings: result.file_warnings,
195 from_cache: false,
196 },
197 handle,
198 )
199}
200
201struct IncrementalResult {
202 unresolvable_dynamic: usize,
203 unresolvable_dynamic_files: Vec<(PathBuf, usize)>,
204}
205
206#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
210fn try_incremental_update(
211 cache: &mut ParseCache,
212 graph: &mut ModuleGraph,
213 changed_files: &[PathBuf],
214 old_unresolvable_total: usize,
215 mut unresolvable_files: Vec<(PathBuf, usize)>,
216 lang: &dyn LanguageSupport,
217) -> Option<IncrementalResult> {
218 let mut unresolvable_delta: isize = 0;
219
220 for path in changed_files {
221 let old_result = cache.lookup_unchecked(path)?;
223 let old_import_count = old_result.imports.len();
224 let old_unresolvable = old_result.unresolvable_dynamic;
225 let old_imports: Vec<_> = old_result
226 .imports
227 .iter()
228 .map(|i| (i.specifier.as_str(), i.kind))
229 .collect();
230
231 let source = std::fs::read_to_string(path).ok()?;
233 let new_result = lang.parse(path, &source).ok()?;
234
235 if new_result.imports.len() != old_import_count
237 || new_result.imports.iter().zip(old_imports.iter()).any(
238 |(new, &(old_spec, old_kind))| new.specifier != old_spec || new.kind != old_kind,
239 )
240 {
241 return None;
242 }
243
244 unresolvable_delta += new_result.unresolvable_dynamic as isize - old_unresolvable as isize;
246
247 unresolvable_files.retain(|(p, _)| p != path);
249 if new_result.unresolvable_dynamic > 0 {
250 unresolvable_files.push((path.clone(), new_result.unresolvable_dynamic));
251 }
252
253 let mid = *graph.path_to_id.get(path)?;
255 let new_size = source.len() as u64;
256 graph.modules[mid.0 as usize].size_bytes = new_size;
257
258 #[allow(clippy::or_fun_call)]
260 let dir = path.parent().unwrap_or(Path::new("."));
261 let resolved_paths: Vec<Option<PathBuf>> = new_result
262 .imports
263 .iter()
264 .map(|imp| lang.resolve(dir, &imp.specifier))
265 .collect();
266 if let Ok(meta) = fs::metadata(path) {
267 let mtime = meta
268 .modified()
269 .ok()
270 .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
271 .map(|d: std::time::Duration| d.as_nanos());
272 if let Some(mtime) = mtime {
273 cache.insert(path.clone(), new_size, mtime, new_result, resolved_paths);
274 }
275 }
276 }
277
278 let new_total = (old_unresolvable_total as isize + unresolvable_delta).max(0) as usize;
279 debug_assert_eq!(
280 new_total,
281 unresolvable_files.iter().map(|(_, c)| c).sum::<usize>(),
282 "unresolvable_dynamic total drifted from per-file sum"
283 );
284 Some(IncrementalResult {
285 unresolvable_dynamic: new_total,
286 unresolvable_dynamic_files: unresolvable_files,
287 })
288}