mod declarations;
mod helpers;
mod visit_impl;
use oxc_ast::ast::{
Argument, BindingPattern, CallExpression, Expression, ImportExpression, ObjectPattern,
ObjectPropertyKind, Statement,
};
use oxc_span::Span;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::suppress::Suppression;
use crate::{
DynamicImportInfo, DynamicImportPattern, ExportInfo, ExportName, ImportInfo, MemberAccess,
MemberInfo, ModuleInfo, ReExportInfo, RequireCallInfo, VisibilityTag,
};
#[derive(Default)]
pub(crate) struct ModuleInfoExtractor {
pub(crate) exports: Vec<ExportInfo>,
pub(crate) imports: Vec<ImportInfo>,
pub(crate) re_exports: Vec<ReExportInfo>,
pub(crate) dynamic_imports: Vec<DynamicImportInfo>,
pub(crate) dynamic_import_patterns: Vec<DynamicImportPattern>,
pub(crate) require_calls: Vec<RequireCallInfo>,
pub(crate) member_accesses: Vec<MemberAccess>,
pub(crate) whole_object_uses: Vec<String>,
pub(crate) has_cjs_exports: bool,
handled_require_spans: FxHashSet<Span>,
handled_import_spans: FxHashSet<Span>,
namespace_binding_names: Vec<String>,
instance_binding_names: FxHashMap<String, String>,
namespace_depth: u32,
pending_namespace_members: Vec<MemberInfo>,
}
impl ModuleInfoExtractor {
pub(crate) fn new() -> Self {
Self::default()
}
fn resolve_instance_member_accesses(&mut self) {
if self.instance_binding_names.is_empty() {
return;
}
let additional_accesses: Vec<MemberAccess> = self
.member_accesses
.iter()
.filter_map(|access| {
self.instance_binding_names
.get(&access.object)
.map(|class_name| MemberAccess {
object: class_name.clone(),
member: access.member.clone(),
})
})
.collect();
let additional_whole: Vec<String> = self
.whole_object_uses
.iter()
.filter_map(|name| self.instance_binding_names.get(name).cloned())
.collect();
self.member_accesses.extend(additional_accesses);
self.whole_object_uses.extend(additional_whole);
}
fn push_type_export(&mut self, name: &str, span: Span) {
self.exports.push(ExportInfo {
name: ExportName::Named(name.to_string()),
local_name: Some(name.to_string()),
is_type_only: true,
visibility: VisibilityTag::None,
span,
members: vec![],
super_class: None,
});
}
pub(crate) fn into_module_info(
mut self,
file_id: fallow_types::discover::FileId,
content_hash: u64,
suppressions: Vec<Suppression>,
) -> ModuleInfo {
self.resolve_instance_member_accesses();
ModuleInfo {
file_id,
exports: self.exports,
imports: self.imports,
re_exports: self.re_exports,
dynamic_imports: self.dynamic_imports,
dynamic_import_patterns: self.dynamic_import_patterns,
require_calls: self.require_calls,
member_accesses: self.member_accesses,
whole_object_uses: self.whole_object_uses,
has_cjs_exports: self.has_cjs_exports,
content_hash,
suppressions,
unused_import_bindings: Vec::new(),
line_offsets: Vec::new(),
complexity: Vec::new(),
flag_uses: Vec::new(),
}
}
pub(crate) fn merge_into(mut self, info: &mut ModuleInfo) {
self.resolve_instance_member_accesses();
info.imports.extend(self.imports);
info.exports.extend(self.exports);
info.re_exports.extend(self.re_exports);
info.dynamic_imports.extend(self.dynamic_imports);
info.dynamic_import_patterns
.extend(self.dynamic_import_patterns);
info.require_calls.extend(self.require_calls);
info.member_accesses.extend(self.member_accesses);
info.whole_object_uses.extend(self.whole_object_uses);
info.has_cjs_exports |= self.has_cjs_exports;
}
}
fn extract_destructured_names(obj_pat: &ObjectPattern<'_>) -> Vec<String> {
if obj_pat.rest.is_some() {
return Vec::new();
}
obj_pat
.properties
.iter()
.filter_map(|prop| prop.key.static_name().map(|n| n.to_string()))
.collect()
}
fn try_extract_require<'a, 'b>(
init: &'b Expression<'a>,
) -> Option<(&'b CallExpression<'a>, &'b str)> {
let Expression::CallExpression(call) = init else {
return None;
};
let Expression::Identifier(callee) = &call.callee else {
return None;
};
if callee.name != "require" {
return None;
}
let Some(Argument::StringLiteral(lit)) = call.arguments.first() else {
return None;
};
Some((call, &lit.value))
}
fn try_extract_dynamic_import<'a, 'b>(
init: &'b Expression<'a>,
) -> Option<(&'b ImportExpression<'a>, &'b str)> {
let import_expr = match init {
Expression::AwaitExpression(await_expr) => match &await_expr.argument {
Expression::ImportExpression(imp) => imp,
_ => return None,
},
Expression::ImportExpression(imp) => imp,
_ => return None,
};
let Expression::StringLiteral(lit) = &import_expr.source else {
return None;
};
Some((import_expr, &lit.value))
}
fn try_extract_arrow_wrapped_import<'a, 'b>(
arguments: &'b [Argument<'a>],
) -> Option<(&'b ImportExpression<'a>, &'b str)> {
for arg in arguments {
let import_expr = match arg {
Argument::ArrowFunctionExpression(arrow) => {
if arrow.expression {
let Some(Statement::ExpressionStatement(expr_stmt)) =
arrow.body.statements.first()
else {
continue;
};
let Expression::ImportExpression(imp) = &expr_stmt.expression else {
continue;
};
imp
} else {
let Some(imp) = extract_import_from_return_body(&arrow.body.statements) else {
continue;
};
imp
}
}
Argument::FunctionExpression(func) => {
let Some(body) = &func.body else {
continue;
};
let Some(imp) = extract_import_from_return_body(&body.statements) else {
continue;
};
imp
}
_ => continue,
};
let Expression::StringLiteral(lit) = &import_expr.source else {
continue;
};
return Some((import_expr, &lit.value));
}
None
}
fn extract_import_from_return_body<'a, 'b>(
stmts: &'b [Statement<'a>],
) -> Option<&'b ImportExpression<'a>> {
for stmt in stmts.iter().rev() {
if let Statement::ReturnStatement(ret) = stmt
&& let Some(Expression::ImportExpression(imp)) = &ret.argument
{
return Some(imp);
}
}
None
}
struct ImportThenCallback {
source: String,
import_span: oxc_span::Span,
destructured_names: Vec<String>,
local_name: Option<String>,
}
fn try_extract_import_then_callback(expr: &CallExpression<'_>) -> Option<ImportThenCallback> {
let Expression::StaticMemberExpression(member) = &expr.callee else {
return None;
};
if member.property.name != "then" {
return None;
}
let Expression::ImportExpression(import_expr) = &member.object else {
return None;
};
let Expression::StringLiteral(lit) = &import_expr.source else {
return None;
};
let source = lit.value.to_string();
let import_span = import_expr.span;
let first_arg = expr.arguments.first()?;
match first_arg {
Argument::ArrowFunctionExpression(arrow) => {
let param = arrow.params.items.first()?;
match ¶m.pattern {
BindingPattern::ObjectPattern(obj_pat) => Some(ImportThenCallback {
source,
import_span,
destructured_names: extract_destructured_names(obj_pat),
local_name: None,
}),
BindingPattern::BindingIdentifier(id) => {
let param_name = id.name.to_string();
if arrow.expression
&& let Some(Statement::ExpressionStatement(expr_stmt)) =
arrow.body.statements.first()
&& let Some(names) =
extract_member_names_from_expr(&expr_stmt.expression, ¶m_name)
{
return Some(ImportThenCallback {
source,
import_span,
destructured_names: names,
local_name: None,
});
}
Some(ImportThenCallback {
source,
import_span,
destructured_names: Vec::new(),
local_name: Some(param_name),
})
}
_ => None,
}
}
Argument::FunctionExpression(func) => {
let param = func.params.items.first()?;
match ¶m.pattern {
BindingPattern::ObjectPattern(obj_pat) => Some(ImportThenCallback {
source,
import_span,
destructured_names: extract_destructured_names(obj_pat),
local_name: None,
}),
BindingPattern::BindingIdentifier(id) => Some(ImportThenCallback {
source,
import_span,
destructured_names: Vec::new(),
local_name: Some(id.name.to_string()),
}),
_ => None,
}
}
_ => None,
}
}
fn extract_member_names_from_expr(expr: &Expression<'_>, param_name: &str) -> Option<Vec<String>> {
match expr {
Expression::StaticMemberExpression(member) => {
if let Expression::Identifier(obj) = &member.object
&& obj.name == param_name
{
Some(vec![member.property.name.to_string()])
} else {
None
}
}
Expression::ObjectExpression(obj) => extract_member_names_from_object(obj, param_name),
Expression::ParenthesizedExpression(paren) => {
extract_member_names_from_expr(&paren.expression, param_name)
}
_ => None,
}
}
fn extract_member_names_from_object(
obj: &oxc_ast::ast::ObjectExpression<'_>,
param_name: &str,
) -> Option<Vec<String>> {
let mut names = Vec::new();
for prop in &obj.properties {
if let ObjectPropertyKind::ObjectProperty(p) = prop
&& let Expression::StaticMemberExpression(member) = &p.value
&& let Expression::Identifier(obj) = &member.object
&& obj.name == param_name
{
names.push(member.property.name.to_string());
}
}
if names.is_empty() { None } else { Some(names) }
}
#[cfg(all(test, not(miri)))]
mod tests;