use skif_core::{HashMap, HashSet};
use std::rc::Rc;
use oxc_ast::ast::{
ArrowFunctionBody, Class, Declaration, ExportDefaultDeclarationKind, Expression, Function, FunctionBody, Program,
Statement, TSTypeName,
};
use oxc_ast_visit::{Visit, walk};
use crate::model::{Export, ExportKind, ImportedKind, ModuleInfo};
type Container = (String, Rc<HashSet<String>>);
struct TypeNames {
names: HashSet<String>,
signature_only: bool,
}
impl TypeNames {
fn new(signature_only: bool) -> Self {
Self {
names: HashSet::default(),
signature_only,
}
}
}
impl<'a> Visit<'a> for TypeNames {
fn visit_ts_type_name(&mut self, it: &TSTypeName<'a>) {
if let TSTypeName::IdentifierReference(id) = it {
self.names.insert(id.name.as_str().to_owned());
}
walk::walk_ts_type_name(self, it);
}
fn visit_function_body(&mut self, it: &FunctionBody<'a>) {
if !self.signature_only {
walk::walk_function_body(self, it);
}
}
fn visit_arrow_function_body(&mut self, it: &ArrowFunctionBody<'a>) {
if !self.signature_only {
walk::walk_arrow_function_body(self, it);
}
}
}
fn function_names(function: &Function<'_>, signature_only: bool) -> HashSet<String> {
let mut visitor = TypeNames::new(signature_only);
if let Some(parameters) = &function.type_parameters {
visitor.visit_ts_type_parameter_declaration(parameters);
}
visitor.visit_formal_parameters(&function.params);
if let Some(return_type) = &function.return_type {
visitor.visit_ts_type_annotation(return_type);
}
if let Some(body) = &function.body {
visitor.visit_function_body(body);
}
visitor.names
}
fn class_names(class: &Class<'_>) -> HashSet<String> {
let mut visitor = TypeNames::new(true);
visitor.visit_class(class);
visitor.names
}
fn expression_names(expression: &Expression<'_>) -> HashSet<String> {
let explicit_return = match expression.get_inner_expression() {
Expression::ArrowFunctionExpression(arrow) => arrow.return_type.is_some(),
Expression::FunctionExpression(function) => function.return_type.is_some(),
_ => false,
};
let mut visitor = TypeNames::new(explicit_return);
visitor.visit_expression(expression);
visitor.names
}
type Wanted<'w> = &'w dyn Fn(&str) -> bool;
fn containers(declaration: &Declaration<'_>, wanted: Wanted<'_>, out: &mut Vec<Container>) {
match declaration {
Declaration::FunctionDeclaration(function) => {
if let Some(id) = &function.id
&& wanted(id.name.as_str())
{
out.push((
id.name.as_str().to_owned(),
Rc::new(function_names(function, function.return_type.is_some())),
));
}
}
Declaration::ClassDeclaration(class) => {
if let Some(id) = &class.id
&& wanted(id.name.as_str())
{
out.push((id.name.as_str().to_owned(), Rc::new(class_names(class))));
}
}
Declaration::VariableDeclaration(variables) => {
for declarator in &variables.declarations {
let ids = declarator.id.get_binding_identifiers();
if !ids.iter().any(|id| wanted(id.name.as_str())) {
continue;
}
let names = Rc::new(if let Some(annotation) = &declarator.type_annotation {
let mut visitor = TypeNames::new(true);
visitor.visit_ts_type_annotation(annotation);
visitor.names
} else if let Some(init) = &declarator.init {
expression_names(init)
} else {
continue;
});
out.extend(ids.iter().map(|id| (id.name.as_str().to_owned(), Rc::clone(&names))));
}
}
_ => {}
}
}
fn type_container(declaration: &Declaration<'_>, wanted: Wanted<'_>, out: &mut Vec<Container>) {
let mut visitor = TypeNames::new(true);
let name = match declaration {
Declaration::TSTypeAliasDeclaration(alias) if wanted(alias.id.name.as_str()) => {
visitor.visit_ts_type_alias_declaration(alias);
&alias.id.name
}
Declaration::TSInterfaceDeclaration(interface) if wanted(interface.id.name.as_str()) => {
visitor.visit_ts_interface_declaration(interface);
&interface.id.name
}
_ => return,
};
out.push((name.as_str().to_owned(), Rc::new(visitor.names)));
}
fn import_bindings(info: &ModuleInfo) -> HashMap<(&str, &ImportedKind), &str> {
let mut bindings = HashMap::default();
for import in &info.imports {
for name in &import.names {
if let Some(local) = name.local.as_deref() {
bindings.entry((import.specifier.as_str(), &name.kind)).or_insert(local);
}
}
}
bindings
}
fn reexported_binding<'a>(bindings: &HashMap<(&str, &ImportedKind), &'a str>, export: &Export) -> Option<&'a str> {
if export.kind != ExportKind::ReExport || export.local_name.is_some() {
return None;
}
let from = export.from.as_deref()?;
let wanted = match export.imported_name.as_deref()? {
"default" => ImportedKind::Default,
name => ImportedKind::Named(name.to_owned()),
};
bindings.get(&(from, &wanted)).copied()
}
fn collect_containers(program: &Program<'_>, wanted: Wanted<'_>) -> Vec<Container> {
let mut found = Vec::new();
for statement in &program.body {
match statement {
Statement::ExportDeclaration(export) => {
containers(&export.declaration, wanted, &mut found);
type_container(&export.declaration, wanted, &mut found);
}
Statement::ExportDefaultDeclaration(export) => match &export.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
if let Some(id) = &function.id
&& wanted(id.name.as_str())
{
found.push((
id.name.as_str().to_owned(),
Rc::new(function_names(function, function.return_type.is_some())),
));
}
}
ExportDefaultDeclarationKind::ClassDeclaration(class) => {
if let Some(id) = &class.id
&& wanted(id.name.as_str())
{
found.push((id.name.as_str().to_owned(), Rc::new(class_names(class))));
}
}
_ => {}
},
other => {
if let Some(declaration) = other.as_declaration() {
containers(declaration, wanted, &mut found);
type_container(declaration, wanted, &mut found);
}
}
}
}
found
}
fn mentioned_by<'a>(
mentions: &HashMap<&'a str, HashSet<&'a str>>,
exported_as: &HashMap<&str, Vec<String>>,
) -> HashMap<&'a str, Vec<&'a str>> {
let mut by_name: HashMap<&str, Vec<&str>> = HashMap::default();
for (container, names) in mentions {
if !exported_as.contains_key(container) {
continue;
}
for name in names {
by_name.entry(*name).or_default().push(*container);
}
}
by_name
}
pub(crate) fn mark(program: &Program<'_>, info: &mut ModuleInfo) {
let exported_as: HashMap<&str, Vec<String>> = info
.exports
.iter()
.filter(|export| {
!export.is_type
&& !matches!(
export.kind,
ExportKind::ReExport | ExportKind::StarReExport | ExportKind::Type
)
})
.filter_map(|export| Some((export.local_name.as_deref()?, export.name.clone())))
.fold(HashMap::default(), |mut map, (local, name)| {
map.entry(local).or_default().push(name);
map
});
if exported_as.is_empty() {
return;
}
let found = collect_containers(program, &|name: &str| exported_as.contains_key(name));
let mut mentions: HashMap<&str, HashSet<&str>> = HashMap::default();
for (local, names) in &found {
mentions
.entry(local.as_str())
.or_default()
.extend(names.iter().map(String::as_str));
}
let mentioned_by = mentioned_by(&mentions, &exported_as);
let bindings = import_bindings(info);
let mut referenced_in: Vec<Vec<String>> = Vec::with_capacity(info.exports.len());
for export in &info.exports {
let mut containing = Vec::new();
if let Some(local) = export
.local_name
.as_deref()
.or_else(|| reexported_binding(&bindings, export))
{
for container in mentioned_by.get(local).into_iter().flatten() {
if *container != local {
containing.extend(exported_as.get(container).into_iter().flatten().cloned());
}
}
}
containing.sort();
containing.dedup();
referenced_in.push(containing);
}
for (export, containing) in info.exports.iter_mut().zip(referenced_in) {
export.referenced_in = containing;
}
}