use rustc_hash::{FxHashMap, FxHashSet};
use fallow_types::discover::FileId;
use fallow_types::extract::{ImportedName, NamespaceObjectAlias};
use crate::resolve::{ResolveResult, ResolvedModule};
use super::ModuleGraph;
use super::narrowing::{
create_synthetic_exports_for_star_re_exports, mark_member_exports_referenced,
};
use super::types::ReferenceKind;
struct PendingCredit {
target_module_idx: usize,
member: String,
consumer_file_id: FileId,
import_span: oxc_span::Span,
}
pub(super) fn propagate_cross_package_aliases(
graph: &mut ModuleGraph,
module_by_id: &FxHashMap<FileId, &ResolvedModule>,
) {
let pending = collect_pending_credits(graph, module_by_id);
apply_pending_credits(graph, &pending);
}
fn collect_pending_credits(
graph: &ModuleGraph,
module_by_id: &FxHashMap<FileId, &ResolvedModule>,
) -> Vec<PendingCredit> {
let mut pending = Vec::new();
for alias_module in module_by_id.values() {
if alias_module.namespace_object_aliases.is_empty() {
continue;
}
let alias_file_id = alias_module.file_id;
for alias in &alias_module.namespace_object_aliases {
let Some(namespace_target_id) = resolve_namespace_target(alias_module, alias) else {
continue;
};
let Some(target_module_idx) = module_index_for_file(graph, namespace_target_id) else {
continue;
};
let reachable =
enumerate_alias_reachable_barrels(graph, alias_file_id, &alias.via_export_name);
collect_credits_for_alias(
module_by_id,
alias_file_id,
alias,
target_module_idx,
&reachable,
&mut pending,
);
}
}
pending
}
fn enumerate_alias_reachable_barrels(
graph: &ModuleGraph,
alias_file_id: FileId,
via_export_name: &str,
) -> FxHashSet<(FileId, String)> {
let mut reachable: FxHashSet<(FileId, String)> = FxHashSet::default();
reachable.insert((alias_file_id, via_export_name.to_string()));
let mut frontier: Vec<(FileId, String)> = vec![(alias_file_id, via_export_name.to_string())];
while let Some((source_file, source_name)) = frontier.pop() {
for (idx, module) in graph.modules.iter().enumerate() {
for edge in &module.re_exports {
if edge.source_file != source_file {
continue;
}
let exported_name = if edge.imported_name == source_name {
edge.exported_name.clone()
} else if edge.imported_name == "*" && edge.exported_name == "*" {
source_name.clone()
} else {
continue;
};
#[expect(
clippy::cast_possible_truncation,
reason = "file count is bounded by project size, well under u32::MAX"
)]
let barrel_file = FileId(idx as u32);
let pair = (barrel_file, exported_name);
if reachable.insert(pair.clone()) {
frontier.push(pair);
}
}
}
}
reachable
}
fn resolve_namespace_target(
alias_module: &ResolvedModule,
alias: &NamespaceObjectAlias,
) -> Option<FileId> {
alias_module.resolved_imports.iter().find_map(|import| {
if import.info.local_name != alias.namespace_local {
return None;
}
if !matches!(import.info.imported_name, ImportedName::Namespace) {
return None;
}
match &import.target {
ResolveResult::InternalModule(file_id) => Some(*file_id),
_ => None,
}
})
}
fn module_index_for_file(graph: &ModuleGraph, file_id: FileId) -> Option<usize> {
let idx = file_id.0 as usize;
if idx >= graph.modules.len() {
return None;
}
Some(idx)
}
fn collect_credits_for_alias(
module_by_id: &FxHashMap<FileId, &ResolvedModule>,
alias_file_id: FileId,
alias: &NamespaceObjectAlias,
target_module_idx: usize,
reachable: &FxHashSet<(FileId, String)>,
pending: &mut Vec<PendingCredit>,
) {
let prefix_match = format!(".{}", alias.suffix);
for consumer in module_by_id.values() {
if consumer.file_id == alias_file_id {
continue;
}
for import in &consumer.resolved_imports {
let ResolveResult::InternalModule(target_file_id) = &import.target else {
continue;
};
let imported_name = match &import.info.imported_name {
ImportedName::Named(n) => n.as_str(),
ImportedName::Default => "default",
_ => continue,
};
if !reachable.contains(&(*target_file_id, imported_name.to_string())) {
continue;
}
let consumer_local = import.info.local_name.as_str();
if consumer_local.is_empty() {
continue;
}
let expected_object = format!("{consumer_local}{prefix_match}");
for access in &consumer.member_accesses {
if access.object != expected_object {
continue;
}
pending.push(PendingCredit {
target_module_idx,
member: access.member.clone(),
consumer_file_id: consumer.file_id,
import_span: import.info.span,
});
}
}
}
}
fn apply_pending_credits(graph: &mut ModuleGraph, pending: &[PendingCredit]) {
type GroupKey = (usize, FileId, oxc_span::Span);
let mut groups: FxHashMap<GroupKey, Vec<String>> = FxHashMap::default();
for credit in pending {
groups
.entry((
credit.target_module_idx,
credit.consumer_file_id,
credit.import_span,
))
.or_default()
.push(credit.member.clone());
}
for ((target_module_idx, consumer_file_id, import_span), members) in groups {
let module = &mut graph.modules[target_module_idx];
let found_members = mark_member_exports_referenced(
&mut module.exports,
consumer_file_id,
&members,
import_span,
ReferenceKind::NamespaceImport,
);
create_synthetic_exports_for_star_re_exports(
&mut module.exports,
&module.re_exports,
consumer_file_id,
&members,
&found_members,
import_span,
);
}
}