use oxc::allocator::{GetAddress, UnstableAddress};
use oxc::ast::{
AstKind, MemberExpressionKind,
ast::{self, AssignmentExpression, Expression, IdentifierReference, PropertyKey},
};
use oxc::span::Span;
use oxc_str::CompactStr;
use rolldown_common::{AstScopes, EcmaModuleAstUsage};
use rolldown_ecmascript_utils::ExpressionExt;
use crate::ast_scanner::IdentifierReferenceKind;
use super::AstScanner;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CommonjsExportSymbolUsage {
pub read: u32,
pub write: u32,
pub bailout: bool,
}
impl CommonjsExportSymbolUsage {
pub fn can_be_removed(&self) -> bool {
!self.bailout && self.read == 0 && self.write == 1
}
pub fn can_be_inlined(&self) -> bool {
!self.bailout && self.write == 1
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommonJsAstType {
ExportsPropWrite(CompactStr),
ExportsRead,
EsModuleFlag,
Reexport(Span),
}
impl<'me, 'ast: 'me> AstScanner<'me, 'ast> {
pub fn commonjs_export_analyzer(
&self,
ident_ref: &IdentifierReference,
ty: CjsGlobalAssignmentType,
) -> Option<CommonJsAstType> {
let cursor = self.visit_path.len() - 1;
let parent = self.visit_path.get(cursor)?;
match parent {
kind if kind.is_member_expression_kind() => match ty {
CjsGlobalAssignmentType::ModuleExportsAssignment => {
let member_expr = kind.as_member_expression_kind().unwrap();
let property_name = member_expr.static_property_name()?;
if property_name != "exports" {
return None;
}
let parent_parent_kind = self.visit_path.get(cursor - 1)?;
match parent_parent_kind {
parent_parent_kind if parent_parent_kind.is_member_expression_kind() => {
let parent_parent = parent_parent_kind.as_member_expression_kind().unwrap();
Self::check_assignment_target_property(
&parent_parent,
self.visit_path.get(cursor - 2)?,
)
}
AstKind::CallExpression(call_expr)
if call_expr
.arguments
.first()
.is_some_and(|arg| arg.address() == parent.address()) =>
{
self.check_object_define_property(call_expr)
}
AstKind::AssignmentExpression(assignment_expr) => {
self.check_assignment_is_cjs_reexport(assignment_expr)
}
_ => None,
}
}
CjsGlobalAssignmentType::ExportsAssignment => {
let member_expr = kind.as_member_expression_kind().unwrap();
Self::check_assignment_target_property(&member_expr, self.visit_path.get(cursor - 1)?)
}
},
AstKind::CallExpression(call_expr)
if call_expr
.arguments
.first()
.is_some_and(|arg| arg.address() == ident_ref.unstable_address()) =>
{
self.check_object_define_property(call_expr)
}
_ => None,
}
}
pub fn update_ast_usage_for_commonjs_export(&mut self, v: Option<&CommonJsAstType>) {
match v.as_ref() {
Some(CommonJsAstType::EsModuleFlag) => {
self.result.ast_usage.insert(EcmaModuleAstUsage::EsModuleFlag);
}
Some(CommonJsAstType::ExportsRead) => {
self.result.ast_usage.remove(EcmaModuleAstUsage::AllStaticExportPropertyAccess);
}
Some(CommonJsAstType::ExportsPropWrite(prop)) if prop == "*" => {
self.result.ast_usage.remove(EcmaModuleAstUsage::AllStaticExportPropertyAccess);
}
Some(CommonJsAstType::Reexport(span)) => {
self.result.ast_usage.insert(EcmaModuleAstUsage::IsCjsReexport);
self.result.cjs_reexport_require_spans.push(*span);
}
_ => {}
}
}
fn check_object_define_property(
&self,
call_expr: &ast::CallExpression<'_>,
) -> Option<CommonJsAstType> {
is_object_define_property_es_module(&self.result.symbol_ref_db.ast_scopes, call_expr)
}
fn check_assignment_target_property(
member_expr: &MemberExpressionKind,
parent: &AstKind<'ast>,
) -> Option<CommonJsAstType> {
let static_property_name = member_expr.static_property_name();
if !member_expr.is_assigned_to_in_parent(parent) {
return Some(CommonJsAstType::ExportsRead);
}
let Some(static_property_name) = static_property_name else {
return Some(CommonJsAstType::ExportsPropWrite(CompactStr::from("*")));
};
if static_property_name.as_str() != "__esModule" {
return Some(CommonJsAstType::ExportsPropWrite(CompactStr::from(static_property_name)));
}
let assignment_expr = parent.as_assignment_expression()?;
let Expression::BooleanLiteral(bool_lit) = &assignment_expr.right else {
return Some(CommonJsAstType::ExportsPropWrite("__esModule".into()));
};
bool_lit.value.then_some(CommonJsAstType::EsModuleFlag)
}
fn check_assignment_is_cjs_reexport(
&self,
assignment_expr: &AssignmentExpression<'ast>,
) -> Option<CommonJsAstType> {
let call_expr = assignment_expr.right.as_call_expression()?;
let callee = call_expr.callee.as_identifier()?;
if !(callee.name == "require"
&& matches!(self.resolve_identifier_reference(callee), IdentifierReferenceKind::Global,)
&& call_expr.arguments.len() == 1)
{
return None;
}
call_expr
.arguments
.first()?
.as_expression()?
.as_string_literal()
.is_some()
.then_some(CommonJsAstType::Reexport(call_expr.span))
}
}
#[derive(Debug, Clone, Copy)]
pub enum CjsGlobalAssignmentType {
ModuleExportsAssignment,
ExportsAssignment,
}
pub fn is_object_define_property_es_module(
scope: &AstScopes,
call_expr: &ast::CallExpression<'_>,
) -> Option<CommonJsAstType> {
let callee = call_expr.callee.as_member_expression()?;
let callee_object = callee.object().as_identifier()?;
if !scope.is_unresolved(callee_object.reference_id()) {
return None;
}
let key_eq_object = callee_object.name == "Object";
let property_eq_define_property = callee.static_property_name()? == "defineProperty";
if !(key_eq_object && property_eq_define_property) {
return Some(CommonJsAstType::ExportsRead);
}
let first = call_expr.arguments.first()?.as_expression()?.as_identifier()?;
if !scope.is_unresolved(first.reference_id()) || first.name != "exports" {
return None;
}
let second = call_expr.arguments.get(1)?;
let Some(string_lit) = second.as_expression().and_then(|item| item.as_string_literal()) else {
return Some(CommonJsAstType::ExportsPropWrite("*".into()));
};
if string_lit.value != "__esModule" {
return Some(CommonJsAstType::ExportsPropWrite(string_lit.value.as_str().into()));
}
let third = call_expr.arguments.get(2)?;
let ret = third
.as_expression()
.and_then(|item| match item {
Expression::ObjectExpression(expr) => Some(expr),
_ => None,
})
.is_some_and(|obj_expr| match obj_expr.properties.as_slice() {
[ast::ObjectPropertyKind::ObjectProperty(kind)] => match (&kind.key, &kind.value) {
(PropertyKey::StaticIdentifier(id), Expression::BooleanLiteral(bool_lit)) => {
id.name == "value" && bool_lit.value
}
_ => false,
},
_ => false,
});
if ret {
Some(CommonJsAstType::EsModuleFlag)
} else {
Some(CommonJsAstType::ExportsPropWrite("__esModule".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxc::{
allocator::Allocator, ast::ast::Program, parser::Parser, semantic::SemanticBuilder,
span::SourceType,
};
use rolldown_common::AstScopes;
fn create_ast_scopes_and_program_from_source<'ast, 'a: 'ast>(
source: &'ast str,
allocator: &'a Allocator,
) -> (AstScopes, Program<'ast>) {
let source_type = SourceType::default();
let ret = Parser::new(allocator, source, source_type).parse();
let program = ret.program;
let semantic_ret = SemanticBuilder::new().build(&program);
(AstScopes::new(semantic_ret.semantic.into_scoping()), program)
}
fn extract_call_expr<'a>(
program: &'a oxc::ast::ast::Program<'a>,
) -> Option<&'a oxc::ast::ast::CallExpression<'a>> {
let first = program.body.first()?;
let oxc::ast::ast::Statement::ExpressionStatement(expr_stmt) = first else {
return None;
};
expr_stmt.expression.as_call_expression()
}
#[test]
fn test_is_object_define_property_es_module_valid() {
let source = r#"Object.defineProperty(exports, "__esModule", { value: true });"#;
let allocator = Allocator::default();
let (ast_scopes, program) = create_ast_scopes_and_program_from_source(source, &allocator);
if let Some(call_expr) = extract_call_expr(&program) {
let result = is_object_define_property_es_module(&ast_scopes, call_expr);
assert_eq!(result, Some(CommonJsAstType::EsModuleFlag));
}
}
#[test]
fn test_is_object_define_property_es_module_invalid() {
let source = r#"Object.defineProperty(exports, "notEsModule", { value: true });"#;
let allocator = Allocator::default();
let (ast_scopes, program) = create_ast_scopes_and_program_from_source(source, &allocator);
if let Some(call_expr) = extract_call_expr(&program) {
let result = is_object_define_property_es_module(&ast_scopes, call_expr);
assert_eq!(result, Some(CommonJsAstType::ExportsPropWrite("notEsModule".into())));
}
}
#[test]
fn test_is_object_define_property_with_false_value() {
let source = r#"Object.defineProperty(exports, "__esModule", { value: false });"#;
let allocator = Allocator::default();
let (ast_scopes, program) = create_ast_scopes_and_program_from_source(source, &allocator);
if let Some(call_expr) = extract_call_expr(&program) {
let result = is_object_define_property_es_module(&ast_scopes, call_expr);
assert_eq!(result, Some(CommonJsAstType::ExportsPropWrite("__esModule".into())));
}
}
}