use rustc_hash::{FxHashMap, FxHashSet};
use fallow_types::extract::MemberKind;
use super::ExportKey;
use crate::discover::FileId;
use crate::extract::ModuleInfo;
use crate::resolve::{ResolveResult, ResolvedModule};
const GLOBAL_BASE_NAMES: &[&str] = &[
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
"Array",
"Map",
"Set",
"WeakMap",
"WeakSet",
"Promise",
"RegExp",
"Date",
"EventTarget",
"Event",
"CustomEvent",
"HTMLElement",
"ReadableStream",
"WritableStream",
"TransformStream",
"Request",
"Response",
"Headers",
"AbortController",
];
pub(super) fn credit_external_override_members(
resolved_modules: &[ResolvedModule],
modules: &[ModuleInfo],
accessed_members: &mut FxHashMap<ExportKey, FxHashSet<String>>,
) {
let module_by_id: FxHashMap<FileId, &ModuleInfo> = modules
.iter()
.map(|module| (module.file_id, module))
.collect();
for resolved in resolved_modules {
if !resolved.exports.iter().any(|export| {
export
.members
.iter()
.any(|member| member.override_base.is_some())
}) {
continue;
}
let names = ModuleNames::new(resolved, module_by_id.get(&resolved.file_id).copied());
for export in resolved.exports.iter() {
let credited: Vec<String> = export
.members
.iter()
.filter(|member| {
matches!(
member.kind,
MemberKind::ClassMethod | MemberKind::ClassProperty
) && member
.override_base
.as_deref()
.is_some_and(|base| names.is_external_base(base))
})
.map(|member| member.name.clone())
.collect();
if credited.is_empty() {
continue;
}
accessed_members
.entry(ExportKey::new(resolved.file_id, export.name.to_string()))
.or_default()
.extend(credited);
}
}
}
fn is_external_target(target: &ResolveResult, source: &str) -> bool {
match target {
ResolveResult::NpmPackage(_)
| ResolveResult::CommonJsNpmPackage(_)
| ResolveResult::ExternalFile(_) => true,
ResolveResult::Unresolvable(_) => has_url_scheme(source),
_ => false,
}
}
fn has_url_scheme(source: &str) -> bool {
let Some((scheme, _)) = source.split_once(':') else {
return false;
};
scheme.len() > 1
&& scheme.starts_with(|c: char| c.is_ascii_alphabetic())
&& scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
}
struct ModuleNames<'a> {
external_by_local: FxHashMap<&'a str, bool>,
declared: FxHashSet<&'a str>,
}
impl<'a> ModuleNames<'a> {
fn new(resolved: &'a ResolvedModule, module: Option<&'a ModuleInfo>) -> Self {
let mut external_by_local: FxHashMap<&'a str, bool> = FxHashMap::default();
for import in &resolved.resolved_imports {
let external = is_external_target(&import.target, &import.info.source);
external_by_local
.entry(import.info.local_name.as_str())
.and_modify(|all_external| *all_external &= external)
.or_insert(external);
}
let declared = resolved
.exports
.iter()
.filter_map(|export| export.local_name.as_deref())
.chain(
module
.into_iter()
.flat_map(|module| module.local_type_declarations.iter())
.map(|declaration| declaration.name.as_str()),
)
.collect();
Self {
external_by_local,
declared,
}
}
fn is_external_base(&self, root: &str) -> bool {
match self.external_by_local.get(root) {
Some(external) => *external,
None => GLOBAL_BASE_NAMES.contains(&root) && !self.declared.contains(root),
}
}
}