1use std::path::{Path, PathBuf};
11
12use rustc_hash::{FxHashMap, FxHashSet};
13
14use fallow_config::{AutoImportKind, AutoImportRule};
15use fallow_types::discover::FileId;
16use fallow_types::extract::{ImportedName, ModuleInfo, ReExportInfo};
17
18use super::types::{ResolveResult, ResolvedImport, ResolvedModule};
19use super::{is_auto_import_builtin, synthetic_auto_import_info};
20
21const AUTO_IMPORT_VIRTUAL_MODULES: &[&str] = &[COMPONENTS_MODULE, IMPORTS_MODULE];
27const COMPONENTS_MODULE: &str = "#components";
28const IMPORTS_MODULE: &str = "#imports";
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct UnreadableAutoImportRead {
34 pub file_id: FileId,
36 pub module: &'static str,
38}
39
40struct AutoImportTarget<'a> {
42 file_id: FileId,
43 kind: AutoImportKind,
44 scope: Option<Vec<&'a Path>>,
47}
48
49type AutoImportTable<'a> = FxHashMap<&'a str, Vec<AutoImportTarget<'a>>>;
50
51#[derive(Default)]
53struct VirtualModuleReads<'a> {
54 names: Vec<&'a str>,
56 unreadable: Vec<UnreadableAutoImportRead>,
59}
60
61pub(super) fn synthesize_auto_import_edges(
78 resolved: &mut [ResolvedModule],
79 modules: &[ModuleInfo],
80 auto_imports: &[AutoImportRule],
81 path_to_id: &FxHashMap<&Path, FileId>,
82 raw_path_to_id: &FxHashMap<&Path, FileId>,
83) {
84 if auto_imports.is_empty() {
85 return;
86 }
87
88 let mut table: AutoImportTable<'_> = FxHashMap::default();
89 for rule in auto_imports {
90 let source = rule.source.as_path();
91 let Some(file_id) = raw_path_to_id
92 .get(source)
93 .or_else(|| path_to_id.get(source))
94 .copied()
95 else {
96 continue;
97 };
98 add_table_target(&mut table, rule, file_id);
99 }
100 if table.is_empty() {
101 return;
102 }
103
104 let virtual_reads: Vec<(usize, Vec<String>, Vec<&'static str>)> =
105 collect_virtual_module_reads(resolved)
106 .into_iter()
107 .filter_map(|(index, reads)| {
108 let names: Vec<String> = reads.names.into_iter().map(str::to_owned).collect();
109 let mut modules: Vec<&'static str> =
110 reads.unreadable.iter().map(|read| read.module).collect();
111 modules.sort_unstable();
112 modules.dedup();
113 (!names.is_empty() || !modules.is_empty()).then_some((index, names, modules))
114 })
115 .collect();
116 let mut sorted_names: Vec<&str> = table.keys().copied().collect();
117 sorted_names.sort_unstable();
118
119 let candidates: FxHashMap<FileId, &[String]> = modules
120 .iter()
121 .filter(|module| !module.auto_import_candidates.is_empty())
122 .map(|module| (module.file_id, module.auto_import_candidates.as_slice()))
123 .collect();
124 for module in resolved.iter_mut() {
125 if let Some(names) = candidates.get(&module.file_id) {
126 for name in *names {
127 credit_auto_import_name(module, name, &table, None);
128 }
129 }
130 }
131 for (index, names, modules) in virtual_reads {
132 let module = &mut resolved[index];
133 for name in &names {
134 credit_auto_import_name(module, name, &table, None);
135 }
136 for virtual_module in modules {
137 for name in &sorted_names {
138 credit_auto_import_name(module, name, &table, Some(virtual_module));
139 }
140 }
141 }
142}
143
144fn add_table_target<'a>(
148 table: &mut AutoImportTable<'a>,
149 rule: &'a AutoImportRule,
150 file_id: FileId,
151) {
152 let targets = table.entry(rule.name.as_str()).or_default();
153 let rule_scope: Option<Vec<&Path>> =
154 (!rule.scope.is_empty()).then(|| rule.scope.iter().map(PathBuf::as_path).collect());
155 let Some(target) = targets
156 .iter_mut()
157 .find(|target| target.file_id == file_id && target.kind == rule.kind)
158 else {
159 targets.push(AutoImportTarget {
160 file_id,
161 kind: rule.kind,
162 scope: rule_scope,
163 });
164 return;
165 };
166 match (&mut target.scope, rule_scope) {
167 (Some(scope), Some(extra)) => {
168 for root in extra {
169 if !scope.contains(&root) {
170 scope.push(root);
171 }
172 }
173 }
174 (scope, _) => *scope = None,
175 }
176}
177
178#[must_use]
182pub fn unreadable_auto_import_reads(resolved: &[ResolvedModule]) -> Vec<UnreadableAutoImportRead> {
183 let mut reads: Vec<UnreadableAutoImportRead> = collect_virtual_module_reads(resolved)
184 .into_iter()
185 .flat_map(|(_, reads)| reads.unreadable)
186 .collect();
187 reads.sort_unstable_by(|a, b| a.file_id.0.cmp(&b.file_id.0).then(a.module.cmp(b.module)));
188 reads.dedup();
189 reads
190}
191
192fn collect_virtual_module_reads(
195 resolved: &[ResolvedModule],
196) -> Vec<(usize, VirtualModuleReads<'_>)> {
197 let forwards_star = resolved.iter().any(|module| {
198 module
199 .re_exports
200 .iter()
201 .any(|re| is_star_from_virtual_module(&re.info))
202 });
203 let importers = forwards_star.then(|| ImporterIndex::build(resolved));
204
205 resolved
206 .iter()
207 .enumerate()
208 .filter_map(|(index, module)| {
209 let reads = virtual_module_reads(module, resolved, importers.as_ref());
210 (!reads.names.is_empty() || !reads.unreadable.is_empty()).then_some((index, reads))
211 })
212 .collect()
213}
214
215fn virtual_module_reads<'a>(
220 module: &'a ResolvedModule,
221 resolved: &'a [ResolvedModule],
222 importers: Option<&ImporterIndex>,
223) -> VirtualModuleReads<'a> {
224 let mut reads = VirtualModuleReads::default();
225 for import in &module.resolved_imports {
226 let Some(virtual_module) = auto_import_virtual_module(&import.info.source) else {
227 continue;
228 };
229 match &import.info.imported_name {
230 ImportedName::Named(name) => reads.names.push(name.as_str()),
231 ImportedName::Namespace => {
232 read_namespace(module, &import.info.local_name, virtual_module, &mut reads);
233 }
234 ImportedName::Default | ImportedName::SideEffect => {}
235 }
236 }
237 for re_export in &module.re_exports {
238 let Some(virtual_module) = auto_import_virtual_module(&re_export.info.source) else {
239 continue;
240 };
241 if re_export.info.imported_name != "*" {
242 reads.names.push(re_export.info.imported_name.as_str());
243 } else if re_export.info.exported_name == "*" {
244 if let Some(importers) = importers {
245 let mut visited = FxHashSet::default();
246 forwarded_reads(
247 module,
248 virtual_module,
249 resolved,
250 importers,
251 &mut visited,
252 &mut reads,
253 );
254 }
255 } else {
256 reads.unreadable.push(UnreadableAutoImportRead {
257 file_id: module.file_id,
258 module: virtual_module,
259 });
260 }
261 }
262 reads
263}
264
265fn read_namespace<'a>(
268 module: &'a ResolvedModule,
269 local: &str,
270 virtual_module: &'static str,
271 reads: &mut VirtualModuleReads<'a>,
272) {
273 if module.unused_import_bindings.contains(local) {
274 return;
275 }
276 let mut accessed = false;
277 for access in module.member_accesses.iter() {
278 if access.object == local {
279 reads.names.push(access.member.as_str());
280 accessed = true;
281 }
282 }
283 let whole = module.whole_object_uses.iter().any(|name| name == local)
284 || module
285 .exports
286 .iter()
287 .any(|export| export.local_name.as_deref() == Some(local));
288 if whole || !accessed {
289 reads.unreadable.push(UnreadableAutoImportRead {
290 file_id: module.file_id,
291 module: virtual_module,
292 });
293 }
294}
295
296fn forwarded_reads<'a>(
301 forwarder: &ResolvedModule,
302 virtual_module: &'static str,
303 resolved: &'a [ResolvedModule],
304 importers: &ImporterIndex,
305 visited: &mut FxHashSet<FileId>,
306 reads: &mut VirtualModuleReads<'a>,
307) {
308 if !visited.insert(forwarder.file_id) {
309 return;
310 }
311 for &index in importers.of(forwarder.file_id) {
312 let importer = &resolved[index];
313 let edges = importer
314 .resolved_imports
315 .iter()
316 .chain(&importer.resolved_dynamic_imports)
317 .filter(|import| imports_file(import, forwarder.file_id));
318 for import in edges {
319 match &import.info.imported_name {
320 ImportedName::Named(name) => reads.names.push(name.as_str()),
321 ImportedName::Namespace => {
322 let mut importer_reads = VirtualModuleReads::default();
323 read_namespace(
324 importer,
325 &import.info.local_name,
326 virtual_module,
327 &mut importer_reads,
328 );
329 reads.names.extend(importer_reads.names);
330 reads.unreadable.extend(importer_reads.unreadable);
331 }
332 ImportedName::Default | ImportedName::SideEffect => {}
333 }
334 }
335 for re_export in &importer.re_exports {
336 if re_export.target.internal_file_id() != Some(forwarder.file_id) {
337 continue;
338 }
339 if re_export.info.imported_name != "*" {
340 reads.names.push(re_export.info.imported_name.as_str());
341 } else if re_export.info.exported_name == "*" {
342 forwarded_reads(
343 importer,
344 virtual_module,
345 resolved,
346 importers,
347 visited,
348 reads,
349 );
350 } else {
351 reads.unreadable.push(UnreadableAutoImportRead {
352 file_id: importer.file_id,
353 module: virtual_module,
354 });
355 }
356 }
357 }
358}
359
360fn imports_file(import: &ResolvedImport, file_id: FileId) -> bool {
362 !import.target.is_synthetic_auto_import() && import.target.internal_file_id() == Some(file_id)
363}
364
365struct ImporterIndex {
367 importers: FxHashMap<FileId, Vec<usize>>,
368}
369
370impl ImporterIndex {
371 fn build(resolved: &[ResolvedModule]) -> Self {
372 let mut importers: FxHashMap<FileId, Vec<usize>> = FxHashMap::default();
373 for (index, module) in resolved.iter().enumerate() {
374 let targets = module
375 .resolved_imports
376 .iter()
377 .chain(&module.resolved_dynamic_imports)
378 .filter(|import| !import.target.is_synthetic_auto_import())
379 .map(|import| &import.target)
380 .chain(module.re_exports.iter().map(|re| &re.target))
381 .filter_map(ResolveResult::internal_file_id);
382 for target in targets {
383 let entry = importers.entry(target).or_default();
384 if entry.last() != Some(&index) {
385 entry.push(index);
386 }
387 }
388 }
389 Self { importers }
390 }
391
392 fn of(&self, file_id: FileId) -> &[usize] {
393 self.importers.get(&file_id).map_or(&[], Vec::as_slice)
394 }
395}
396
397fn is_star_from_virtual_module(info: &ReExportInfo) -> bool {
399 info.imported_name == "*"
400 && info.exported_name == "*"
401 && auto_import_virtual_module(&info.source).is_some()
402}
403
404fn auto_import_virtual_module(source: &str) -> Option<&'static str> {
406 if !source.starts_with('#') {
407 return None;
408 }
409 AUTO_IMPORT_VIRTUAL_MODULES
410 .iter()
411 .copied()
412 .find(|module| *module == source)
413}
414
415fn kind_belongs_to(kind: AutoImportKind, virtual_module: &str) -> bool {
418 matches!(kind, AutoImportKind::DefaultComponent) == (virtual_module == COMPONENTS_MODULE)
419}
420
421fn scope_covers(scope: Option<&[&Path]>, path: &Path) -> bool {
423 scope.is_none_or(|roots| roots.iter().any(|root| path.starts_with(root)))
424}
425
426fn credit_auto_import_name(
429 module: &mut ResolvedModule,
430 name: &str,
431 table: &AutoImportTable<'_>,
432 only_module: Option<&str>,
433) {
434 if is_auto_import_builtin(name) {
435 return;
436 }
437 let Some(targets) = table.get(name) else {
438 return;
439 };
440 for target in targets {
441 if target.file_id == module.file_id
442 || !scope_covers(target.scope.as_deref(), &module.path)
443 || only_module
444 .is_some_and(|virtual_module| !kind_belongs_to(target.kind, virtual_module))
445 {
446 continue;
447 }
448 module.resolved_imports.push(ResolvedImport {
449 info: synthetic_auto_import_info(name, target.kind),
450 target: ResolveResult::SyntheticAutoImport(target.file_id),
451 });
452 }
453}